PageSourceSearch

https://thread-fall.com/static/app.min.js?v=2772

js thread-fall.com collected 2026-09-24 14:54:32 UTC 2,123,021 bytes, 3,647 lines download raw bytes

1const _TF_INVITE_CODE_RE=/^[A-Za-z0-9_-]{4,64}$/;let _campSavedInviteCode="";function _applyTheme(e){e==="light"?document.documentElement.removeAttribute("data-theme"):document.documentElement.setAttribute("data-theme","dark")}function _setTheme(e){const t=e==="light"?"light":"dark";try{localStorage.setItem("theme",t)}catch{}_applyTheme(t),typeof _tfAlignLockup=="function"&&requestAnimationFrame(_tfAlignLockup),document.querySelectorAll(".profile-theme-toggle button[data-theme-pref]").forEach(n=>{n.classList.toggle("active",n.dataset.themePref===t)})}let _tfAlignRetries=0;function _tfAlignLockup(){const e=.6007194244604317;let t=!1;document.querySelectorAll(".tf-lockup").forEach(n=>{if(n.classList.contains("tf-lockup--nav")||n.classList.contains("tf-lockup--header")){const p=n.querySelector(".tf-wordmark");p&&(p.style.transform="");return}const a=n.querySelector(".tf-lockup-mark"),s=n.querySelector(".tf-wordmark"),i=s&&s.querySelector(".pipe");if(!a||!s||!i)return;if(!i.offsetWidth){t=!0;return}const o=s.style.transition;s.style.transition="none",s.style.transform="",s.offsetWidth;const r=a.getBoundingClientRect(),c=i.getBoundingClientRect();if(!r.width){s.style.transition=o,t=!0;return}const l=r.left+r.width*e,d=(c.left+c.right)/2,u=l-d;Math.abs(u)>.5?s.style.transform=`translateX(${u.toFixed(2)}px)`:_tfAlignRetries=0,s.offsetWidth,s.style.transition=o}),t&&_tfAlignRetries<12&&(_tfAlignRetries++,setTimeout(_tfAlignLockup,80))}try{_applyTheme(localStorage.getItem("theme")||"dark")}catch{_applyTheme("dark")}document.fonts&&document.fonts.ready?document.fonts.ready.then(()=>requestAnimationFrame(_tfAlignLockup)):window.addEventListener("load",()=>requestAnimationFrame(_tfAlignLockup)),window.addEventListener("resize",()=>requestAnimationFrame(_tfAlignLockup)),document.addEventListener("click",e=>{const t=e.target.closest&&e.target.closest(".profile-theme-toggle button[data-theme-pref]");t&&_setTheme(t.dataset.themePref)});function getStoredToken(){return localStorage.getItem("tf_jwt")||""}function _tfJwtSub(e){try{const t=String(e||"").split(".")[1]||"",n=atob(t.replace(/-/g,"+").replace(/_/g,"/"));return String(JSON.parse(n).sub||"")}catch{return""}}function setStoredToken(e){try{e&&typeof _tfHeldRecording<"u"&&_tfHeldRecording&&_tfHeldRecording.ownerSub&&_tfJwtSub(e)!==_tfHeldRecording.ownerSub&&(_liveTel("held_recording_discard",{bytes:_tfHeldRecording.blob?.size||0,on:"account_switch"}),_tfHeldRecording=null,_hideCaptureAlert())}catch{}try{e?localStorage.setItem("tf_jwt",e):localStorage.removeItem("tf_jwt")}catch{}}window._setAdminToken=e=>{try{return localStorage.setItem("tf_admin_token",(e||"").trim()),"admin token set"}catch(t){return"failed: "+t.message}},window._clearAdminToken=()=>{try{return localStorage.removeItem("tf_admin_token"),"admin token cleared"}catch(e){return"failed: "+e.message}};const _TFW_CODES=["api_failed","unhandled_rejection","script_error","render_failed"],_TFW_MAX=3,_TFW_WINDOW_MS=10*60*1e3;let _tfwSent=[];function _tfwRoute(e){return typeof e!="string"||!e.startsWith("/api/")?null:e.split("?")[0].replace(/\/\d+(?=\/|$)/g,"/{id}").slice(0,110)}function _tfwReport(e,t,n){try{if(!_TFW_CODES.includes(e))return;const a=Date.now();if(_tfwSent=_tfwSent.filter(i=>a-i<_TFW_WINDOW_MS),_tfwSent.length>=_TFW_MAX||!getStoredToken())return;_tfwSent.push(a);const s={code:e,status:t||null,route:_tfwRoute(n)};try{currentCampaign&&currentCampaign.id&&(s.campaign_id=currentCampaign.id)}catch{}fetch("/api/gm-watch/client-error",{method:"POST",credentials:"include",keepalive:!0,headers:{"Content-Type":"application/json",Authorization:"Bearer "+getStoredToken()},body:JSON.stringify(s)}).catch(()=>{})}catch{}}window.addEventListener("error",e=>{e&&e.message==="Script error."||_tfwReport("script_error",null,null)}),window.addEventListener("unhandledrejection",()=>_tfwReport("unhandled_rejection",null,null));async function api(e,t,n){const a={"Content-Type":"application/json"},s=getStoredToken();if(s&&(a.Authorization="Bearer "+s),typeof t=="string"&&t.startsWith("/api/admin")){const r=(()=>{try{return localStorage.getItem("tf_admin_token")}catch{return null}})();r&&(a["X-Admin-Token"]=r)}const i={method:e,credentials:"include",headers:a};n&&(i.body=JSON.stringify(n)),console.log("[api]",e,t,"hasToken:",!!s);const o=await fetch(t,i);if(console.log("[api]",e,t,"->",o.status),o.status===401&&!t.startsWith("/api/auth/"))throw setStoredToken(null),currentRole=null,document.getElementById("live-session-screen")?.classList.add("hidden"),showAuthView("login"),showScreen("auth-screen"),new Error("Session expired, please log in again.");if(!o.ok){if(o.status>=500&&_tfwReport("api_failed",o.status,t),o.status===429){const u=new Error("Rate limit reached, please wait a moment and try again.");throw u.status=429,u}const r=await o.text().catch(()=>"");let c="";try{c=JSON.parse(r).detail||""}catch{c=r}let l;if(typeof c=="string")l=c;else if(c&&typeof c=="object"&&!Array.isArray(c)&&typeof c.message=="string")l=c.message;else if(Array.isArray(c)&&c.length&&c[0]&&typeof c[0].msg=="string"){const u=c[0],p=Array.isArray(u.loc)?u.loc.filter(f=>f!=="body"&&typeof f=="string").join("."):"",m=String(u.msg).replace(/^Value error,\s*/i,"");l=p?`${p}: ${m}`:m}else l=`HTTP ${o.status}`;const d=new Error(l);throw d.status=o.status,d.detail=c,d}if((e==="PUT"||e==="POST"||e==="DELETE")&&typeof t=="string"&&!t.endsWith("/generate-recap")){const r=t.match(/\/sessions\/(\d+)(\b|\/)/);if(r&&typeof _recapUpToDateLocal<"u"){const c=parseInt(r[1],10);!Number.isNaN(c)&&_recapUpToDateLocal[c]&&delete _recapUpToDateLocal[c]}}return o.status===204?null:o.json()}const VISIT_PING_MS=10*60*1e3;let _visitPingLast=Date.now();function _visitPing(){document.visibilityState==="visible"&&getStoredToken()&&(Date.now()-_visitPingLast<VISIT_PING_MS||(_visitPingLast=Date.now(),api("POST","/api/auth/visit").
1catch(()=>{})))}["pointerdown","keydown"].forEach(e=>document.addEventListener(e,_visitPing,{capture:!0,passive:!0}));async function _apiAllPages(e){let t=[],n=null;do{const a=n?`${e}${e.includes("?")?"&":"?"}cursor=${n}`:e,s=await api("GET",a);if(Array.isArray(s)){t=t.concat(s);break}t=t.concat(s.items||[]),n=s.next_cursor||null}while(n);return t}let currentRole=null,currentUser=null,_realRole=null,_realIsAdmin=null;function _isGm(){return currentRole==="gm"||currentRole==="co_gm"}function _isReadOnlyRole(){return currentRole==="observer"||currentRole==="viewer"}function _tfMaybeObserverNotice(){if(currentRole!=="observer"||!currentCampaign)return;const e=`tf_observer_notice_${currentCampaign.id}`;try{if(localStorage.getItem(e))return;localStorage.setItem(e,"1")}catch{}showToast("You're an Observer in this campaign. You can read and watch along, but you can't post or edit.",7e3)}let _consentedSessionId,_pendingRecStart=!1,_recConsentAcknowledged=!1;const LIVE_LOG_POLL_MS=2e3,LIVE_LOG_POLL_MS_ABLY=1e4,LIVE_TRANSCRIPT_POLL_MS=5e3,_TF_POLL_FIRST_TICK_MS=1200;let _liveLogPollTimer=null,_liveLogMaxId=0,_liveLogTyping=!1,_liveTranscriptPollTimer=null,_liveTranscriptPollLastText="",_ablyClient=null,_ablyChannel=null,_ablyControl=null,_ablyConnected=!1;async function _ablyConnect(e,t){if(!(typeof Ably>"u")&&!(_ablyConnected&&_ablyChannel?.name===`live:${e}:${t}`)){_ablyDisconnect();try{const n=await api("GET",`/api/campaigns/${e}/live-token`);_ablyClient=new Ably.Realtime({token:n.token,clientId:n.clientId,authCallback:async(i,o)=>{try{const r=await api("GET",`/api/campaigns/${e}/live-token`);o(null,r.token)}catch(r){o(r,null)}}}),await new Promise((i,o)=>{const r=setTimeout(()=>o(new Error("Ably connection timeout")),8e3);_ablyClient.connection.once("connected",()=>{clearTimeout(r),i()}),_ablyClient.connection.once("failed",()=>{clearTimeout(r),o(new Error("Ably connection failed"))})});const a=`live:${e}:${t}`;_ablyChannel=_ablyClient.channels.get(a),_ablyChannel.subscribe("log_entry",i=>{_onAblyLogEntry(i.data)}),_ablyChannel.subscribe("transcript_update",i=>{const o=i.data?.text;!o||liveRecording||(_liveTranscriptPollLastText=o,_applyPolledTranscript(o))}),_ablyChannel.subscribe("speaker_relabel",i=>{const o=i.data?.from,r=i.data?.to;o&&r&&_absorbSpeakerRelabel(o,r)}),_ablyChannel.subscribe("speaker_relabel_line",i=>{const o=i.data?.from,r=i.data?.to,c=i.data?.excerpt;o&&r&&_absorbSpeakerRelabelLine(o,r,c)});const s=`live:${e}:control`;_ablyControl=_ablyClient.channels.get(s),_ablyControl.subscribe("session_start",()=>{_watcherIsLive=!0;const i=document.getElementById("feed-live-indicator");i&&i.classList.remove("hidden"),_enforceLiveConsent()}),_ablyControl.subscribe("map_pin",i=>{_onLiveMapPinEvent(i&&i.data||{})}),_ablyControl.subscribe("party_pos",i=>{_onLivePartyPosEvent(i&&i.data||{})}),_ablyControl.subscribe("countdowns_changed",()=>{typeof _cdLoad=="function"&&_cdLoad()});try{await _ablyChannel.presence.enter({userId:currentUser?.id,name:currentUser?.display_name||"Unknown",role:currentRole}),_ablyChannel.presence.subscribe(()=>_onAblyPresenceChange())}catch(i){console.warn("[ably] presence enter failed (non-fatal):",i.message)}_ablyConnected=!0,console.log("[ably] connected \u2014",a),_ablyClient.connection.on("disconnected",()=>{_ablyConnected&&(_ablyConnected=!1,console.warn("[ably] disconnected \u2014 switching to full-rate poll"),_startLiveLogPoll())}),_ablyClient.connection.on("connected",()=>{_ablyConnected||(_ablyConnected=!0,console.log("[ably] reconnected \u2014 relaxing poll interval"),_startLiveLogPoll())}),_startLiveLogPoll()}catch(n){n.message?.includes("503")||console.warn("[ably] unavailable, falling back to polling:",n.message),_ablyConnected=!1,_ablyClient=null}}}function _ablyDisconnect(){if(_ablyChannel){try{_ablyChannel.presence.leave()}catch{}try{_ablyChannel.unsubscribe()}catch{}_ablyChannel=null}if(_ablyControl){try{_ablyControl.unsubscribe()}catch{}_ablyControl=null}if(_ablyClient){try{_ablyClient.close()}catch{}_ablyClient=null}_ablyConnected=!1}function _onAblyLogEntry(e){!liveSession||!e?.id||e.session_id!==liveSession.id||(e.tag==="flag"&&typeof _tfReactionFlash=="function"&&_tfReactionFlash(e.flag_kind),new Set(liveLogEntries.map(n=>n.id)).has(e.id))||(e.id>_liveLogMaxId&&(_liveLogMaxId=e.id),e._remote=!0,liveLogEntries=[e,...liveLogEntries],_updateWallAnchor([e]),renderLiveLog())}function _onAblyPresenceChange(){_ablyChannel&&_ablyChannel.presence.get().then(e=>{_presCount=e.length,_presUpdateLiveTab(_presCount),_updateLiveViewersBar(e)}).catch(()=>{})}function _updateLiveViewersBar(e){const t=document.getElementById("live-viewers-bar");if(!t)return;
1if(!_isGm()){t.classList.add("hidden");return}let n;if(Array.isArray(e)?n=e.filter(s=>s.data?.userId!==currentUser?.id).map(s=>({display_name:s.data?.name||"?",is_observer:s.data?.role==="observer"})):e?.live_users?n=e.live_users:n=[],!n.length){t.classList.add("hidden");return}t.classList.remove("hidden");const a=n.filter(s=>s.is_observer).length;t.innerHTML=n.map(s=>{const i=s.display_name||"?",o=i.trim().charAt(0).toUpperCase()||"?",r=["live-viewer-chip","is-online"];s.is_recording&&r.push("is-recording"),s.is_observer&&r.push("is-observer");const c=s.is_recording?" (recording)":s.is_observer?" (Observer)":"";return`<span class="${r.join(" ")}" title="${escHtml(i+c)}">${escHtml(o)}</span>`}).join("")+(a?`<span class="live-viewer-observers">${a} observing</span>`:"")}let _liveViewersPollTimer=null,_liveHadRecording=!1,_liveViewersFirstPoll=!0;async function _pollLiveViewers(){if(currentCampaign)try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/presence`);_updateLiveViewersBar(e);const t=(e.live_users||[]).some(n=>n.is_recording);t&&!_liveHadRecording&&!_isGm()&&!_liveViewersFirstPoll&&showToast("\u{1F534} Recording has started"),_liveHadRecording=t,_liveViewersFirstPoll=!1}catch{}}function _startLiveLogPoll(){if(_stopLiveLogPoll(),!liveSession)return;_liveLogMaxId=liveLogEntries.length?Math.max(...liveLogEntries.map(t=>t.id)):0,_liveLogPollTimer=setInterval(_pollLiveLog,_ablyConnected?LIVE_LOG_POLL_MS_ABLY:LIVE_LOG_POLL_MS)}function _stopLiveLogPoll(){_liveLogPollTimer&&(clearInterval(_liveLogPollTimer),_liveLogPollTimer=null)}function _startTranscriptPoll(){_stopTranscriptPoll(),liveSession&&(_liveTranscriptPollTimer=setInterval(_pollTranscript,LIVE_TRANSCRIPT_POLL_MS))}function _stopTranscriptPoll(){_liveTranscriptPollTimer&&(clearInterval(_liveTranscriptPollTimer),_liveTranscriptPollTimer=null)}async function _pollTranscript(){if(!(!liveSession||!currentCampaign||liveRecording||typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive))try{const t=(await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}`))?.raw_transcript||"";if(!t||t===_liveTranscriptPollLastText)return;_liveTranscriptPollLastText=t,_applyPolledTranscript(t)}catch{}}const _SPEAKER_PALETTE=["var(--accent-teal)","var(--color-purple)","var(--accent-green)","var(--accent-blue)","var(--accent-amber)","var(--color-ember)"];function _speakerColor(e){let t=0;for(let n=0;n<e.length;n++)t=t*31+e.charCodeAt(n)>>>0;return _SPEAKER_PALETTE[t%_SPEAKER_PALETTE.length]}function _speakerAvatar(e){const t=(e||"").trim();if(/^gm$/i.test(t)){const a=typeof currentCampaign<"u"?currentCampaign:null,s=a&&a.gm_brand,i=a&&(a.gm_credit_name||"").trim()||s&&(s.gm_name||"").trim()||"",o=i?"GM "+i:"GM",r=s&&s.logo_url||a&&(a.gm_brand_logo_url||"").trim()||a&&(a.guild_icon_url||"").trim()||"",c=s&&s.name?s.name:o;return`${r?`<span class="tspk-thumb tspk-thumb--brand" title="${escHtml(c)}"><img src="${escHtml(r)}" alt="" /></span>`:""}<span class="tspk-pill tspk-pill--gm" title="${escHtml(o)}">${escHtml(o)}</span>`}let n=(allEntities||[]).find(a=>a.type==="pc"&&a.name===t);if(!n&&t.length>=6){const a=(allEntities||[]).filter(s=>s.type==="pc"&&s.name&&s.name.startsWith(t));a.length===1&&(n=a[0])}if(n){const a=(n.name||"").trim().split(/\s+/)[0]||n.name,s=typeof _entityImageUrl=="function"?_entityImageUrl(n):n.image_url,i=s?`<span class="tspk-thumb"><img src="${escHtml(thumbUrl(s))}" alt="" /></span>`:"";return`${i}<span class="${i?"tspk-pill tspk-pill--with-thumb":"tspk-pill"}" style="--spk-color:${_speakerColor(n.name)}" title="${escHtml(n.name)}">${escHtml(a)}</span>`}return`<span class="tspk-pill tspk-pill--neutral" title="${escHtml(t)}">${escHtml(t||"?")}</span>`}function _buildTranscriptHTML(e){const t=e.split(`
2`);let n="";for(let a=0;a<t.length;a++){const i=t[a].trim();if(!i)continue;const o=i.match(/^([^:]{1,30}):\s+(.+)$/);if(o){const r=o[1].trim(),c=/^gm$/i.test(r)?" transcript-speaker--gm":"";n+=`<div class="transcript-turn"><button class="transcript-speaker transcript-speaker--col transcript-speaker--clickable transcript-speaker--relabel${c}
2" data-spk-label="${escHtml(r)}" data-line-idx="${a}" title="${escHtml(r)}: click to reassign this line">${_speakerAvatar(r)}</button><span class="transcript-text">${escHtml(o[2])}</span></div>`}else n+=`<div class="transcript-divider">${escHtml(i)}</div>`}return n||'<span class="transcript-empty">Listening\u2026</span>'}function _buildPriorTranscriptHTML(e){const t=(e||"").split(`
3`);let n="";for(const a of t){const s=a.trim();if(!s)continue;const i=s.match(/^([^:]{1,30}):\s+(.+)$/);if(i){const o=i[1].trim(),r=/^gm$/i.test(o)?" transcript-speaker--gm":"";n+=`<div class="transcript-turn transcript-turn--prior"><span class="transcript-speaker transcript-speaker--col${r}" title="${escHtml(o)}">${_speakerAvatar(o)}</span><span class="transcript-text">${escHtml(i[2])}</span></div>`}else n+=`<div class="transcript-divider">${escHtml(s)}</div>`}return n}let _priorTxHtmlCache={src:null,html:""};function _applyPolledTranscript(e){const t=document.getElementById("live-transcript-box"),n=document.getElementById("live-transcript-wordcount"),a=document.getElementById("live-transcript-panel");if(!t)return;const s=t.scrollHeight-t.scrollTop-t.clientHeight<32;if(t.innerHTML=_buildTranscriptHTML(e),a&&a.classList.remove("hidden"),n){const i=e.trim().split(/\s+/).filter(Boolean).length;n.textContent=`${i} word${i!==1?"s":""}`}s&&(t.scrollTop=t.scrollHeight),liveTranscriptAccum=e;try{_voicesAbsorbPolled(e)}catch{}}async function _pollLiveLog(){if(!(!liveSession||!currentCampaign||_liveLogTyping))try{const e=_liveLogMaxId>0,t=`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries`+(_liveLogMaxId>0?`?since_id=${_liveLogMaxId}`:""),n=await api("GET",t);if(!n.length)return;const a=Math.max(...n.map(o=>o.id));a>_liveLogMaxId&&(_liveLogMaxId=a);const s=new Set(liveLogEntries.map(o=>o.id)),i=n.filter(o=>!s.has(o.id));if(!i.length)return;i.forEach(o=>{o._remote=!0}),e&&typeof _tfReactionFlash=="function"&&i.forEach(o=>{o.tag==="flag"&&_tfReactionFlash(o.flag_kind)}),liveLogEntries=[...i,...liveLogEntries],_updateWallAnchor(i),renderLiveLog()}catch{}}const PRESENCE_HB_MS=2e4,PRESENCE_MAIN_HB_MS=3e4,PRESENCE_POLL_MS=15e3,PRESENCE_SEL_MS=2e4;let _presHbTimer=null,_presPollTimer=null,_presSelTimer=null,_presInLive=!1,_presCount=0,_presRecordingNow=!1,_presCurrentTab="sessions",_presMainHbTimer=null,_presRecordingUsers=new Set;const PRESENCE_IDLE_MS=5*6e4;let _presLastActivity=Date.now(),_presActivityBound=!1;function _presMarkActivity(){_presLastActivity=Date.now()}function _presMainActive(){return(typeof document>"u"||document.visibilityState==="visible")&&Date.now()-_presLastActivity<PRESENCE_IDLE_MS}function _presBindActivity(){_presActivityBound||typeof window>"u"||(_presActivityBound=!0,["mousemove","mousedown","keydown","scroll","touchstart","click"].forEach(e=>window.addEventListener(e,_presMarkActivity,{passive:!0})),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&(_presMarkActivity(),_presMainHbTimer&&_presSendMainHb().catch(()=>{}))}))}async function _presSendHb(){if(currentCampaign)try{await api("PUT",`/api/campaigns/${currentCampaign.id}/presence`,{tab:_presCurrentTab,display_name:currentUser?.display_name||"",is_recording:typeof liveRecording<"u"?!!liveRecording:!1})}catch{}}async function _presSendMainHb(){if(_presMainActive())return _presSendHb()}async function _presSendLeave(){if(currentCampaign)try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/presence`)}catch{}}function _updateCampPresencePill(e){const t=document.getElementById("camp-presence-pill");t&&(e>0?(t.textContent=e===1?"1 online":`${e} online`,t.classList.remove("hidden")):t.classList.add("hidden"))}async function _presPollCurrent(){if(currentCampaign)try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/presence`);_presCount=e.count||0,_presRecordingNow=!!e.recording,_presUpdateLiveTab(_presCount),_updateCampPresencePill(_presCount)}catch{}}async function _presPollSelector(){try{const e=await api("GET","/api/presence/summary");_presUpdateSelectorCards(e)}catch{}}function _presSetTab(e){_presCurrentTab=e||"sessions",_presMarkActivity(),_presSendMain
3Hb().catch(()=>{})}function _presStartMainHb(){_presMainHbTimer||(_presBindActivity(),_presMarkActivity(),_presCurrentTab=_lastActiveTab||"sessions",_presSendMainHb().catch(()=>{}),_presMainHbTimer=setInterval(()=>_presSendMainHb().catch(()=>{}),PRESENCE_MAIN_HB_MS))}function _presStopMainHb(){_presMainHbTimer&&(clearInterval(_presMainHbTimer),_presMainHbTimer=null)}function _presEnterLive(){_presInLive||(_presInLive=!0,_presStopPoll(),_presCurrentTab="live",_presSendHb().catch(()=>{}),_presHbTimer&&clearInterval(_presHbTimer),_presHbTimer=setInterval(()=>_presSendHb().catch(()=>{}),PRESENCE_HB_MS),_liveHadRecording=!1,_liveViewersFirstPoll=!0,_pollLiveViewers().catch(()=>{}),_liveViewersPollTimer&&clearInterval(_liveViewersPollTimer),_liveViewersPollTimer=setInterval(()=>_pollLiveViewers().catch(()=>{}),PRESENCE_HB_MS))}function _presExitLive(){_presInLive&&(_presInLive=!1,_presHbTimer&&(clearInterval(_presHbTimer),_presHbTimer=null),_liveViewersPollTimer&&(clearInterval(_liveViewersPollTimer),_liveViewersPollTimer=null),_presCount=0,_presRecordingNow=!1,_presUpdateLiveTab(0),_updateCampPresencePill(0),_presSendLeave().catch(()=>{}))}function _presStartPoll(){_presStopPoll(),_presPollTimer=setInterval(()=>_presPollCurrent().catch(()=>{}),PRESENCE_POLL_MS);const e=_presPollTimer;setTimeout(()=>{_presPollTimer===e&&_presPollCurrent().catch(()=>{})},_TF_POLL_FIRST_TICK_MS)}function _presStopPoll(){_presPollTimer&&(clearInterval(_presPollTimer),_presPollTimer=null)}function _presStartSelPoll(){_presStopSelPoll(),_presSelTimer=setInterval(()=>_presPollSelector().catch(()=>{}),PRESENCE_SEL_MS);const e=_presSelTimer;setTimeout(()=>{_presSelTimer===e&&_presPollSelector().catch(()=>{})},_TF_POLL_FIRST_TICK_MS)}function _presStopSelPoll(){_presSelTimer&&(clearInterval(_presSelTimer),_presSelTimer=null)}function _presStopAll(){_presExitLive(),_presStopPoll(),_presStopSelPoll(),_stopWatcherPoll(),_stopFeedPoll()}function _presUpdateLiveTab(e){const t=document.getElementById("live-nav-btn"),n=document.getElementById("live-presence-badge");!t||!n||(_presRecordingNow?(t.classList.add("live-active","live-recording"),n.textContent="Recording",n.classList.remove("hidden")):e>0?(t.classList.remove("live-recording"),t.classList.add("live-active"),n.textContent=e===1?"Session Active":`${e} in Live`,n.classList.remove("hidden")):(t.classList.remove("live-active","live-recording"),n.classList.add("hidden")))}const _PRES_TAB_LABELS={sessions:"Chronicle",compendium:"Codex",party:"Party",quests:"Quests",threads:"GM Prep",map:"Atlas",roster:"Roster",feed:"Feed",live:"Live",oracle:"Oracle"};function _fmtRecElapsed(e){if(!e)return"";const t=Math.max(0,Math.floor((Date.now()-new Date(e).getTime())/6e4));return t<60?`${t}m`:`${Math.floor(t/60)}h ${t%60}m`}function _presUpdateSelectorCards(e){{const n=new Set,a=[];Object.entries(e).forEach(([o,r])=>{(r.recording_count||0)>0&&n.add(Number(o)),r.campaign_name&&a.push({id:Number(o),name:r.campaign_name,cover_image_url:r.cover_image_url||null,__recordingSynthetic:!0})});const s=a.length!==_csAdminRecordingExtra.length||a.some((o,r)=>o.id!==(_csAdminRecordingExtra[r]&&_csAdminRecordingExtra[r].id));_csAdminRecordingExtra=a,(n.size!==_csRecordingIds.size||[...n].some(o=>!_csRecordingIds.has(o))||s)&&!document.querySelector("#campaign-list-screen .cc-new-card--editing")&&(_csRecordingIds=n,Array.isArray(_csAllCampaigns)&&_csAllCampaigns.length&&!document.getElementById("campaign-selector")?.classList.contains("hidden")&&_renderCampaignCards(_csAllCampaigns))}const t=!!currentUser?.is_admin;document.querySelectorAll(".cc-live").forEach(n=>{n.classList.add("hidden"),n.classList.remove("pres-badge--browsing","camp-badge--recording")}),Object.entries(e).forEach(([n,a])=>{const s=document.getElementById(`camp-live-${n}`);if(!s)return;const{count:i,live_count:o,recording_count:r=0,tabs:c}=a,l=s.querySelector(".cc-live-label");if(t){if(!i&&r===0)return;if(r>0){s.classList.add("camp-badge--recording");const d=r>1?`${r} Recording`:"Recording",u=_fmtRecElapsed(a.recording_started_at);l&&(l.textContent=u?`${d} \xB7 ${u}`:d)}else{let d;c?d=Object.entries(c).sort(([,p],[,m])=>m-p).map(([p,m])=>`${m} ${_PRES_TAB_LABELS[p]||p}`).join(" \xB7 "):o>0?d=i===o?`${i} in Live`:`${i} active \xB7 ${o} in Live`:d=`${i} active`,l&&(l.textContent=d),o||s.classList.add("pres-badge--browsing")}s.classList.remove("hidden")}else if(r>0){s.classList.add("camp-badge--recording");const d=_fmtRecElapsed(a.recording_started_at);l&&(l.textContent=d?`Recording \xB7 ${d}`:"Recording"),s.classList.remove("hidden")}else o>0&&(l&&(l.textContent=o>1?`${o} in Live`:"Live"),s.classList.remove("hidden"))})}document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&_presInLive&&_presSendHb().catch(()=>{})});function _tfClearBootRestoring(){document.documentElement.classList.remove("tf-restoring","tf-restore-cs","tf-cs-cards","tf-restore-app")}function showScreen(e){_tfClearBootRestoring(),["auth-screen","campaign-selector","main-app","pricing-screen","topup-screen"].forEach(n=>{const a=document.getElementById(n);a&&a.classList.toggle("hidden",n!==e)}),document.getElementById("chat-widget")?.classList.toggle("hidden",e!=="main-app"),document.getElementById("app-header")?.classList.add("hidden");const t=document.getElementById("admin-screen");t&&(t.style.display="none"),typeof _tfAlignLockup=="function"&&(_tfAlignRetries=0,requestAnimationFrame(_tfAlignLockup))}function showAuthView(e){["login","register","verify-pending","forgot","reset"].forEach(t=>{document.getElementById(`auth-view-${t}`)?.classList.toggle("hidden",t!==e)}),e==="register"&&_maybePopulateReferralOnRegister()}async function _maybePopulateReferralOnRegister(){const e=document.getElementById("auth-reg-referral-banner"),t=document.getElementById("auth-reg-referral-headline"),n=document.getElementById("reg-referral-code"),a=document.getElementById("reg-referral-details");if(!e||!t)return;let s="",i="";try{s=(localStorage.getItem("tf_referral_code")||"").toUpperCase()}catch{}try{i=localStorage.getItem("tf_referral_source")||""}catch{}if(!s){const r=(document.cookie||"").match(/(?:^|; )tf_ref=([^;
3]+)/);r&&(s=decodeURIComponent(r[1]).toUpperCase(),i="ref_cookie")}if(!s||!/^[A-Z0-9]{6,16}$/.test(s)){e.classList.add("hidden");return}n&&(n.value=s),a&&(a.open=!1);let o=null;try{const r=await api("GET",`/api/referral/info?ref=${encodeURIComponent(s)}`);o=r&&r.referrer_first_name||null}catch{o=null}t.textContent=o?`${o} shared Threadfall with you.`:"A friend shared Threadfall with you.",e.classList.remove("hidden")}async function initAuth(){const e=new URLSearchParams((location.hash||"").replace(/^#/,""));if(e.get("tf_login")){if(setStoredToken(e.get("tf_login")),e.get("tf_new")==="1")try{localStorage.setItem("tf_ask_source","1")}catch{}history.replaceState({},"","/");try{const m=localStorage.getItem("tf_vid");m&&api("POST","/api/account/landing-visitor",{visitor_id:m}).catch(()=>{})}catch{}}const t=new URLSearchParams(location.search),n=t.get("oauth_error"),a=t.get("oauth_link");(n||a)&&(history.replaceState({},"","/"),window._tfAuthNotice=a==="password"?"An account with that email already exists. Sign in with your password, then link Discord from your Profile so Discord sign-in works next time.":n==="unverified"?"Your Discord email is not verified. Verify it on Discord, or create an account with your email below.":"Discord sign-in did not complete. Try again, or use your email below.");const s=t.get("verify"),i=t.get("reset"),o=(t.get("qc")||"").trim();o&&/^[a-z0-9]{6,16}$/i.test(o)&&(window._tfQcLanding=o.toLowerCase());const r=(t.get("i")||"").trim();r&&_TF_INVITE_CODE_RE.test(r)&&(window._tfDoorCode=r);const c=(location.pathname||"").match(/^\/join\/([A-Za-z0-9_-]{4,64})\/?$/);c&&(window._tfDoorCode=c[1]),!window._tfDoorCode&&((location.pathname||"").startsWith("/join/")||r)&&(window._tfDeadDoor=!0);const l=(t.get("ref")||"").trim();if(l&&/^[A-Z0-9]{6,16}$/i.test(l)){const m=l.toUpperCase();try{localStorage.setItem("tf_referral_code",m)}catch{}try{localStorage.setItem("tf_referral_source","ref_param")}catch{}try{document.cookie=`tf_ref=${encodeURIComponent(m)}; path=/; max-age=${60*60*24*30}; SameSite=Lax`}catch{}}const d=(t.get("gift")||"").trim();if(d&&/^[A-Z0-9]{6,16}$/i.test(d))try{localStorage.setItem("tf_gift_code",d.toUpperCase())}catch{}if(location.pathname==="/import/roll20"&&t.get("n")&&!localStorage.getItem("tf_jwt")){const m=t.get("n"),f=t.get("cid")||"";try{sessionStorage.setItem("tf_import_landing",JSON.stringify({n:m,cid:f,src:t.get("src")||"",t:Date.now()}))}catch{}if(window.opener){try{window.opener.postMessage({type:"tf-import-hold",nonce:m},TF_ROLL20_ORIGIN)}catch{}try{window.opener.postMessage({type:"tf-import-hold",nonce:m},TF_DDB_ORIGIN)}catch{}}window._tfAuthNotice="Sign in and the import will continue by itself.",showAuthView("login"),showScreen("auth-screen"),document.getElementById("auth-email")?.focus();return}if(s){history.replaceState({},"","/"),showAuthView("login"),showScreen("auth-screen");const m=document.querySelector("#auth-view-login"),f=document.createElement("p");f.id="verify-loading-msg",f.style.cssText="text-align:center;color:var(--text-2);margin:1rem 0;",f.textContent="Verifying your email\u2026",m&&m.prepend(f);let g=!1;try{const h=await fetch(`/api/auth/verify-email?token=${encodeURIComponent(s)}`);if(h.ok){const v=await h.json().catch(()=>({}));v.token&&(setStoredToken(v.token),g=!0)}}catch{}if(document.getElementById("verify-loading-msg")?.remove(),!g){showToast("Verification link expired or invalid. Please sign in or request a new link.",7e3),document.getElementById("auth-email")?.focus();return}showToast("Email verified. Welcome to Threadfall.",4e3),_tfaApplyPendingSource()}if(i){history.replaceState({},"","/"),window._resetToken=i,showAuthView("reset"),showScreen("auth-screen"),document.getElementById("reset-password")?.focus();return}const u=t.get("discord_linked"),p=t.get("discord_error");(u||p)&&history.replaceState({},"","/");try{const m=await api("GET","/api/auth/check");currentUser=m,setTimeout(()=>_tfRenderPasswordNudge(m),800),document.getElementById("admin-panel-btn")?.classList.toggle("hidden",!m?.is_admin),updateNavAvatar(),initAdminPulse(),_tfMarkInternalBrowser(),_tfLoadBillingTier(),_tfMaybeRedeemGift(),_tfaMaybeAskSource(),u&&showToast("Discord linked! Your avatar and name have been imported.",6e3),p&&showToast("Discord linking failed, please try again.",5e3);const f=await _handleRoll20ImportLandingFromPath(),g=!f&&await _handleBillingLandingFromPath(),h=!f&&!g&&_handleHashRoute(),v=!f&&!g&&!h&&/^#codex\/\d+\/\d+$/.test(location.hash||"")&&await _codex2HandleHash(),b=!f&&!g&&!h&&!v&&/^#chronicle\/\d+(\/\d+)?$/.test(location.hash||"")&&await _chronicleHandleHash(),y=!f&&!g&&!h&&!v&&!b&&/^#party\/\d+(\/[a-z]+(\/\d+)?)?$/.test(location.hash||"")&&await _partyHandleHash(),_=!f&&!g&&!h&&!v&&!b&&!y&&/^#quests\/\d+(\/\d+)?$/.test(location.hash||"")&&await _questsHandleHash(),w=!f&&!g&&!h&&!v&&!b&&!y&&!_&&/^#atlas\/\d+(\/\d+)?(\/\d+)?$/.test(location.hash||"")&&await _atlasHandleHash(),$=!f&&!g&&!h&&!v&&!b&&!y&&!_&&!w&&await _adminHandleHash();!f&&!g&&!h&&!v&&!b&&!y&&!_&&!w&&!$&&_tfConsumePostLoginPath();const E=!f&&!g&&!h&&!v&&!b&&!y&&!_&&!w&&!$&&await _handleCampaignDeepLink();if(!f&&!g&&!h&&!v&&!b&&!y&&!_&&!w&&!$&&!E){try{history.replaceState({type:"selector"},"","/")}catch{}showScreen("campaign-selector"),await loadCampaignSelector(),_tfWarnDeadDoor()}}catch{if(location.hash==="#pricing"||location.pathname.replace(/\/+$/,"")==="/pricing"){_showPricingScreen();return}try{const f=new URLSearchParams(location.search||""),g=window._TF_MARKETING_PARAM_RE||/^(utm_|rdt_cid$|gclid$|gbraid$|wbraid$|fbclid$|msclkid$|ttclid$|twclid$|li_fat_id$|epik$|igshid$|mc_cid$|mc_eid$)/i,h=[...f.keys()].every(b=>g.test(b));if((location.pathname==="/"||location.pathname==="")&&!location.hash&&h){document.documentElement.classList.remove("tf-app"),document.documentElement.classList.add("tf-mkt"),_tfClearBootRestoring();return}}catch{}_tfStashPostLoginPath();const m=new URLSearchParams(location.search||"").get("go");showAuthView(m==="login"||location.hash==="#login"?"login":"register");try{const f=new URLSearchParams(location.search||"").get("plan");if(f&&/^(hobby|standard|master|pro)$/.test(f)){sessionStorage.setItem("tf_plan_intent",f);const g=f.charAt(0).toUpperCase()+f.slice(1),h=document.querySelector("#auth-view-register .tfa-view-sub");h&&(h.textContent="One quick account, then straight to "+g+" checkout.");const v=document.querySelector("#auth-view-register .tfa-price-note");v&&(v.textContent="Create your account below, then continue to secure Stripe checkout for "+g+".");const b=document.getElementById("auth-register-btn");b&&(b.textContent="Create account & continue \u2192")}else m==="signup"&&sessionStorage.removeItem("tf_plan_intent")}catch{}showScreen("auth-screen"),_pingLandingHit(),_tfArmLandingEvents(),_tfRenderInviteTeaser();try{localStorage.getItem("tf_gift_code")&&showToast("You have a Threadfall gift waiting. Sign in or create a free account and it applies instantly.",9e3)}catch{}document.getElementById(m==="login"||location.hash==="#login"?"auth-email":"reg-email")?.focus()}}function applyRoleUI(){const e=_isGm();document.getElementById("profile-nav-btn").classList.remove("hidden"),document.querySelectorAll(".gm-only").forEach(a=>a.classList.toggle("hidden",!e)),document.body.classList.toggle("tf-readonly-role",_isReadOnlyRole());const t=typeof liveRecording<"u"&&liveRecording||typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive;document.getElementById("rec-ready")?.classList.toggle("hidden",!e||t),typeof liveRecording<"u"&&liveRecording&&document.getElementById("rec-active")?.classList.remove("hidden"),document.getElementById("rec-member-notice")?.classList.toggle("hidden",e),document.getElementById("rec-btn-row")?.classList.toggle("hidden",!e),document.getElementById("rec-discard-btn")?.classList.toggle("hidden",!e),document.getElementById("live-quest-add-btn")?.classList.toggle("hidden",!e),document.getElementById("live-map-manage-btn")?.classList.toggle("hidden",!e),e||document.getElementById("live-tab-speakers-btn")?.classList.add("hidden"),document.getElementById("nav-manage-btn")?.classList.toggle("hidden",!e),_applyThreadsTabVisibility(),_applyMyCharTabVisibility();const n=document.getElementById("new-campaign-btn");n&&n.classList.remove("hidden"),updateViewAsBar(),typeof renderLivePcSidebar=="function"&&livePcSidebarPcs.length>0&&renderLivePcSidebar()}function _isRealAdmin(){return _realIsAdmin!==null?_realIsAdmin:!!currentUser?.is_admin}
3function _applyAdminGatedUI(){document.getElementById("admin-panel-btn")?.classList.toggle("hidden",!currentUser?.is_admin)}function updateViewAsBar(){const e=!!_isRealAdmin(),n=_realRole!==null||_realIsAdmin!==null?"\u{1F441} Viewing as:":"\u{1F6E1} View as:",a=document.getElementById("view-as-bar");if(a&&(a.style.display=e?"flex":"none",e)){const s=!!currentUser?.is_admin,i=!!currentCampaign,o=i?_isGm():_realRole==="gm"||_realRole==="co_gm";document.getElementById("view-as-admin-btn")?.classList.toggle("active",s),document.getElementById("view-as-gm-btn")?.classList.toggle("active",!s&&(i?o:currentRole==="gm")),document.getElementById("view-as-member-btn")?.classList.toggle("active",!s&&currentRole==="member");const r=document.getElementById("view-as-label");r&&(r.textContent=n)}}function _rerenderForViewAs(){_applyAdminGatedUI(),applyRoleUI(),Array.isArray(_csAllCampaigns)&&_csAllCampaigns.length&&_renderCampaignCards(_csAllCampaigns),renderCompendium(),renderQuests();const e=document.getElementById("tab-party2");e&&!e.classList.contains("hidden")&&typeof renderParty2=="function"&&renderParty2({cid:currentCampaign&&currentCampaign.id,fromCache:!0});const t=(location.hash||"").match(/^#codex\/(\d+)\/(\d+)$/);if(typeof renderBehindScreen=="function"){const a=document.getElementById("tab-threads");_isGm()?a&&!a.classList.contains("hidden")&&renderBehindScreen():_btsTeardown()}t&&typeof renderCodexEntry=="function"&&renderCodexEntry(+t[2]),typeof _codex2RenderRail=="function"&&_codex2RenderRail(t?+t[2]:null);const n=(location.hash||"").match(/^#chronicle\/(\d+)\/(\d+)$/);n&&typeof renderChronicleRecap=="function"&&renderChronicleRecap(+n[2],{background:!0})}function activateViewAs(e){if(_isRealAdmin()){if(_realRole===null&&(_realRole=currentRole),_realIsAdmin===null&&(_realIsAdmin=!!currentUser?.is_admin),e==="admin")currentUser&&(currentUser.is_admin=!0),currentRole=_realRole==="member"?"gm":_realRole;else if(e==="gm")currentUser&&(currentUser.is_admin=!1),currentRole="gm";else if(e==="member")currentUser&&(currentUser.is_admin=!1),currentRole="member";else return;_rerenderForViewAs()}}function showToast(e,t=7e3){if(typeof window.tfToast=="function"){const a=String(e||""),s=a.toLowerCase(),i=/\b(error|fail|failed|failure|denied|invalid|unable)\b/.test(s)?"error":/\b(saved|done|success|ok|generated|complete|created|updated)\b/.test(s)?"success":"info";return window.tfToast({type:i,text:a,duration:t})}const n=document.getElementById("toast");n&&(n._msg=e,n.innerHTML=`<span class="toast-text">${escHtml(e)}</span><button class="toast-copy-btn" title="Copy message" aria-label="Copy">\u2398</button>`,n.querySelector(".toast-copy-btn").addEventListener("click",a=>{a.stopPropagation(),navigator.clipboard.writeText(n._msg).catch(()=>{}),a.target.textContent="\u2713",setTimeout(()=>{a.target&&(a.target.textContent="\u2398")},1200)}),n.classList.add("visible"),clearTimeout(n._timer),n._timer=setTimeout(()=>n.classList.remove("visible"),t))}function _initModalA11y(){const e='button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';if(!_initModalA11y._focusHistory){const a=_initModalA11y._focusHistory=[],s=i=>{!i||i===document.body||i===document.documentElement||a[a.length-1]!==i&&(a.push(i),a.length>16&&a.shift())};document.addEventListener("focusin",i=>s(i.target),!0),document.addEventListener("pointerdown",i=>{_initModalA11y._lastInput=i.pointerType,s(i.target&&i.target.closest?i.target.closest(e):null)},!0),document.addEventListener("keydown",()=>{_initModalA11y._lastInput="keyboard",s(document.activeElement)},!0)}_initModalA11y._stack||(_initModalA11y._stack=[]);const t=_initModalA11y._stack;function n(a,s){if(a.id==="confirm-modal"||a.dataset.a11yWired==="1")return;a.dataset.a11yWired="1";let i=null,o=null;function r(){if(!a.querySelector('[role="dialog"]')&&(a.hasAttribute("role")||a.setAttribute("role","dialog"),a.hasAttribute("aria-modal")||a.setAttribute("aria-modal","true"),!a.hasAttribute("aria-labelledby")&&!a.hasAttribute("aria-label"))){const p=a.querySelector(".modal-hdr h2")||a.querySelector('h2, h3, [class$="-title"]');
3p&&(p.id||(p.id="modal-h2-"+Math.random().toString(36).slice(2,9)),a.setAttribute("aria-labelledby",p.id))}const l=document.activeElement,d=!!l&&l!==document.body&&a.contains(l);if(i=null,l&&l!==document.body&&!d)i=l;else{const p=_initModalA11y._focusHistory||[];for(let m=p.length-1;m>=0;m--){const f=p[m];if(f&&f.isConnected&&f!==document.body&&!a.contains(f)){i=f;break}}}const u=d&&_initModalA11y._lastInput==="touch"&&l.matches('textarea, [contenteditable="true"], input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="file"])');(!d||u)&&a.querySelector(e)?.focus(),t.push(a),o=p=>{if(_tfTopModal()===a){if(p.key==="Escape"){if(p.__tfModalEsc)return;p.__tfModalEsc=!0,p.stopPropagation();const m=s&&a.querySelector('[aria-label="Close"]');m?m.click():a.classList.add("hidden")}else if(p.key==="Tab"){const m=[...a.querySelectorAll(e)].filter(h=>h.offsetParent!==null);if(!m.length)return;const f=m[0],g=m[m.length-1];a.contains(document.activeElement)?p.shiftKey&&document.activeElement===f?(p.preventDefault(),g.focus()):!p.shiftKey&&document.activeElement===g&&(p.preventDefault(),f.focus()):(p.preventDefault(),(p.shiftKey?g:f).focus())}}},document.addEventListener("keydown",o)}function c(){o&&document.removeEventListener("keydown",o),o=null;const l=t.lastIndexOf(a);l!==-1&&t.splice(l,1);const d=document.activeElement;!(!!d&&d!==document.body&&!a.contains(d)&&!!d.closest(".modal-overlay:not(.hidden)"))&&i&&typeof i.focus=="function"&&document.body.contains(i)&&i.getClientRects().length&&i.focus(),i=null}a._tfA11yClose=()=>{o&&c()},new MutationObserver(()=>{const l=!a.classList.contains("hidden");l&&!o?r():!l&&o&&c()}).observe(a,{attributes:!0,attributeFilter:["class"]}),a.classList.contains("hidden")||r()}document.querySelectorAll(".modal-overlay").forEach(a=>n(a,!1)),!_initModalA11y._bodyObserver&&document.body&&(_initModalA11y._bodyObserver=new MutationObserver(a=>{a.forEach(s=>{s.addedNodes.forEach(i=>{i.nodeType===1&&i.classList.contains("modal-overlay")&&n(i,!0)}),s.removedNodes.forEach(i=>{i.nodeType===1&&!i.isConnected&&typeof i._tfA11yClose=="function"&&i._tfA11yClose()})})}),_initModalA11y._bodyObserver.observe(document.body,{childList:!0}))}document.addEventListener("DOMContentLoaded",_initModalA11y),document.readyState!=="loading"&&_initModalA11y();function _tfTopModal(){const e=document.getElementById("confirm-modal");if(e&&!e.classList.contains("hidden"))return e;const t=_initModalA11y._stack||[];for(let n=t.length-1;n>=0;n--)if(t[n].isConnected&&!t[n].classList.contains("hidden"))return t[n];return null}function _tfModalEsc(e,t){return!e||e.key!=="Escape"||e.__tfModalEsc||!t||!t.isConnected||t.classList.contains("hidden")||_tfTopModal()!==t?!1:(e.__tfModalEsc=!0,!0)}function _tfCoolPaperizeModal(e){const t=e&&e.querySelector(".modal-box");if(!t)return;const n=document.getElementById("admin-screen"),a=!!n&&getComputedStyle(n).display!=="none",s=document.getElementById("live-session-screen"),i=!!s&&!s.classList.contains("hidden"),o=!a&&!i&&!!document.querySelector(".tab-panel.cool-paper-surface:not(.hidden), #campaign-selector.cool-paper-surface:not(.hidden)");t.classList.toggle("cool-paper-surface",o)}function confirmDialog({title:e="Confirm",message:t="Are you sure?",confirmLabel:n="Confirm",cancelLabel:a="Cancel",danger:s=!0,checkbox:i=null,checkboxes:o=null,textInput:r=null,selectInput:c=null,pillChoice:l=null,fieldOrder:d=null}={}){return new Promise(u=>{const p=document.getElementById("confirm-modal");_tfCoolPaperizeModal(p);const m=document.getElementById("confirm-modal-title"),f=document.getElementById("confirm-modal-message"),g=document.getElementById("confirm-modal-confirm"),h=document.getElementById("confirm-modal-cancel"),v=document.getElementById("confirm-modal-close");m.textContent=e,f.textContent=t,g.textContent=n,h.textContent=a;const b=[],y={};let _=null;const w="display:flex;align-items:center;gap:0.45rem;margin-top:0.85rem;font-size:var(--font-size-sm);color:var(--text-2);cursor:pointer;";if(Array.isArray(o)&&o.length){let x=f;for(const M of o){const D=document.createElement("label");D.style.cssText=w,D.dataset.cdKind="checkboxes";const O=document.createElement("input");O.type="checkbox",O.checked=!!M.defaultChecked,D.appendChild(O),D.appendChild(document.createTextNode(" "+(M.label||""))),x.parentNode.insertBefore(D,x.nextSibling),b.push(D),M.key&&
3(y[M.key]=O),x=D}}else if(i){const x=document.createElement("label");x.style.cssText=w,x.dataset.cdKind="checkboxes",_=document.createElement("input"),_.type="checkbox",_.checked=!!i.defaultChecked,x.appendChild(_),x.appendChild(document.createTextNode(" "+(i.label||""))),f.parentNode.insertBefore(x,f.nextSibling),b.push(x)}let $=null;if(r){const x=document.createElement("label");if(x.style.cssText="display:block;margin-top:0.85rem;font-size:var(--font-size-sm);color:var(--text-2);",r.label){const D=document.createElement("span");D.style.cssText="display:block;margin-bottom:0.35rem;",D.textContent=r.label,x.appendChild(D)}x.dataset.cdKind="text",$=document.createElement("textarea"),$.rows=r.rows||2,r.maxlength&&($.maxLength=r.maxlength),r.placeholder&&($.placeholder=r.placeholder),r.defaultValue&&($.value=r.defaultValue),$.style.cssText="width:100%;box-sizing:border-box;resize:vertical;font:inherit;padding:0.5rem 0.6rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--text-1);",x.appendChild($);const M=b.length?b[b.length-1]:f;M.parentNode.insertBefore(x,M.nextSibling),b.push(x)}let E=null;if(c){const x=document.createElement("label");if(x.style.cssText="display:block;margin-top:0.85rem;font-size:var(--font-size-sm);color:var(--text-2);",c.label){const D=document.createElement("span");D.style.cssText="display:block;margin-bottom:0.35rem;",D.textContent=c.label,x.appendChild(D)}x.dataset.cdKind="select",E=document.createElement("select"),E.style.cssText="width:100%;box-sizing:border-box;font:inherit;padding:0.5rem 0.6rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--text-1);",(c.options||[]).forEach(D=>{const O=document.createElement("option");O.value=D.value,O.textContent=D.label,c.defaultValue!=null&&String(D.value)===String(c.defaultValue)&&(O.selected=!0),E.appendChild(O)}),x.appendChild(E);const M=b.length?b[b.length-1]:f;M.parentNode.insertBefore(x,M.nextSibling),b.push(x)}let k=null,C=null;if(l&&(l.current||Array.isArray(l.options)&&l.options.length)){const x=document.createElement("div");if(x.className="cd-pillchoice",x.dataset.cdKind="pill",l.label){const N=document.createElement("span");N.className="form-section-label",N.textContent=l.label,x.appendChild(N)}l.editable&&(C=document.createElement("input"),C.type="text",C.className="cd-pill-input",C.value=l.current||"",C.setAttribute("aria-label",l.label||"Value"),C.style.cssText="width:100%;box-sizing:border-box;font:inherit;padding:0.5rem 0.6rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--text-1);",x.appendChild(C));const M=document.createElement("div");M.className="cd-pillchoice-row";const D=(l.editable?l.options||[]:[l.current,...l.options||[]]).filter(Boolean);D.forEach((N,G)=>{const F=document.createElement("button");F.type="button",F.className="btn-new btn-xs cd-pill"+(!l.editable&&G===0?" cd-pill--on":""),F.textContent=N,F.addEventListener("click",()=>{C?(C.value=N,C.focus()):(M.querySelectorAll(".cd-pill").forEach(W=>W.classList.remove("cd-pill--on")),F.classList.add("cd-pill--on")),k=N,typeof _tfTitleAltsHintSeen=="function"&&_tfTitleAltsHintSeen()}),M.appendChild(F)}),D.length&&x.appendChild(M);const O=b.length?b[b.length-1]:f;O.parentNode.insertBefore(x,O.nextSibling),b.push(x)}if(Array.isArray(d)&&d.length){let x=f;for(const M of d)for(const D of b.filter(O=>O.dataset&&O.dataset.cdKind===M))x.parentNode.insertBefore(D,x.nextSibling),x=D}g.classList.toggle("btn-danger",!!s),g.classList.toggle("btn-new",!s),g.classList.remove("btn-primary");const S=g.parentElement;S&&(S.style.flexDirection=s?"row":"row-reverse");let q=!1;function I(x){p.classList.add("hidden"),g.removeEventListener("click",T),h.removeEventListener("click",P),v.removeEventListener("click",P),p.removeEventListener("mousedown",B),p.removeEventListener("click",H),document.removeEventListener("keydown",A);const M=!!(_&&_.checked),D={};for(const z in y)D[z]=!!y[z].checked;const O=x&&$?$.value:"",N=x&&E?E.value:"",G=C?C.value.trim():null,F=x?C?G&&G!==(l.current||"").trim()?G:"":k&&l&&k!==l.current?k:"":"";for(const z of b)z.remove();let W;if(Array.isArray(o)&&o.length){const z={};for(const V of o)V.key&&(z[V.key]=!1);W=x?{confirmed:!0,checked:D}:{confirmed:!1,checked:z}}else i?W=x?{confirmed:!0,checked:M}:{confirmed:!1,checked:!1}
3:r||c||l?W={confirmed:!!x}:W=x;r&&W&&typeof W=="object"&&(W.text=O),c&&W&&typeof W=="object"&&(W.select=N),l&&W&&typeof W=="object"&&(W.pill=F),u(W)}function T(){I(!0)}function P(){I(!1)}function B(x){q=x.target===p}function H(x){x.target===p&&q&&I(!1)}function A(x){x.key==="Escape"?I(!1):x.key==="Enter"&&x.target&&x.target.tagName==="TEXTAREA"||(x.key==="Enter"&&C&&x.target===C?(x.preventDefault(),C.blur()):x.key==="Enter"&&(x.preventDefault(),I(!0)))}g.addEventListener("click",T),h.addEventListener("click",P),v.addEventListener("click",P),p.addEventListener("mousedown",B),p.addEventListener("click",H),document.addEventListener("keydown",A),p.classList.remove("hidden"),setTimeout(()=>g.focus(),0)})}function escHtml(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}window.tfToast=function(e){try{const t=document.getElementById("tf-toast-stack");if(!t)return;const{type:n="info",text:a="",action:s=null,duration:i}=e||{},o=i===void 0?4e3:i,r=document.createElement("div");r.className="tf-toast"+(n==="success"?" tf-toast--success":n==="error"?" tf-toast--error":"");const c=n==="success"?"\u2713":n==="error"?"\u26A0":"\u24D8";let l="";s&&s.label&&(l=`<button class="tf-toast-action">${escHtml(s.label)}</button>`),r.innerHTML=`<span class="tf-toast-icon" aria-hidden="true">${c}</span><span class="tf-toast-text">${escHtml(a)}</span>`+l+'<button class="tf-toast-action tf-toast-dismiss" aria-label="Dismiss">\xD7</button>',t.appendChild(r),requestAnimationFrame(()=>r.classList.add("is-in"));const d=()=>{r.classList.remove("is-in"),r.classList.add("is-out"),setTimeout(()=>{try{r.remove()}catch{}},220)},u=r.querySelector(".tf-toast-dismiss");if(u&&u.addEventListener("click",d),s&&s.onClick){const p=r.querySelector(".tf-toast-action:not(.tf-toast-dismiss)");p&&p.addEventListener("click",m=>{try{s.onClick(m)}catch{}d()})}return o>0&&setTimeout(d,o),d}catch(t){console.warn("tfToast failed:",t)}};function _renderSkeletonCards(e,t){return`<div class="tf-card ${t==="sm"||t==="xs"||t==="lg"?`tf-card--${t}`:"tf-card--md"} tf-skel-card" aria-hidden="true"><span class="tf-skeleton" style="height:14px;width:38%;margin-bottom:8px;"></span><span class="tf-skeleton" style="height:10px;width:78%;"></span></div>`.repeat(Math.max(1,Math.min(e,12)))}function _scrollActiveTabIntoView(e="smooth"){const t=document.querySelector("#tab-scroll-area .tab-btn.active");t&&t.scrollIntoView({behavior:e,inline:"nearest",block:"nearest"})}document.addEventListener("focusin",e=>{const t=e.target.closest("#tab-scroll-area .tab-btn");t&&t.scrollIntoView({behavior:"smooth",inline:"nearest",block:"nearest"})}),document.querySelectorAll(".tab-btn, .nav-utility-btn[data-tab]").forEach(e=>{e.addEventListener("click",()=>{if(++_tfNavSeq,e.dataset.tab&&(_lastActiveTab=e.dataset.tab),document.querySelectorAll(".tab-btn, .nav-utility-btn[data-tab]").forEach(n=>{n.classList.remove("active"),n.setAttribute("aria-selected","false")}),document.querySelectorAll(".tab-panel").forEach(n=>n.classList.add("hidden")),e.classList.add("active"),e.setAttribute("aria-selected","true"),_scrollActiveTabIntoView(),e.dataset.tab==="sessions"){renderChronicleTimeline(),_setupCardSyncFromServer(),typeof _presSetTab=="function"&&_presSetTab("sessions"),typeof _tfRouterPush=="function"&&currentCampaign&&_tfRouterPush({type:"campaign",cid:currentCampaign.id,tab:"sessions",sid:null});return}if(e.dataset.tab==="party"){const n=_party2Panel();_party2Show(n,currentCampaign?`#party/${currentCampaign.id}`:"#party"),renderParty2({cid:currentCampaign&&currentCampaign.id,fromCache:!0}),typeof _presSetTab=="function"&&_presSetTab("party"),typeof _tfRouterPush=="function"&&currentCampaign&&_tfRouterPush({type:"campaign",cid:currentCampaign.id,tab:"party",sid:null});return}if(e.dataset.tab==="map"){const n=_atlas2Panel(),a=currentCampaign&&currentCampaign.id;_atlas2Show(n,a?`#atlas/${a}`:"#atlas"),renderAtlas2({cid:a}
3),typeof _presSetTab=="function"&&_presSetTab("map"),typeof _tfRouterPush=="function"&&currentCampaign&&_tfRouterPush({type:"campaign",cid:currentCampaign.id,tab:"map",sid:null});return}const t=document.getElementById(`tab-${e.dataset.tab}`);t&&(t.classList.remove("hidden"),t.classList.remove("tf-entering"),t.offsetWidth,t.classList.add("tf-entering"),e.dataset.tab==="compendium"&&loadEntities(),e.dataset.tab==="manage"&&loadManageTab(),e.dataset.tab==="threads"&&(renderBehindScreen(),loadThreads()),e.dataset.tab==="mychar"&&renderMyCharacterTab(),e.dataset.tab&&_presSetTab(e.dataset.tab),e.dataset.tab&&typeof _tfRouterPush=="function"&&currentCampaign&&_tfRouterPush({type:"campaign",cid:currentCampaign.id,tab:e.dataset.tab,sid:null}))})});async function checkHealth(){const e=document.getElementById("db-status");try{const t=await api("GET","/api/health");e.textContent=`DB: ${t.db}`,e.className=t.db==="connected"?"visible ok":"visible err"}catch{e.textContent="DB: unreachable",e.className="visible err"}setTimeout(()=>e.className=e.className.replace("visible","").trim(),4e3)}let currentCampaign=null,_tfPendingLanding=null,_tfNavSeq=0;function _tfNavStale(e){return e!==_tfNavSeq}let _csBootSeq=0,_tfBootPayload=null;function _campaignStale(e){return!currentCampaign||currentCampaign.id!==e}function _tfStampCampaignHash(e){if(/^\/c\/\d+/.test(location.pathname)&&location.hash!==e)try{history.replaceState(null,"",e)}catch{}}async function bootWithCampaignId(e){const t=Number(e);if(!Number.isInteger(t)||t<=0){console.warn("bootWithCampaignId: ignoring invalid campaign id",e);return}e=t,setTimeout(checkHealth,_TF_POLL_FIRST_TICK_MS);const n=++_csBootSeq;try{_presStopSelPoll();let a,s=null;try{s=await api("GET",`/api/campaigns/${e}/boot`),a=s.campaign}catch{s=null,a=await api("GET",`/api/campaigns/${e}`)}if(n!==_csBootSeq||(currentCampaign=a,typeof _tfRsResetForCampaign=="function"&&_tfRsResetForCampaign(),_tfBootPayload=s?{cid:a.id,data:s}:null,currentRole=currentCampaign.user_role||"member",typeof _applySystemGating=="function"&&_applySystemGating(),_realIsAdmin!==null&&currentUser&&(currentUser.is_admin=_realIsAdmin),_realRole=null,_realIsAdmin=null,await showMain(),n!==_csBootSeq))return;if(_tfMaybeObserverNotice(),applyRoleUI(),_presStartPoll(),typeof _tfRoll20MaybeContinueImport=="function"&&_tfRoll20MaybeContinueImport(),typeof _tfRoll20ConsumedCid<"u"&&_tfRoll20ConsumedCid!==null&&_tfRoll20ConsumedCid!==e&&typeof _tfRoll20Teardown=="function"&&_tfRoll20Teardown("campaign-switch"),typeof _loadDiscordConfig=="function"&&currentRole!=="member"&&_loadDiscordConfig(e).then(()=>{typeof _applyDiscordAnnounceCapability=="function"&&_applyDiscordAnnounceCapability()}).catch(()=>{}),typeof _tfRouterPush=="function"&&currentCampaign){const i=document.querySelector(".tab-btn.active[data-tab], .nav-utility-btn.active[data-tab]");(!i||i.dataset.tab==="sessions")&&_tfRouterPush({type:"campaign",cid:currentCampaign.id,tab:"sessions",sid:null})}}catch{if(n!==_csBootSeq)return;showScreen("campaign-selector"),await loadCampaignSelector(),typeof _tfRouterReplace=="function"&&_tfRouterReplace({type:"selector"})}}async function showMain(){showScreen("main-app"),typeof _applySystemGating=="function"&&_applySystemGating(),typeof _tfApplyDocTitle=="function"&&_tfApplyDocTitle(),updateViewAsBar(),_presStartMainHb(),document.getElementById("campaign-name").textContent=currentCampaign.name;const e=document.getElementById("nav-campaign-name");if(e){const r=e.querySelector(".ncn-text");r&&(r.textContent=currentCampaign.name||""),e.title=currentCampaign.name||""}const t=document.querySelector(".nav-right");if(t){let r=currentCampaign.cover_image_url||null;if(!r){const c=_matchCoverPrefix(currentCampaign.name);c&&(r=_COVER_DEFAULTS[c]||null,!r&&Array.isArray(_csAllCampaigns)&&(r=_csAllCampaigns.find(d=>d.cover_image_url&&_matchCoverPrefix(d.name)===c)?.cover_image_url||null))}r?(t.style.setProperty("--cover-img",`url("${r.replace(/"/g,"%22")}")`),t.classList.add("has-cover")):(t.style.removeProperty("--cover-img"),t.classList.remove("has-cover"))}document.querySelectorAll(".tab-btn").forEach(r=>r.classList.remove("active")),document.querySelectorAll(".tab-panel").forEach(r=>r.classList.add("hidden"));const n=_tfPendingLanding==="codex"?"compendium":_tfPendingLanding||"sessions";document.querySelector(`[data-tab="${n}"]`)?.classList.add("active"),_scrollActiveTabIntoView("instant"),partyTreasury=null,partySpellUsage={},partyBestQuotes={},allEntities=[],_entitiesLoadedCid=null,allSessions=[],allQuests=[],expandedEntityId=null,_codexModeAuto=!0,_codexRecentIds=[],_tfThreadsLoaded=!1,_hooksAllThreads=[],chatLoaded=!1,chatMessages=[],campaignTally=null,liveEntriesCache={},sessionTallyCache={},_fmapAddOpenFid=null,_p2RnData=null,_p2RnParty=null,typeof _fmapPopClose=="function"&&_fmapPopClose(),typeof _fmapLogClose=="function"&&_fmapLogClose(),document.querySelectorAll(".modal-overlay:not(.hidden)").forEach(r=>r.classList.add("hidden")),renderCompendium(),currentCampaign&&(!_tfPendingLanding||_tfPendingLanding==="sessions")&&renderChronicleTimeline(),allFeedItems=[],_feedLatestId=0,_feedLoaded=!1,document.getElementById("feed-list").innerHTML='<p class="feed-empty placeholder placeholder--padded">No events yet. Events appear here as sessions progress.</p>',_startFeedPoll(),_watcherLastSeq=0,_watcherIsLive=!1,_watcherPanelOpen=!1,document.getElementById("live-broadcast-banner")?.classList.add("hidden"),document.getElementById("live-watcher-panel")?.classList.add("hidden"),_startWatcherPoll();const a=currentCampaign.id,s=_tfBootPayload&&_tfBootPayload.cid===a?_tfBootPayload.data:null;
3if(_tfBootPayload=null,s){if(await loadSessions(s.sessions||void 0),_campaignStale(a))return;let r=s.entities&&Array.isArray(s.entities.items)?s.entities.items.slice():null;if(r&&s.entities.next_cursor)try{let c=s.entities.next_cursor;do{const l=await api("GET",`/api/campaigns/${a}/entities?cursor=${c}`);if(_campaignStale(a))return;r=r.concat(l.items||[]),c=l.next_cursor||null}while(c)}catch{}if(_campaignStale(a))return;r&&(allEntities=r,_entitiesLoadedCid=a),partyBestQuotes=s.party_quotes||{},campaignTally=s.campaign_tally??null,_rosterPcs=s.roster||[],partyTreasury=s.party_treasury||{pp:0,gp:0,ep:0,sp:0,cp:0,items:[]},_refreshSpellUsage(),_qcLoadCardedSet(),_tfBootRenderAll();return}const i=Promise.allSettled([_apiAllPages(`/api/campaigns/${a}/entities`),_refreshPartyData(),api("GET",`/api/campaigns/${a}/party-treasury`)]),o=!!(_tfPendingLanding&&_tfPendingLanding!=="sessions");await loadSessions(),!_campaignStale(a)&&(o&&await i.catch(()=>{}),i.then(([r,,c])=>{_campaignStale(a)||(r.status==="fulfilled"&&(allEntities=r.value,_entitiesLoadedCid=a),_qcLoadCardedSet(),partyTreasury=c.status==="fulfilled"?c.value:{pp:0,gp:0,ep:0,sp:0,cp:0,items:[]},_tfBootRenderAll())}).catch(r=>console.warn("showMain deferred-data continuation failed:",r)))}function _tfBootRenderAll(){renderCompendium(),_setupCardRefresh(),allQuests.length>0&&renderQuests(),_tfThreadsLoaded&&_hooksAllThreads.length&&_renderThreadsBody();try{_maybeShowPcClaimPopup()}catch{}}const _PCCLAIM_BACKOFF_AT=3;let _pcClaimSelectedId=null;function _pcClaimKey(){const e=currentUser&&currentUser.id,t=currentCampaign&&currentCampaign.id;return e&&t?`tf_pcclaim_${e}_${t}`:null}function _pcClaimState(){const e=_pcClaimKey();if(!e)return null;try{return JSON.parse(localStorage.getItem(e)||"")||{}}catch{return{}}}function _pcClaimSave(e){const t=_pcClaimKey();if(t)try{localStorage.setItem(t,JSON.stringify(e))}catch{}}function _pcClaimSessionRank(){const e={};return[...allSessions||[]].filter(t=>t&&t.id!=null&&t.date).sort((t,n)=>new Date(t.date)-new Date(n.date)).forEach((t,n)=>{e[t.id]=n+1}),e}function _pcClaimFatePc(e,t){let n=null,a=-1;for(const s of e){const i=s.is_dead?s.death_session_id:s.departed_session_id,o=i!=null&&t[i]?t[i]:0;(n===null||o>a)&&(n=s,a=o)}return n}function _pcClaimDormant(){if(currentCampaign&&currentCampaign.next_session_at&&new Date(currentCampaign.next_session_at).getTime()>Date.now())return!1;const t=(allSessions||[]).filter(a=>a&&a.date);if(!t.length)return!1;const n=Math.max(...t.map(a=>new Date(a.date).getTime()));return Date.now()-n>21*864e5}function _pcClaimCopy(e,t,n){const a=(n||"").trim();return e==="death"?t?{title:"The Story Continues",sub:a?`${a}'s tale has ended, but the table goes on. Who do you pick up next?`:"Your character's tale has ended, but the table goes on. Who do you pick up next?"}:{title:"The Story Continues",sub:a?`${a} has fallen. When you're ready, your GM can help you bring someone new to the table.`:"When you're ready, your GM can help you bring someone new to the table.",talkgm:"Talk to your GM about your next character: they can create or import them here."}:e==="departed"?t?{title:"Until Next Time",sub:a?`${a} has stepped away, but your seat at the table is still yours.`:"Your seat at the table is still yours, ready to pick up someone new?"}:{title:"Until Next Time",sub:a?`${a} has stepped away, but your seat at the table is still yours.`:"Your seat at the table is still yours.",talkgm:"Talk to your GM about your next character: they can create or import them here."}:t?{title:"Find Your Place in the Story",sub:"Pick the character who's yours: your quotes, moments, and sheet will follow."}:{title:"Let's Get You a Character",sub:"No open characters yet: your GM can create or import yours in a minute.",talkgm:"Ask your GM to add or import your character, then come back here to claim."}}function _maybeShowPcClaimPopup(){if(_isReadOnlyRole()||typeof _isGm=="function"&&_isGm()||!currentCampaign||!currentUser)return;const e=_splitPcsByLifecycle(),t=e.active;if(t.some(l=>l.owned_by_me))return;const n=[...e.fallen,...e.departed].filter(l=>l.owned_by_me),a=_pcClaimFatePc(n,_pcClaimSessionRank()),s=a?a.is_dead?"death":"departed":"welcome",i=t.some(l=>l.assignable),o=_pcClaimState();if(o===null)return;const r=Date.now();if(o.holdUntil&&r<o.holdUntil||_pcClaimDormant())return;const c=(o.dismisses||0)>=_PCCLAIM_BACKOFF_AT?7*864e5:864e5;o.lastShown&&r-o.lastShown<c||(o.lastShown=r,_pcClaimSave(o),_pcClaimTrack("shown",s),_openPcClaimModal({variant:s,hasClaimable:i,oldName:a?a.name:null,activePcs:t}))}function _pcClaimCardHtml(e){const t=typeof ENTITY_CFG<"u"&&ENTITY_CFG[e.type]||{color:"var(--accent-blue)"},n=e.portrait_offset_x!=null?e.portrait_offset_x:50,a=e.portrait_offset_y!=null?e.portrait_offset_y:0,s=e.image_crop_url||e.image_url,
3i=s?typeof thumbUrl=="function"?thumbUrl(s):s:null,o=i?`<img src="${escHtml(i)}" alt="" loading="lazy">`:entityGlyphRef(e.type,"pcclaim-card-glyph"),r=!e.assignable,c=r?e.claimed_by_name&&e.claimed_by_name!=="another player"?"Claimed by "+escHtml(String(e.claimed_by_name).trim().split(/\s+/)[0]):"Claimed":"Unclaimed";return`<button class="pcclaim-strip-card tf-card tf-card--sm${r?" is-departed":""}" role="radio" aria-checked="false" ${r?'tabindex="-1" aria-disabled="true"':'tabindex="0"'} data-pcid="${e.id}" data-pcname="${escHtml(e.name||"Unnamed")}" style="--entity-color:${t.color}"><span class="pcclaim-card-portrait" style="--portrait-x:${n}%;--portrait-y:${a}%">${o}</span><span class="pcclaim-card-info"><span class="pcclaim-card-name">${escHtml(e.name||"Unnamed")}</span><span class="pcclaim-card-owner">${c}</span></span></button>`}function _pcClaimSelectCard(e){document.getElementById("pc-claim-strip").querySelectorAll(".pcclaim-strip-card").forEach(a=>{a.classList.remove("is-selected"),a.setAttribute("aria-checked","false")}),e.classList.add("is-selected"),e.setAttribute("aria-checked","true"),_pcClaimSelectedId=parseInt(e.dataset.pcid);const n=document.getElementById("pc-claim-confirm");n&&(n.textContent="Claim "+(e.dataset.pcname||"character"),n.classList.remove("hidden"),n.disabled=!1)}function _pcClaimWireCards(e){const t=()=>[...e.querySelectorAll(".pcclaim-strip-card:not(.is-departed)")];t().forEach(n=>{n.addEventListener("click",()=>_pcClaimSelectCard(n)),n.addEventListener("keydown",a=>{if(a.key==="Enter"||a.key===" ")a.preventDefault(),_pcClaimSelectCard(n);else if(a.key==="ArrowRight"||a.key==="ArrowLeft"){a.preventDefault();const s=t(),i=s.indexOf(n),o=s[i+(a.key==="ArrowRight"?1:-1)];o&&o.focus()}})})}function _openPcClaimModal(e){const t=document.getElementById("pc-claim-modal"),n=document.getElementById("pc-claim-strip"),a=document.getElementById("pc-claim-talkgm"),s=document.getElementById("pc-claim-title"),i=document.getElementById("pc-claim-subhead"),o=document.getElementById("pc-claim-confirm");if(!t||!n||!a)return;const r=_pcClaimCopy(e.variant,e.hasClaimable,e.oldName);s&&(s.textContent=r.title),i&&(i.textContent=r.sub||""),_pcClaimSelectedId=null,o&&(o.classList.add("hidden"),o.disabled=!0,o.textContent="Claim");let c=document.getElementById("pc-claim-reassign");if(e.hasClaimable){a.classList.add("hidden"),n.classList.remove("hidden");const l=[...e.activePcs].sort((d,u)=>!!d.assignable==!!u.assignable?(d.name||"").localeCompare(u.name||""):d.assignable?-1:1);n.innerHTML=l.map(_pcClaimCardHtml).join(""),c||(c=document.createElement("p"),c.id="pc-claim-reassign",c.className="pcclaim-card-reassign",n.parentNode.insertBefore(c,n.nextSibling)),c.textContent="Playing as someone else? Your GM can reassign it.",c.classList.toggle("hidden",!l.some(d=>!d.assignable)),_pcClaimWireCards(n)}else n.classList.add("hidden"),n.innerHTML="",c&&c.classList.add("hidden"),a.classList.remove("hidden"),a.textContent=r.talkgm||"";t.classList.remove("hidden"),setTimeout(()=>{try{(n.querySelector(".pcclaim-strip-card:not(.is-departed)")||document.getElementById("pc-claim-skip")).focus()}catch{}},0)}function _pcClaimTrack(e,t){try{const n=currentCampaign&&currentCampaign.id;if(!n)return;api("POST",`/api/campaigns/${n}/pc-claim-event`,t?{event:e,variant:t}:{event:e}).catch(()=>{})}catch{}}function _dismissPcClaimModal(){const e=document.getElementById("pc-claim-modal");e&&e.classList.add("hidden"),_pcClaimTrack("dismissed");const t=_pcClaimState();t!==null&&(t.dismisses=(t.dismisses||0)+1,_pcClaimSave(t),t.dismisses===_PCCLAIM_BACKOFF_AT&&typeof showToast=="function"&&showToast("No rush, we'll check back next week instead.",4e3))}function _pcClaimNotReady(){const e=document.getElementById("pc-claim-modal");e&&e.classList.add("hidden"),_pcClaimTrack("notready");const t=_pcClaimState();t&&(t.holdUntil=Date.now()+7*864e5,_pcClaimSave(t)),typeof showToast=="function"&&showToast("\u2713 We'll hold off for a week. Find us here when you're ready.",4e3)}async function _claimPcFromPopup(){const e=currentCampaign&&currentCampaign.id,t=_pcClaimSelectedId,n=document.getElementById("pc-claim-confirm");if(!(!e||!t)){n&&(n.disabled=!0);try{await api("POST",`/api/campaigns/${e}/entities/${t}/claim`);const a=_afterPcClaimed(t),s=_pcClaimState();s&&(s.claimed=!0,_pcClaimSave(s));const i=document.getElementById("pc-claim-modal");i&&i.classList.add("hidden"),typeof showToast=="function"&&showToast(`\u2713 You're playing as ${a&&a.name||"your character"}.`,4e3);try{renderCompendium()}catch{}}catch(a){n&&(n.disabled=!1),typeof showToast=="function"&&showToast("Claim failed: "+(a&&a.message||a),4e3)}}}(function(){const t=document.getElementById("pc-claim-skip"),n=document.getElementById("pc-claim-close"),a=document.getElementById("pc-claim-notready"),s=document.getElementById("pc-claim-confirm");t&&t.addEventListener("click",_dismissPcClaimModal),n&&n.addEventListener("click",_dismissPcClaimModal),s&&s.addEventListener("click",_claimPcFromPopup),a&&(a.addEventListener("click",_pcClaimNotReady),a.addEventListener("keydown",i=>{(i.key==="Enter"||i.key===" ")&&(i.preventDefault(),_pcClaimNotReady())}))})(),initAuth().catch(()=>{_tfClearBootRestoring(),showScreen("auth-screen")}
3);const CLASS_ICONS={barbarian:"\u{1FA93}",bard:"\u{1F3B5}",cleric:"\u{1F31F}",druid:"\u{1F343}",fighter:"\u2694\uFE0F",monk:"\u{1F44A}",paladin:"\u{1F6E1}\uFE0F",ranger:"\u{1F3F9}",rogue:"\u{1F5E1}\uFE0F",sorcerer:"\u{1F4AB}",warlock:"\u{1F52E}",wizard:"\u{1F9D9}",artificer:"\u2699\uFE0F","blood hunter":"\u{1FA78}"},_GLYPH_TYPES=new Set(["npc","location","faction","item","spell","deity","pc","companion","lore"]),_TF_DEAD_IMG="data:image/svg+xml;utf8,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%238A8F98" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 16l4.5-4.5a2 2 0 0 1 2.8 0L14 15"/><path d="M14.5 13.5l1.6-1.6a2 2 0 0 1 2.8 0L21 14"/><circle cx="9" cy="9" r="1.2"/></svg>');document.addEventListener("error",e=>{const t=e.target;if(!t||t.tagName!=="IMG"||t.dataset.tfDead==="1")return;const n=t.getAttribute("src")||"";!n||n.startsWith("data:")||(t.dataset.tfDead="1",t.classList.add("tf-img-dead"),t.removeAttribute("srcset"),t.src=_TF_DEAD_IMG,t.getAttribute("title")||(t.title="This image could not be loaded"))},!0);function entityGlyphRef(e,t){const n=_GLYPH_TYPES.has(e)?e:"npc";return`<svg class="${t||""}" viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-${n}"/></svg>`}const ENTITY_CFG={pc:{icon:"\u{1F9D9}",color:"var(--accent-blue)",label:"PC"},companion:{icon:"\u{1F43E}",color:"var(--accent-teal)",label:"Companion"},deity:{icon:"\u{1F31F}",color:"#7E3FA0",label:"Deity"},location:{icon:"\u{1F4CD}",color:"var(--text-3)",label:"Location"},npc:{icon:"\u{1F464}",color:"var(--color-muted-violet)",label:"NPC"},item:{icon:"\u{1F48E}",color:"var(--accent-gold)",label:"Item"},faction:{icon:"\u{1F3F0}",color:"var(--accent-amber)",label:"Faction"},lore:{icon:"\u{1F4DC}",color:"var(--accent-green)",label:"Lore"},spell:{icon:"\u{1FA84}",color:"#2D6DA0",label:"Spell",plural:"Spells"}};let allEntities=[],_entitiesLoadedCid=null,expandedEntityId=null;function _compendiumDoneKey(e){return`compendium_done_${e}`}function markCompendiumDone(e){localStorage.setItem(_compendiumDoneKey(e),"1")}function clearCompendiumDone(e){localStorage.removeItem(_compendiumDoneKey(e))}function isCompendiumDone(e){return localStorage.getItem(_compendiumDoneKey(e))==="1"}function applyCompendiumDoneStates(e){e.querySelectorAll(".entry-compendium-btn").forEach(t=>{isCompendiumDone(t.dataset.entryId)&&(t.classList.add("compendium-btn-done"),t.textContent="\u2705 AI Sync")})}let compendiumTypeFilter="",compendiumQuery="",compendiumSort="alpha",editingEntityId=null,codexMode="archive",_codexModeAuto=!0;const _CODEX_MODES=["archive","live","new","updated","bardic","with-images"],_CODEX_MODE_LABELS={archive:"All Entries",live:"In Play",new:"New",updated:"Updated",bardic:"Bard\u2019s Take","with-images":"Has Art"};let _codexRecentIds=[];const _CODEX_TYPE_PRI={location:0,npc:1,faction:2,pc:3,item:4,spell:5,lore:6,deity:7,companion:8};function _sessionCtx(){let e=null;if(liveSession){const t=allSessions.filter(a=>!a.is_session_zero),n=t.findIndex(a=>a.id===liveSession.id);n>=0&&n+1<t.length&&(e=t[n+1].id)}return{sessionId:liveSession?.id??null,prevSessionId:e,active:!!liveSession,recentlyViewedEntityIds:_codexRecentIds}}function getEntityRelevanceScore(e,t){let n=0;const a=[];t.sessionId&&e.last_seen_session_id===t.sessionId&&(n+=40,a.push("ThisSession")),t.recentlyViewedEntityIds.includes(e.id)&&(n+=25,a.push("Recent"));const s=e.name.toLowerCase();return allQuests.some(o=>o.status==="active"&&((o.title||"").toLowerCase().includes(s)||(o.description||"").toLowerCase().includes(s)))&&(n+=60,a.push("Quest")),!a.includes("ThisSession")&&t.prevSessionId&&e.last_seen_session_id===t.prevSessionId&&(n+=15,a.push("PrevSession")),{score:n,reasons:a}}function _trackEntityView(e){_codexRecentIds=[e,..._codexRecentIds.filter(t=>t!==e)].slice(0,20)}const _REASON_CFG={Quest:{label:"Quest",title:"Linked to an active quest"},ThisSession:{label:"This Session",title:"Referenced in the current session"},Recent:{label:"Recent",title:"Recently viewed in Codex"},PrevSession:{label:"Prev Session",title:"Referenced in the previous session"}};function _reasonBadgesHtml(e){return e.map(t=>{const n=_REASON_CFG[t];return n?`<span class="codex-reason-badge codex-reason-${t.toLowerCase()}" title="${n.title}">${n.label}</span>`:""}).join("")}function thumbUrl(e){if(!e)return null;const t=e.match(/\/upload\/((?:a_hflip\/)?c_crop,[^/]+)\//);return t?e.replace(t[0],`/upload/${t[1]}/w_80,h_80,c_fill,g_north/`):e.replace("/upload/","/upload/w_80,h_80,c_fill,g_north/")}function _tfHflipUrl(e){return!e||e.indexOf("/upload/")===-1||e.indexOf("/a_hflip/")!==-1?e:e.replace("/upload/","/upload/a_hflip/")}function _tfDownloadUrl(e,t){if(!e||e.indexOf("/upload/")===-1||e.indexOf("fl_attachment")!==-1)return e||"";const n=String(t||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60);return e.replace(/\/upload\/(?:f_auto[^/]*\/)?/,`/upload/fl_attachment${n?":"+n:""}/`)}
3function _entityImageUrlRaw(e){if(!e)return null;if(e.image_url)return e.image_url;if(e.dnd_beyond_data)try{return(typeof e.dnd_beyond_data=="string"?JSON.parse(e.dnd_beyond_data):e.dnd_beyond_data)?.avatar_url||null}catch{}return null}function _entityImageUrl(e){return e?e.image_crop_url||_entityImageUrlRaw(e):null}function _entityImageGallery(e){if(!e)return[];const t=Array.isArray(e.image_urls)?e.image_urls.filter(Boolean):[];if(t.length)return t;const n=_entityImageUrlRaw(e);return n?[n]:[]}function _ensureSearchClears(){document.querySelectorAll("input[data-clearable]").forEach(e=>{if(e.parentElement&&e.parentElement.classList.contains("search-clear-host"))return;const t=document.createElement("span");t.className="search-clear-host",e.parentNode.insertBefore(t,e),t.appendChild(e);const n=document.createElement("button");n.type="button",n.className="search-clear-btn",n.textContent="\xD7",n.title="Clear",n.setAttribute("aria-label","Clear search"),n.tabIndex=-1,n.addEventListener("mousedown",s=>s.preventDefault()),n.addEventListener("click",s=>{s.stopPropagation(),s.preventDefault(),e.value&&(e.value="",e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})),e.focus(),a())}),t.appendChild(n);const a=()=>{n.classList.toggle("search-clear-btn--visible",!!e.value)};e.addEventListener("input",a),e.addEventListener("change",a),a()})}document.addEventListener("DOMContentLoaded",_ensureSearchClears);function _spellMetaPills(e){if(e.type!=="spell")return"";let t={};try{t=(typeof e.dnd_beyond_data=="string"?JSON.parse(e.dnd_beyond_data):e.dnd_beyond_data)||{}}catch{}const n={abjuration:"var(--accent-blue)",conjuration:"var(--accent-purple)",divination:"var(--accent-gold)",enchantment:"var(--color-ember)",evocation:"var(--accent-red)",illusion:"var(--accent-teal)",necromancy:"var(--accent-green)",transmutation:"var(--accent-amber)"},a=t.spell_school||"",s=t.spell_level!=null?t.spell_level===0?"Cantrip":`Level ${t.spell_level}`:"",i=n[a]||"var(--color-text-tertiary)";let o="";return s&&(o+=`<span class="spell-pill spell-pill--level">${escHtml(s)}</span>`),a&&(o+=`<span class="spell-pill spell-pill--school" style="--spell-school-color:${i}">${escHtml(a.charAt(0).toUpperCase()+a.slice(1))}</span>`),t.is_concentration&&(o+='<span class="spell-pill spell-pill--conc">Concentration</span>'),t.is_ritual&&(o+='<span class="spell-pill spell-pill--ritual">Ritual</span>'),o?`<div class="spell-pills">${o}</div>`:""}function _itemMetaPills(e){if(e.type!=="item")return"";const t=_itemRarityFor(e);return t?`<div class="item-pills"><span class="item-pill item-pill--${t.slug}">${escHtml(t.label)}</span></div>`:""}function _itemRarityFor(e){if(!e||e.type!=="item")return null;let t={};try{t=(typeof e.dnd_beyond_data=="string"?JSON.parse(e.dnd_beyond_data):e.dnd_beyond_data)||{}}catch{}const n=(t.item_rarity||"").toLowerCase().trim();if(!n||n==="none"||n==="common"||n==="mundane")return null;const a=n.replace(/[_\s]+/g," ").trim();return{slug:a.replace(/\s+/g,"-"),label:a.replace(/\b\w/g,s=>s.toUpperCase())}}const _PLUS_N_RARITY_LABEL={1:"Uncommon",2:"Rare",3:"Very Rare"};function _itemRarityForName(e,t){if(!e)return null;const n=e.toLowerCase();if(t){let o={};try{o=(typeof t.dnd_beyond_data=="string"?JSON.parse(t.dnd_beyond_data):t.dnd_beyond_data)||{}}catch{}const r=o.item_rarities||{},c=Object.keys(r).find(l=>l.toLowerCase()===n);if(c){const l=String(r[c]||"").toLowerCase().trim();if(l&&l!=="none"&&l!=="common"&&l!=="mundane"){const d=l.replace(/[_\s]+/g," ").trim();return{slug:d.replace(/\s+/g,"-"),label:d.replace(/\b\w/g,u=>u.toUpperCase())}}}}const a=(allEntities||[]).find(o=>o.type==="item"&&o.name.toLowerCase()===n),s=a?_itemRarityFor(a):null;if(s)return s;const i=e.match(/\+([123])\b/);if(i){const o=_PLUS_N_RARITY_LABEL[Number(i[1])];if(o)return{slug:o.toLowerCase().replace(/\s+/g,"-"),label:o}}return null}function sortKey(e){return(e||"").replace(/^(the|a|an)\s+/i,"").toLowerCase()}function _splitPcsByLifecycle(e){const t=(e||allEntities||[]).filter(r=>r.type==="pc"),n=(r,c)=>(r.name||"").localeCompare(c.name||""),a=[...t].sort(n),s=t.filter(r=>!r.is_dead&&!r.is_inactive).sort(n),i=t.filter(r=>r.is_dead&&!r.is_inactive).sort(n),o=t.filter(r=>r.is_inactive).sort(n);return{all:a,active:s,fallen:i,departed:o,fallenDeparted:[...i,...o]}}function _activePcs(e){return _splitPcsByLifecycle(e).active}function _hasBardicTake(e){return!!(e&&e.bardic_flavor&&String(e.bardic_flavor).trim())}function _partyFaction(){return(allEntities||[]).find(e=>e&&e.type==="faction"&&Array.isArray(e.aliases)&&e.aliases.some(t=>(t||"").toLowerCase()==="the party"))||null}const _BARDIC_QUILL_IND='<svg class="bardic-quill-ind" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 4s-7 1-12 6c-3 3-4 9-4 9s6-1 9-4c5-5 7-11 7-11z"/><path d="M4 19l5-5"/></svg>';function filteredEntities(){const e=compendiumQuery.toLowerCase(),t=
3allEntities.filter(s=>{if(s.type==="quote"||s.pending_reveal||s.type==="companion"&&s.parent_entity_id&&compendiumTypeFilter!=="companion"&&!e||compendiumTypeFilter&&s.type!==compendiumTypeFilter)return!1;if(!e)return!0;const i=s.name.toLowerCase(),o=(s.description||"").toLowerCase(),r=(Array.isArray(s.aliases)?s.aliases:[]).join(" ").toLowerCase();return i.includes(e)||o.includes(e)||r.includes(e)});if(!e)return t.sort((s,i)=>sortKey(s.name).localeCompare(sortKey(i.name)));const n=(s,i)=>{let o=0,r=0;for(;(r=s.indexOf(i,r))!==-1;)o++,r++;return o},a=s=>{const i=s.name.toLowerCase(),o=(s.description||"").toLowerCase(),r=(Array.isArray(s.aliases)?s.aliases:[]).join(" ").toLowerCase();let c=0;return i.startsWith(e)&&(c+=1e3),c+=n(i,e)*100,c+=n(r,e)*50,c+=n(o,e)*1,c};return t.sort((s,i)=>{const o=a(i)-a(s);return o!==0?o:sortKey(s.name).localeCompare(sortKey(i.name))})}function getSessionNum(e){if(!e||e.is_session_zero)return 0;const t=allSessions.filter(a=>!a.is_session_zero),n=t.findIndex(a=>a.id===e.id);return n===-1?"":t.length-n}let partyTreasury=null;const PARTY_TREASURY_IMAGE_URL="https://res.cloudinary.com/deh3jqq6q/image/upload/v1774576619/threadfall/party-treasury.png";let campaignTally=null;function _initPortraitCrop(){function e(n){const a=n.querySelector("img");if(!a)return;const s=parseInt(n.dataset.uploadPortrait),i=parseFloat(n.dataset.portraitOffset)??50,o=parseFloat(n.dataset.portraitOffsetY)??0,r=!!n.closest(".live-pc-sidebar");_activeCrop&&t(!1),_activeCrop={portrait:n,img:a,entityId:s,allowVertical:r,originalX:i,currentX:i,originalY:o,currentY:o,drag:null},n.classList.add("portrait-cropping");const c=n.querySelector(".portrait-crop-bar");c?.classList.remove("hidden");const l=c?.querySelector(".portrait-crop-bar-hint");l&&(l.textContent=r?"\u2725 drag to reposition":"\u2194 drag to reposition")}function t(n){if(!_activeCrop)return;const{portrait:a,img:s,entityId:i,allowVertical:o,originalX:r,currentX:c,originalY:l,currentY:d}=_activeCrop;if(a.classList.remove("portrait-cropping"),a.querySelector(".portrait-crop-bar")?.classList.add("hidden"),n){const u=Math.round(c),p=Math.round(d);a.dataset.portraitOffset=u,a.dataset.portraitOffsetY=p;const m=o?{portrait_offset_x:u,portrait_offset_y:p}:{portrait_offset_x:u};api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${i}`,m).then(()=>{allEntities=allEntities.map(f=>f.id===i?{...f,portrait_offset_x:u,...o?{portrait_offset_y:p}:{}}:f)}).catch(f=>showToast("Crop save failed: "+f.message))}else s.style.setProperty("--portrait-x",`${r}%`),s.style.setProperty("--portrait-y",`${l}%`),a.dataset.portraitOffset=r,a.dataset.portraitOffsetY=l;_activeCrop=null}document.addEventListener("click",n=>{const a=n.target.closest(".portrait-crop-btn");a&&(n.stopPropagation(),e(a.closest(".party-pc-portrait")))}),document.addEventListener("click",n=>{n.target.closest(".portrait-crop-confirm-btn")&&(n.stopPropagation(),t(!0))}),document.addEventListener("click",n=>{n.target.closest(".portrait-crop-cancel-btn")&&(n.stopPropagation(),t(!1))}),document.addEventListener("click",n=>{_activeCrop&&(_activeCrop.portrait.contains(n.target)||t(!1))}),document.addEventListener("click",n=>{const a=n.target.closest&&n.target.closest(".tf-cite");if(!a)return;n.preventDefault(),n.stopPropagation();const s=a.closest(".session-card[data-id]");if(!s)return;const i=parseInt(s.dataset.id,10),o=typeof allSessions<"u"&&Array.isArray(allSessions)?allSessions.find(d=>d.id===i):null,r=parseInt(a.dataset.citeId||a.textContent.trim(),10),l=(Array.isArray(o?.recap_citations)?o.recap_citations:[]).find(d=>Number(d.n)===r);l&&l.text?typeof window.tfToast=="function"?window.tfToast({type:"info",text:`[${r}] ${l.text}`,duration:8e3}):typeof showToast=="function"&&showToast(`[${r}] ${l.text}`):typeof window.tfToast=="function"&&window.tfToast({type:"info",text:`Citation [${r}] has no source. Regenerate this recap to populate.`,duration:5e3})}),document.addEventListener("pointerdown",n=>{if(_activeCrop&&_activeCrop.portrait.contains(n.target)&&!n.target.closest(".portrait-crop-bar")){n.preventDefault(),_activeCrop.drag={startClientX:n.clientX,startClientY:n.clientY,startX:_activeCrop.currentX,startY:_activeCrop.currentY};try{n.target.setPointerCapture(n.pointerId)}catch{}}}),document.addEventListener("pointermove",n=>{if(!_activeCrop?.drag)return;n.preventDefault();const{drag:a,allowVertical:s,portrait:i,img:o}=_activeCrop,r=(i.offsetWidth||160)*.5,c=(i.offsetHeight||160)*.5,l=n.clientX-a.startClientX,d=Math.max(0,Math.min(100,a.startX+l/r*100));if(_activeCrop.currentX=d,o.style.setProperty("--portrait-x",`${d}%`),s){const u=n.clientY-a.startClientY,p=Math.max(0,Math.min(100,a.startY+u/c*100));_activeCrop.currentY=p,o.style.setProperty("--portrait-y",`${p}%`)}},{passive:!1}),document.addEventListener("pointerup",()=>{_activeCrop&&(_activeCrop.drag=null)}),document.addEventListener("pointercancel",()=>{_activeCrop&&(_activeCrop.drag=null)})}const _DH_HEX=[{key:"agility",lbl:"AGILITY",half:"body",verbs:["Sprint","Dodge","Leap"]},{key:"strength",lbl:"STRENGTH",half:"body",verbs:["Lift","Smash","Grapple"]},{key:"finesse",lbl:"FINESSE",half:"body",verbs:["Control","Hide","Tinker"]},{key:"instinct",lbl:"INSTINCT",half:"mind",verbs:["Perceive","Sense","N
3avigate"]},{key:"presence",lbl:"PRESENCE",half:"mind",verbs:["Charm","Perform","Deceive"]},{key:"knowledge",lbl:"KNOWLEDGE",half:"mind",verbs:["Recall","Analyze","Comprehend"]}];function _dhPartyData(){const e=_activePcs().map(a=>({e:a,s:_dhSheet(a)})).filter(a=>a.s),t={};_DH_HEX.forEach(a=>{t[a.key]=e.map(s=>({name:s.e.name,e:s.e,v:Number((s.s.traits||{})[a.key])})).filter(s=>isFinite(s.v)).sort((s,i)=>i.v-s.v)});const n={};return(typeof _party2Spec=="object"&&_party2Spec&&_party2Spec.active||[]).forEach(a=>{n[a.id]=a.dh_cards||[]}),e.forEach(a=>{a.cards=n[a.e.id]||[]}),{pcs:e,traits:t}}function _dhtDomain(e){let t=null,n=null;return _DH_HEX.forEach(a=>(e[a.key]||[]).forEach(s=>{(t===null||s.v<t)&&(t=s.v),(n===null||s.v>n)&&(n=s.v)})),t===null?{min:-1,max:1}:{min:t-1,max:n+1}}function _dhtPct(e,t){return Math.max(0,Math.min(100,(e-t.min)/Math.max(1,t.max-t.min)*100))}const _DHT_ICON={agility:"dex",strength:"con",finesse:"control",instinct:"perception",presence:"party_face",knowledge:"int"};function _dhtCoin(e,t,n){const a=r=>(r>=0?"+":"")+r,s=e&&e.image_url||"",i=e&&e.name||"",o=s?'<img src="'+escHtml(s)+'" alt="" loading="lazy">':'<span class="dht-coin-i">'+escHtml((i.trim()[0]||"?").toUpperCase())+"</span>";return'<span class="dht-coin dht-coin--'+n+'" title="'+escHtml(i+" "+a(t))+'">'+o+"</span>"}function _dhTraitStrip(e){const t=r=>r==null?"\u2014":(r>=0?"+":"")+r,n=_dhtDomain(e);let a="";for(let r=n.min;r<=n.max;r++)a+='<span class="dht-grid'+(r===0?" dht-grid--zero":"")+'" style="left:'+_dhtPct(r,n).toFixed(2)+'%"></span>';const s=_DH_HEX.map(r=>{const c=e[r.key]||[],l=c[0],d=c[c.length-1],u='<span class="dht-ico"><svg viewBox="0 0 24 24" aria-hidden="true">'+_P2_FACET_ICON[_DHT_ICON[r.key]]+"</svg></span>",p='<span class="dht-verbs">'+r.verbs.map(y=>"<span>"+escHtml(y)+"</span>").join("")+"</span>";if(!l)return'<div class="dht-row dht-row--'+r.half+'">'+p+u+'<span class="dht-lbl">'+r.lbl+'</span><span class="dht-avg">\u2014</span><span class="dht-track">'+a+"</span></div>";const m=Math.round(c.reduce((y,_)=>y+_.v,0)/c.length),f=_dhtPct(d.v,n),g=_dhtPct(l.v,n),h=_dhtPct(m,n),v=l.v===d.v,b=r.lbl+", average "+t(m)+", "+(v?"whole party at "+t(l.v):t(d.v)+" at worst, "+t(l.v)+" at best");return'<div class="dht-row dht-row--'+r.half+' p2-hit" data-facet="dh_'+r.key+'" tabindex="0" role="button" aria-label="'+escHtml(b)+'">'+p+u+'<span class="dht-lbl">'+r.lbl+'</span><span class="dht-avg'+(m<0?" dht-avg--neg":"")+'">'+escHtml(t(m))+'</span><span class="dht-track">'+a+(v?"":'<span class="dht-bar" style="left:'+f.toFixed(2)+"%;width:"+Math.max(.6,g-f).toFixed(2)+'%"></span>')+'<span class="dht-mean" style="left:'+h.toFixed(2)+'%" title="Party average '+escHtml(t(m))+'"></span>'+(v?"":_dhtCoinAt(d,f,"low"))+_dhtCoinAt(l,g,"best")+"</span></div>"}).join("");let i="";for(let r=n.min;r<=n.max;r++)i+='<span class="dht-tick'+(r===0?" dht-tick--zero":"")+'" style="left:'+_dhtPct(r,n).toFixed(2)+'%">'+(r>0?"+":"")+r+"</span>";return'<div class="dht">'+'<div class="dht-row dht-row--head"><span class="dht-verbs"></span><span class="dht-ico"></span><span class="dht-lbl"></span><span class="dht-avg dht-avg-hd"><i class="dht-dia" aria-hidden="true"></i>Avg.</span><span class="dht-track"></span></div>'+s+'<div class="dht-row dht-row--axis"><span class="dht-verbs"></span><span class="dht-ico"></span><span class="dht-lbl"></span><span class="dht-avg"></span><span class="dht-track dht-axis">'+i+"</span></div></div>"}function _dhtCoinAt(e,t,n){return'<span class="dht-coinwrap" style="left:'+t.toFixed(2)+'%">'+_dhtCoin(e.e,e.v,n)+"</span>"}function _dhWho(e){return e.filter(Boolean).slice(0,3).map(t=>({name:t.name,image_url:t.image_url}))}function _dhCoverage(){const e=typeof _party2Spec=="object"&&_party2Spec&&_party2Spec.dh_coverage||null;return e&&Array.isArray(e.facets)?e:null}function _dhFacets(e){const t=_dhCoverage();if(!t)return[];const n={};return(e.pcs||[]).forEach(a=>{n[a.e.id]=a.e}),t.facets.map(a=>({key:a.ui_key,group:a.group,score:a.score,scale:a.scale||7,par:a.par||4,reasoning:a.reasoning,who:_dhWho((a.who||[]).map(s=>n[s]))}))}function _dhStressHtml(){const e=typeof _party2Spec=="object"&&_party2Spec&&_party2Spec.dh_stress||null;if(!e||!e.rows||!e.rows.length)return"";const t=e.rows.map(s=>s.scenes),n=Math.min.apply(null,t),a=Math.max.apply(null,t)>n;return'<div class="dhp-stress"><div class="dh-diff-eyebrow form-section-label">Rest cadence</div><div class="dhp-sgrid"><span class="dhp-sh"></span><span class="dhp-sh">Stress<br>track</span><span class="dhp-sh">Loadout<br>w/ Stress</span><span class="dhp-sh">Scenes until<br>Vulnerable</span>'+e.rows.map(s=>'<span class="dhp-sn" title="'+escHtml(s.name+" \xB7 "+((s.stress_cards||[]).join(", ")||"no Stress-cost cards"))+'">'+escHtml(s.name)+'</span><span class="dhp-sv">'+s.stress_max+'</span><span class="dhp-sv">'+(s.stress_cards||[]).length+'</span><span class="dhp-sv dhp-sv--key'+(a&&s.scenes===n?" dhp-sv--low":"")+'">~'+s.scenes+"</span>").join("")+"</div></div>"}function _dhPartyPanelHtml(){const e=_dhPartyData();if(!e.pcs.length)return`<div class="party-box party-box-empty"><span>Add character stats (\u270F\uFE0F Edit on a character, or paste a Demiplane link) to see the party's shape.</span></div>`;const t=l=>l==null?"\u2014":(l>=0?"+":"")+l,n=_dhFacets(e),a=_DH_HEX.map(l=>{const d=e.traits[l.key]||[],u=d[0],p=d[d.length-1],m=u&&p&&u.v===p.v,f=d.length?Math.round(d.reduce((h,v)=>h+v.v,0)/d.length):null;let g;return u?m?g=l.verbs.join(", ")+". "+(u.v<=0?"The whole party sits at "+t(u.v)+" \u2014 nobody has invested here, so a scene gated on it asks everyone to roll cold.":"The whole party matches at "+t(u.v)+"."):g=l.verbs.join(", ")+". Runs "+t(p.v)+" ("+p.name+") to "+t(u.v)+" ("+u.name+") \u2014 whoever has to carry this cold is only as good as "+p.name+"\u2019s "+t(p.v)+".":g="No modifier recorded for this trait yet.",{key:"dh_"+l.key,group:l.half==="body"?"dhbody":"dhmind",score:f,scoreLabel:f==null?null:t(f),reasoning:g,who:m?[]:_dhWho([u&&u.e,p&&p.e])}});if(typeof _party2Spec=="object"&&_party2Spec){_party2Spec._facetMap=_party2Spec._facetMap||{}
3,n.concat(a).forEach(u=>{_party2Spec._facetMap[u.key]=u}),_party2Spec._dhFacets=n;const l={dh_face:"presence"},d=_party2Spec.facet_reasoning||{};n.forEach(u=>{const p=l[u.key]||u.key.replace(/^dh_/,""),m=d[u.key]||d[p];m&&String(m).trim()&&(u.reasoning=String(m).trim())})}const s=_dhCoverage(),i=s&&s.cards_recorded===!1,o=s&&s.cards_recorded&&s.cards_missing_for||[],r=(s&&s.cards_unrecognised||[]).map(l=>l[0]+" ("+l[1]+")");return'<section class="dhp"><p class="p2-dice-hint">Hover, focus, or tap any facet for the reading behind it.</p>'+(i?'<p class="p2-dice-hint dhp-gap">No domain cards are recorded for this party, so Control and Support are read from the domains each class grants. Add them on a character (Edit, Domain Cards) and those two read from the cards instead.</p>':(o.length?'<p class="p2-dice-hint dhp-gap">No domain cards are recorded for '+escHtml(o.join(", "))+".</p>":"")+(r.length?'<p class="p2-dice-hint dhp-gap">Not counted, because these are not cards we recognise: '+escHtml(r.slice(0,8).join(", "))+".</p>":""))+'<div class="p2-legend"><span class="p2-lg p2-lg-combat">Combat</span><span class="p2-lg p2-lg-noncombat">Non-combat</span><span class="p2-lg p2-lg-par">Par 4 \u2014 covered</span></div><div class="p2-dice-wrap dhp-wrap"><div class="p2-die p2-d8"><div class="p2-die-cap">The D8 \xB7 Coverage</div>'+_p2DiceSvg(n,8,{cx:150,cy:150,maxR:96,lbl:30,w:300,h:300,label:"Party coverage across eight facets"})+'</div><div class="p2-facet-readout" id="p2-facet-readout" role="status" aria-live="polite" data-default="Hover, focus, or tap any facet for the reading behind it."><div class="p2-fro-default"><p class="p2-facet-hint">Hover, focus, or tap any facet for the reading behind it.</p></div></div></div><div class="dhp-hexrow"><div class="p2-die dhp-hex"><div class="p2-die-cap">Traits</div>'+_dhTraitStrip(e.traits)+'<div class="dhp-legend"><span class="dhp-lg dhp-lg-sw dhp-lg-body">Body</span><span class="dhp-lg dhp-lg-sw dhp-lg-mind">Mind</span></div></div></div>'+(_isGm()?'<div class="p2-d6-dcs gm-surface dhp-screen"><div class="gm-surface-head"><span class="gm-surface-ico" aria-hidden="true">'+_CX2_ACT_ICO.screen+'</span><span class="gm-surface-label">Behind the Screen</span><span class="gm-surface-seal">GM&nbsp;only</span></div><div class="dhp-thr-mount"></div>'+_dhStressHtml()+"</div>":"")+"</section>"}let partySpellUsage={},partyBestQuotes={},_activeCrop=null;const _PC_SOURCE_META={roll20:{label:"Roll20",verb:"synced",cls:"p2-src--roll20",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true"><path d="M8 1 14 4.8V11.2L8 15 2 11.2V4.8Z"/><path d="M8 1V6M2 4.8 8 6M14 4.8 8 6M8 6V15M2 11.2 8 6M14 11.2 8 6"/></svg>'},ddb:{label:"D&D Beyond",verb:"synced",cls:"p2-src--ddb",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5c-1.2-.9-2.8-1.3-4.5-1.3v9.6c1.7 0 3.3.4 4.5 1.3M8 3.5c1.2-.9 2.8-1.3 4.5-1.3v9.6c-1.7 0-3.3.4-4.5 1.3M8 3.5v9.6"/></svg>'},foundry:{label:"Foundry",verb:"updated",cls:"p2-src--foundry",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" aria-hidden="true"><path d="M8 1 14 4.8V11.2L8 15 2 11.2V4.8Z"/></svg>'},scan:{label:"Scan",verb:"updated",cls:"p2-src--scan",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" aria-hidden="true"><path d="M4 1.5h6l2 2v11a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-13Z"/><path d="M10 1.5V4h2.5"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h3"/></svg>'},demiplane:{label:"Demiplane",verb:"synced",cls:"p2-src--demiplane",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" aria-hidden="true"><path d="M6.5 9.5 9.5 6.5"/><path d="M7 4.5 8.3 3.2a2.2 2.2 0 0 1 3.1 3.1L9.9 7.6"/><path d="M9 11.5 7.7 12.8a2.2 2.2 0 0 1-3.1-3.1L6.1 8.4"/></svg>'},manual:{label:"Manual",verb:"updated",cls:"p2-src--manual",glyph:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 2.5 13.5 5 5 13.5 2 14l.5-3Z"/><path d="M9.5 4 12 6.5"/></svg>'}};function _pcAgoCompact(e){if(!e)return"";const t=Date.parse(e);if(isNaN(t))return"";const n=Math.floor((Date.now()-t)/6e4);if(n<1)return"just now";if(n<60)return`${n}m ago`;const a=Math.floor(n/60);if(a<24)return`${a}h ago`;const s=Math.floor(a/24);if(s<30)return`${s}d ago`;const i=Math.floor(s/30);return i<12?`${i}mo ago`:`${Math.floor(i/12)}y ago`}function _tfStatSourceForEntity(e){if(!e)return null;var t=e.dnd_beyond_data,n=null;try{t&&typeof t=="object"?n=t:typeof t=="string"&&t.trim()&&(n=JSON.parse(t))}catch{n=null}
3var a=!!(n&&typeof n=="object"&&Object.keys(n).length);if(!a)return null;var s=e.pc_import_source,i;s==="Roll20 import"||n.roll20_id?i="roll20":(s==null||s==="Sheet import")&&(e.last_ddb_import_at||n.character_id)?i="ddb":s==="Foundry VTT import"?i="foundry":s==="PDF import"||s==="Image import"||s==="Scan import"?i="scan":n.demiplane_url?i="demiplane":i="manual";var o=_PC_SOURCE_META[i];return{src:i,label:o?o.label:i}}const _IMPORT_LAST_USED_TILE_ID={roll20:"import-pick-roll20-btn",ddb:"import-pick-ddb-btn",foundry:"import-pick-vtt-label",scan:"import-pick-file-label",demiplane:"import-pick-demiplane-btn"};function _importLastUsedSource(e){var t=null;if(e)t=allEntities.find(function(c){return c.id===e})||null;else{var n=-1/0;if((allEntities||[]).forEach(function(c){if(c.type==="pc"){var l=Date.parse(c.pc_import_at||""),d=Date.parse(c.last_ddb_import_at||""),u=Math.max(isNaN(l)?-1/0:l,isNaN(d)?-1/0:d);u>n&&(n=u,t=c)}}),!t||n===-1/0)return null}if(!t)return null;var a=_tfStatSourceForEntity(t);if(!a)return null;var s=Date.parse(t.pc_import_at||""),i=Date.parse(t.last_ddb_import_at||""),o=Math.max(isNaN(s)?-1/0:s,isNaN(i)?-1/0:i),r=o===-1/0?null:new Date(o).toISOString();return{src:a.src,label:a.label,whenIso:r,name:t.name||""}}function _pcSheetControlHtml(e,t,n){n=n||{};const a=n.dataAttr||"data-p2-import-pc",s=n.clusterClass||"p2-flank-sheet-cluster",i=!!n.canImport,o=!!n.canClaim,r=n.editAttr||"",c=r&&i?`<button class="p2-flank-sheet" type="button" ${r}="${e.id}" title="${t?"Edit this character\u2019s stats by hand":"Enter this character\u2019s stats by hand"}"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 2.5 13.5 5 5 13.5 2 14l.5-3Z"/><path d="M9.5 4 12 6.5"/></svg><span class="p2-flank-sheet-lbl">${t?"Edit Stats":"Enter Stats"}</span></button>`:"",l=y=>c?`<div class="${s}"><div class="p2-flank-sheet-row">${y}${c}</div></div>`:`<div class="${s}">${y}</div>`;if(!i){if(!o)return"";const y=n.claimAttr||"data-p2-claim-pc",_=n.claimClass||"p2-flank-sheet p2-flank-claim",w=n.claimLabel||"Claim this character";return`<div class="${s}"><button class="${_}" type="button" ${y}="${e.id}" title="Claim this character as yours so you can keep its sheet current, your GM can reassign it">${w}</button></div>`}if(!t)return l(`<button class="p2-flank-sheet p2-flank-sheet--cta" type="button" ${a}="${e.id}" title="Import this character's sheet so they count in the party analysis">Import Sheet</button>`);const d=e.stat_source&&_PC_SOURCE_META[e.stat_source],u=d?escHtml(e.stat_source_label||d.label):null,p=d?d.verb:"updated",m=_pcAgoCompact(e.synced_at||e.stat_synced_at),f=d?d.glyph:"",g=d?` ${d.cls}`:"",h=u?`${u} \xB7 ${p}${m?" "+m:""}`:"Update Sheet",b=`${u?`${u} \xB7 ${p}${m?" "+m:""}`:"Update this sheet"}. Stats only: your notes and portrait are kept.`;return l(`<button class="p2-flank-sheet${g}" type="button" ${a}="${e.id}" title="${b}">${f}<span class="p2-flank-sheet-lbl">${h}</span></button>`)}async function _pcSheetControlClick(e){const t=allEntities.find(i=>i.id===e),n=t&&t.name||"this character",a=t&&t.stat_source,s=t&&t.stat_source_url;if(a==="roll20"||a==="ddb"){if(s){try{window.open(s,"_blank","noopener")}catch{}showToast(`Click your ${currentCampaign&&currentCampaign.name||TF_IMPORT_BOOKMARK_NAME} bookmark on that page to update ${n}`,7e3)}else _openImportReview(e);return}if(a==="demiplane"){_openImportReview(e),typeof _importOpenSourcePanel=="function"&&_importOpenSourcePanel("demiplane");return}_openImportReview(e)}function renderPartyCard(e,t){t=t||{};const n=ENTITY_CFG.pc||ENTITY_CFG.location,a=j=>(j>=0?"+":"")+j,s=!!e.is_dead,i=!!e.is_inactive,o=s?" party-pc-portrait--dead":i?" party-pc-portrait--departed":"",r=_isGm(),c=e.portrait_offset_x??50,l=e.portrait_offset_y??0,d=(()=>{try{return JSON.parse(e.dnd_beyond_data||"{}").avatar_url||null}catch{return null}})(),u=e.image_url||d,p=u?`<img src="${escHtml(u)}" alt="${escHtml(e.name)}" style="--portrait-x:${c}%;--portrait-y:${l}%" />`:`<div class="party-pc-portrait-placeholder" style="--entity-color:${n.color}">${entityGlyphRef(e.type,"party-pc-portrait-glyph")}
3</div>`,m=r&&e.image_url?`
4    <button class="portrait-crop-btn" title="Adjust crop">\u2702</button>
5    <div class="portrait-crop-bar hidden">
6      <span class="portrait-crop-bar-hint">\u2194 drag to reposition</span>
7      <button class="portrait-crop-confirm-btn">\u2713</button>
8      <button class="portrait-crop-cancel-btn">\u2715</button>
9    </div>`:"",f=`<div class="party-pc-portrait${o}${r?" party-pc-portrait--editable":""}"
10    ${r?`data-upload-portrait="${e.id}"`:""}
11    data-portrait-offset="${c}"
12    data-portrait-offset-y="${l}">
13    ${p}${m}
14  </div>`,g=allEntities.filter(j=>j.type==="companion"&&j.parent_entity_id===e.id),h=g.length>0?`
15    <div class="pc-companions-section">
16      <div class="pc-companions-hdr">
17        <span class="ddb-row-label">\u{1F43E} Companions</span>
18        ${_isGm()?`<button class="btn-ghost btn-xs add-companion-btn" data-pc-id="${e.id}">+ Add</button>`:""}
19      </div>
20      ${g.length?`<div class="companion-chips">${g.map(j=>`<button class="btn-ghost companion-chip-btn party-pc-name-btn" data-id="${j.id}">
21          ${j.image_url?`<img src="${escHtml(thumbUrl(j.image_url))}" class="companion-chip-img" alt="" />`:entityGlyphRef("companion","companion-chip-glyph")}
22          <span>${escHtml(j.name)}</span>
23        </button>`).join("")}</div>`:'<span class="companion-none">None yet.</span>'}
24    </div>`:"";let v="";if(_isGm())if(i)v=`<button class="btn-ghost btn-xs rejoin-btn" data-id="${e.id}" title="Rejoin the party">\u21A9 Rejoin</button>`;else if(s)v=`<button class="btn-ghost btn-xs toggle-dead-btn" data-id="${e.id}" data-dead="true" title="Revive">\u{1F480} Revive</button>`;else{const j=`<button class="btn-ghost btn-xs toggle-dead-btn" data-id="${e.id}" data-dead="false" title="Mark as fallen">\u{1F480}</button>`,Z=`<button class="btn-ghost btn-xs toggle-inactive-btn" data-id="${e.id}" title="Mark as left party">\u{1F44B}</button>`;v=j+Z}const b=(()=>{if(!e.dnd_beyond_data)return!1;try{return JSON.parse(e.dnd_beyond_data).ai_generated===!0}catch{return!1}})();if(!e.dnd_beyond_data||b){const j="",Z="",pe=!!e.dnd_beyond_url?"":'<span class="form-helper-text">No D&amp;D Beyond link.</span>';return t.forCodex?`<div class="tf-card tf-card--md tf-card--watermarked party-pc-card${s?" is-dead party-pc-card--dead":i?" is-departed party-pc-card--departed":""}">
25      ${f}
26      <div class="party-pc-stats">
27        ${_pcSheetControlHtml(e,!1,{dataAttr:"data-cx2-sheet-pc",clusterClass:"cx2-pc-sheet-cluster",claimAttr:"data-sb-claim",claimClass:"p2-flank-sheet p2-flank-claim",claimLabel:"Claim This Character",canImport:t.canImport,canClaim:t.canClaim})}
28        <div class="party-pc-name-row party-pc-name-row--stacked">
29          <button class="party-pc-name-btn" data-id="${e.id}">${escHtml(e.name)}</button>
30        </div>
31        <div class="party-pc-no-data">${pe||"Character data not yet loaded."}</div>
32        ${h}
33      </div>
34    </div>`:`<div class="tf-card tf-card--md tf-card--watermarked party-pc-card${s?" is-dead party-pc-card--dead":i?" is-departed party-pc-card--departed":""}">
35      ${f}
36      <div class="party-pc-stats">
37        ${v?`<div class="party-pc-actions party-pc-actions--top">${v}${j}${Z}</div>`:""}
38        <div class="party-pc-name-row party-pc-name-row--stacked">
39          <button class="party-pc-name-btn" data-id="${e.id}">${escHtml(e.name)}</button>
40        </div>
41        <div class="party-pc-no-data">${pe||"Character data not yet loaded."}</div>
42        ${h}
43      </div>
44    </div>`}let y;try{y=JSON.parse(e.dnd_beyond_data)}catch{return""}if(y.system==="daggerheart"){const j=!e.is_dead&&!e.is_inactive,Z=y.traits||{},ie=allEntities.filter(ae=>ae.type==="pc"&&ae.id!==e.id&&!ae.is_dead&&!ae.is_inactive).map(ae=>_dhSheet(ae)).filter(Boolean),pe=_DH_TRAITS.map(ae=>{const ee=Number(Z[ae]),ke=isFinite(ee);let Te="ddb-ab-cell";if(j&&ke&&ie.length){const Ne=ie.map(Re=>Number((Re.traits||{})[ae])).filter(isFinite);if(Ne.length){const Re=Math.max(...Ne);ee>=Re&&(Te+=ee>Re?" ddb-ab-best":" ddb-ab-tied-best")}}return`<div class="${Te}">
45        <div class="ddb-ab-label">${_DH_TRAIT_LBL[ae]}</div>
46        <div class="ddb-ab-mod">${ke?a(ee):"\u2014"}</div>
47      </div>`}
47).join(""),re=[y.ancestry,y.community].filter(Boolean).join(" \xB7 "),_e=(y.classes||[]).join(" / ")||[y.class,y.level,y.subclass?`(${y.subclass})`:""].filter(Boolean).join(" ")||"\u2014",fe=[];y.evasion!=null&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">Evasion</span>${escHtml(String(y.evasion))}</span>`),y.armor_score!=null&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">Armor</span>${escHtml(String(y.armor_score))}</span>`),y.thresholds&&y.thresholds.major!=null&&fe.push(`<span class="ddb-strip-item" title="Major/Severe damage thresholds"><span class="ddb-row-label">Thresholds</span>${escHtml(String(y.thresholds.major))}/${escHtml(String(y.thresholds.severe??"\u2014"))}</span>`),y.hp_max!=null&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">HP</span>${escHtml(String(y.hp_max))}</span>`),y.stress_max!=null&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">Stress</span>${escHtml(String(y.stress_max))}</span>`),y.hope_max!=null&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">Hope</span>${escHtml(String(y.hope_max))}</span>`),y.scars&&fe.push(`<span class="ddb-strip-item"><span class="ddb-row-label">Scars</span>${escHtml(String(y.scars))}</span>`);const $e=(y.experiences||[]).map(ae=>{const ee=ae&&typeof ae=="object"?ae.name:String(ae),ke=ae&&typeof ae=="object"&&ae.mod!=null?` +${ae.mod}`:"";return`<span class="dh-exp-pill">${escHtml(ee)}${ke}</span>`}).join(""),Ee=(y.domain_cards||[]).map(ae=>`<span class="dh-card-pill">${escHtml(String(ae))}</span>`).join(""),Ae=(y.weapons||[]).join(", ");return`<div class="tf-card tf-card--md tf-card--watermarked party-pc-card${s?" is-dead party-pc-card--dead":i?" is-departed party-pc-card--departed":""}">
48      ${f}
49      <div class="party-pc-stats">
50        ${v?`<div class="party-pc-actions party-pc-actions--top">${v}</div>`:""}
51        <div class="party-pc-name-row party-pc-name-row--stacked">
52          <button class="party-pc-name-btn" data-id="${e.id}">${escHtml(e.name)}</button>
53        </div>
54        <div class="ddb-class-line">${escHtml(_e)}
54${re?` <span class="ddb-class-subclass">\xB7 ${escHtml(re)}</span>`:""}</div>
55        <div class="ddb-ability-grid dh-trait-grid">${pe}</div>
56        ${fe.length?`<div class="dh-vitals-strip">${fe.join("")}</div>`:""}
57        ${$e?`<div class="dh-pill-row"><span class="ddb-row-label">Experiences</span>${$e}</div>`:""}
58        ${Ee?`<div class="dh-pill-row"><span class="ddb-row-label">Domain Cards</span>${Ee}</div>`:""}
59        ${Ae?`<div class="dh-pill-row"><span class="ddb-row-label">Weapons</span><span class="dh-wpn-line">${escHtml(Ae)}</span></div>`:""}
60        ${y.gold?`<div class="dh-pill-row"><span class="ddb-row-label">Gold</span><span class="dh-wpn-line">${escHtml(String(y.gold))}</span></div>`:""}
61        ${h}
62      </div>
63    </div>`}const w=((y.classes||[]).join(" / ")||"\u2014").replace(/\s*(\([^)]*\))/g,' <span class="ddb-class-subclass">$1</span>'),$=["STR","DEX","CON","INT","WIS","CHA"],E=y.ability_scores||{},k=!e.is_dead&&!e.is_inactive,C=k?allEntities.filter(j=>j.type==="pc"&&j.dnd_beyond_data&&j.id!==e.id&&!j.is_dead&&!j.is_inactive).map(j=>{try{return JSON.parse(j.dnd_beyond_data).ability_scores||{}}catch{return null}}).filter(Boolean):[],S=$.map(j=>{const Z=E[j]||{score:10,mod:0};let ie="ddb-ab-cell";if(k&&C.length>=1){const pe=C.map(Ae=>(Ae[j]||{mod:0}).mod),re=Math.max(...pe),_e=Z.mod,fe=Z.score,$e=Math.max(_e,re),Ee=2*$e+9;_e===$e?ie+=_e>re?" ddb-ab-best":" ddb-ab-tied-best":fe===Ee&&(ie+=" ddb-ab-near-best")}return`<div class="${ie}">
64      <div class="ddb-ab-label">${j}</div>
65      <div class="ddb-ab-mod">${a(Z.mod)}</div>
66      <div class="ddb-ab-score">${Z.score}</div>
67    </div>`}).join(""),q=a(y.initiative??(E.DEX?E.DEX.mod:0)),I=y.advantage_initiative?`${q}<span class="sp-adv">A</span>`:q,T=y.speed?`${y.speed} ft`:"30 ft",P=y.skills||{},B=y.passive_perception,H=y.passive_investigation??10+(P.Investigation??(E.INT?E.INT.mod:0)),A=y.passive_insight??10+(P.Insight??(E.WIS?E.WIS.mod:0)),x=k?allEntities.filter(j=>j.type==="pc"&&j.dnd_beyond_data&&j.id!==e.id&&!j.is_dead&&!j.is_inactive).map(j=>{try{const Z=JSON.parse(j.dnd_beyond_data),ie=Z.skills||{},pe=Z.ability_scores||{};return{perc:Z.passive_perception??10+(ie.Perception??(pe.WIS||{}).mod??0),inv:Z.passive_investigation??10+(ie.Investigation??(pe.INT||{}).mod??0),ins:Z.passive_insight??10+(ie.Insight??(pe.WIS||{}).mod??0)}}catch{return null}}).filter(Boolean):[],M=(j,Z)=>{if(!k||!x.length)return"ddb-ab-cell";const ie=Math.max(...x.map(re=>re[Z])),pe=Math.max(j,ie);return j===pe?"ddb-ab-cell "+(j>ie?"ddb-ab-best":"ddb-ab-tied-best"):j===pe-1?"ddb-ab-cell ddb-ab-near-best":"ddb-ab-cell"},D=k?allEntities.filter(j=>j.type==="pc"&&j.dnd_beyond_data&&!j.is_dead&&!j.is_inactive).map(j=>{try{return{id:j.id,skills:JSON.parse(j.dnd_beyond_data).skills||{}}}catch{return null}}).filter(Boolean):[],O=["Defense","Dueling","Great Weapon Fighting","Protection","Archery","Two-Weapon Fighting","Blind Fighting","Interception","Superior Technique","Thrown Weapon Fighting","Unarmed Fighting","Close Quarters Shooter","Mariner","Tunnel Fighter","Druidic Warrior","Blessed Warrior"],N=["Acrobatics","Animal Handling","Arcana","Athletics","Deception","History","Insight","Intimidation","Investigation","Medicine","Nature","Perception","Performance","Persuasion","Religion","Sleight of Hand","Stealth","Survival"],G={Acrobatics:"Acro.","Animal Handling":"Animal.",Arcana:"Arcana",Athletics:"Athl.",Deception:"Decep.",History:"Hist.",Insight:"Insight",Intimidation:"Intim.",Investigation:"Inves.",Medicine:"Med.",Nature:"Nature",Perception:"Percep.",Performance:"Perf.",Persuasion:"Persua.",Religion:"Relig.","Sleight of Hand":"Sleight.",Stealth:"Stealth",Survival:"Surv."};let F="";if(y.skills){const j=D.filter(re=>re.id!==e.id),Z=re=>(re>=0?"+":"")+re,ie=new Set((y.advantage_skills||[]).map(re=>re.toLowerCase()));F=`<div class="skill-pills-grid">${N.map(re=>{const _e=y.skills[re]??null,fe=ie.has(re.toLowerCase());if(_e===null)return`<div class="skill-pill"><span class="sp-name">${G[re]}</span><span class="sp-val">\u2014</span></div>`;let $e="";if(k&&j.length){const Ae=j.map(ke=>ke.skills[re]??0),ae=Math.max(...Ae),ee=Math.max(_e,ae);_e===ee?$e=_e>ae?" sp-best":" sp-tied":_e===ee-1&&($e=" sp-near")}const Ee=fe?'<span class="sp-adv">A</span>':"";return`<div class="skill-pill${$e}"><span class="sp-name">${G[re]}</span><span class="sp-val">${Z(_e)}${Ee}</span></div>`}).join("")}</div>`}const W=k?allEntities.filter(j=>j.type==="pc"&&j.dnd_beyond_data&&j.id!==e.id&&!j.is_dead&&!j.is_inactive).map(j=>{try{return JSON.parse(j.dnd_beyond_data)}catch{return null}}
67).filter(Boolean):[],z=k?allEntities.filter(j=>j.type==="pc"&&j.dnd_beyond_data&&!j.is_dead&&!j.is_inactive).map(j=>{try{return JSON.parse(j.dnd_beyond_data)}catch{return null}}).filter(Boolean):[],V=(j,Z)=>k&&z.length>=2&&z.every(ie=>(ie[j]||[]).includes(Z)),K=(j,Z)=>k&&W.every(ie=>!(ie[j]||[]).includes(Z)),se=j=>k&&W.every(Z=>(Z.senses||{})[j]===void 0),Q=(j,Z)=>`<span class="${Z?"party-unique-tag":"party-trait-tag"}">${escHtml(j)}</span>`,X=y.senses||{},te={darkvision:"Darkvision","dark vision":"Darkvision",darkvsion:"Darkvision",darkvison:"Darkvision",blindsight:"Blindsight","blind sight":"Blindsight",tremorsense:"Tremorsense","tremor sense":"Tremorsense",truesight:"Truesight","true sight":"Truesight","devil's sight":"Devil's Sight","devils sight":"Devil's Sight","devil-s-sight":"Devil's Sight"},le=(j,Z)=>{let ie=j.trim(),pe=typeof Z=="number"?Z:0;const re=ie.match(/\s+(\d+)\s*(?:feet?|ft\.?)?$/i);if(re&&(pe||(pe=parseInt(re[1])),ie=ie.slice(0,re.index).trim()),!pe&&typeof Z=="string"){const $e=Z.match(/(\d+)/);$e&&(pe=parseInt($e[1]))}const _e=ie.toLowerCase().replace(/[^a-z '\-]/g,"").trim();return{canon:te[_e]||Object.entries(te).find(([$e])=>_e.includes($e))?.[1]||ie.replace(/\b\w/g,$e=>$e.toUpperCase()),feet:pe}},ue=Object.entries(X).map(([j,Z])=>{const{canon:ie,feet:pe}=le(j,Z),re=pe>0?`${ie} ${pe} ft`:ie;return Q(re,se(ie))}).join(""),me=j=>(j||"").replace(/-/g," ").replace(/\b\w/g,Z=>Z.toUpperCase()),de=[...(y.resistances||[]).map(j=>Q(`${me(j)} Res.`,K("resistances",j))),...(y.immunities||[]).map(j=>Q(`${me(j)} Imm.`,K("immunities",j))),...(y.vulnerabilities||[]).map(j=>Q(`${me(j)} Vuln.`,K("vulnerabilities",j)))].join(""),ye=(y.languages||[]).filter(j=>!V("languages",j)).map(j=>Q(j,K("languages",j))).join(""),Le={"Light Armor":"Lt Armor","Medium Armor":"Md Armor","Heavy Armor":"Hvy Armor","Simple Weapons":"Smpl Wpns","Martial Weapons":"Mrt Wpns","Simple Melee Weapons":"Smpl Melee","Simple Ranged Weapons":"Smpl Ranged","Martial Melee Weapons":"Mrt Melee","Martial Ranged Weapons":"Mrt Ranged","Improvised Weapons":"Improv Wpns"},Se={"Thieves' Tools":"Thieves'","Tinker's Tools":"Tinker's","Smith's Tools":"Smith's","Artisan's Tools":"Artisan's","Navigator's Tools":"Nav Tools","Alchemist's Supplies":"Alchemist's","Poisoner's Kit":"Poisoner's","Forgery Kit":"Forgery Kit","Disguise Kit":"Disg Kit","Herbalism Kit":"Herb Kit","Healer's Kit":"Healer's","Gaming Set":"Gaming Set"},he=(j,Z)=>j[Z]||Z.replace(/\bTools?\b/,"Tools").replace(/\bWeapons?\b/,"Wpns").replace(/\bProficiency\b/i,"Prof"),be=y.armor_weapon_profs||[],ne=(...j)=>j.some(Z=>be.includes(Z)),Me=be.filter(j=>!(j==="Simple Weapons"&&ne("Martial Weapons")||j==="Simple Melee Weapons"&&ne("Martial Weapons","Martial Melee Weapons")||j==="Simple Ranged Weapons"&&ne("Martial Weapons","Martial Ranged Weapons")||j==="Light Armor"&&ne("Medium Armor","Heavy Armor")||j==="Medium Armor"&&ne("Heavy Armor"))).filter(j=>!V("armor_weapon_profs",j)).map(j=>Q(he(Le,j),K("armor_weapon_profs",j))).join(""),Ue=(y.tool_profs||[]).filter(j=>!V("tool_profs",j)).map(j=>Q(he(Se,j),K("tool_profs",j))).join(""),Ce=(j,Z,ie=!1)=>Z?`<div class="ddb-row party-tags-row${ie?" party-tags-nowrap":""}"><span class="ddb-row-label">${j}</span><span class="party-tags-wrap">${Z}</span></div>`:"";return`<div class="tf-card tf-card--md tf-card--watermarked party-pc-card${s?" is-dead party-pc-card--dead":i?" is-departed party-pc-card--departed":""}">
68    ${f}
69    <div class="party-pc-stats">
70      <!-- v1121: actions row on top, name+meta block below \u2014 prevents
71           the long name + pronouns + background line from wrapping when
72           there are many GM action buttons crowding the right side. -->
73      ${v?`<div class="party-pc-actions party-pc-actions--top">${v}</div>`:""}
74      ${t.forCodex?_pcSheetControlHtml(e,!0,{dataAttr:"data-cx2-sheet-pc",clusterClass:"cx2-pc-sheet-cluster",claimAttr:"data-sb-claim",claimClass:"p2-flank-sheet p2-flank-claim",claimLabel:"Claim This Character",canImport:t.canImport,canClaim:t.canClaim}):""}
75      <div class="party-pc-name-row party-pc-name-row--stacked">
76        <button class="party-pc-name-btn" data-id="${e.id}">${escHtml(e.name)}</button>
77        ${e.player_name?`<span class="pc-player-name">played by ${escHtml(e.player_name)}</span>`:""}
78        ${e.pronouns?`<span class="pc-pronouns">${escHtml(e.pronouns)}</span>`:""}
79        ${y.species||y.background?`<span class="ddb-background">${escHtml([y.species,y.background].filter(Boolean).join(" "))}</span>`:""}
80      </div>
81      <div class="ddb-class-line">
82        ${w}
83        ${e.party_role?`<span class="pc-role-badge">${escHtml(e.party_role)}</span>`:""}
84        ${e.noncombat_role?`<span class="pc-noncombat-badge">${escHtml(e.noncombat_role)}</span>`:""}
85        ${y.ai_generated?'<span class="pc-ai-badge" title="Stats estimated by AI from codex entry, may not be exact">\u2728 AI est.</span>':""}
86        ${y.import_source==="sheet"?`<span class="pc-ai-badge" title="Stats imported from character sheet${e.pc_import_at?" on "+new Date(e.pc_import_at).toLocaleDateString():""}">\u{1F4C4} Sheet</span>`:""}
87        ${e.last_ddb_import_at?`<span class="pc-ai-badge pc-ai-badge--ddb" title="Synced from D&D Beyond on ${new Date(e.last_ddb_import_at).toLocaleString()}">\u{1F4E5} DDB \xB7 ${new Date(e.last_ddb_import_at).toLocaleDateString()}</span>`:""}
88      </div>
89      ${y.ai_generated?'<div class="pc-ai-banner">\u26A0\uFE0F AI-Generated Estimate: stats may not be exact</div>':""}
90      <div class="ddb-combat-row">
91        <span class="ddb-stat"><span class="ddb-stat-label">HP</span>${y.hp}</span>
92        <span class="ddb-stat"><span class="ddb-stat-label">AC</span>${y.ac}</span>
93        <span class="ddb-stat"><span class="ddb-stat-label">Init</span>${I}</span>
94        <span class="ddb-stat"><span class="ddb-stat-label">Speed</span>${T}</span>
95      </div>
96      <div class="pill-section-gap"></div>
97      <div class="ddb-ability-grid">
98        <div class="${M(B,"perc")}"><div class="ddb-ab-label">Pass. Perc</div><div class="ddb-ab-mod">${B}</div></div>
99        <div class="${M(H,"inv")}"><div class="ddb-ab-label">Pass. Inv</div><div class="ddb-ab-mod">${H}</div></div>
100        <div class="${M(A,"ins")}"><div class="ddb-ab-label">Pass. Ins</div><div class="ddb-ab-mod">${A}</div></div>
101      </div>
102      <div class="pill-section-gap"></div>
103      <div class="ddb-ability-grid">${S}</div>
104      <!-- v1150: kitchen-sink sections wrapped so the Party tab's TL;DR
105           mode (.party-cards-grid--tldr) can collapse the card down to
106           HP / passives / main stats only. Everything in here hides
107           when TL;DR is active; the wrapper itself is transparent. -->
108      <div class="party-pc-extras">
109      <div class="pill-section-gap"></div>
110      ${F}
111      <div class="pill-section-gap"></div>
112      ${Ce("Defenses",de)}
113      ${Ce("Senses",ue)}
114      <div class="pill-section-gap"></div>
115      ${Ce("Languages",ye)}
116      ${Ce("Profs",Me)}
117      ${Ce("Tools",Ue)}
118      <div class="pill-section-gap"></div>
119      ${(()=>{const j=ae=>{const ee=(ae||"").match(/^Spell Scroll \((.+)\)$/i);return ee?`Scroll of ${ee[1]}`:ae},Z={};(y.magic_items||[]).forEach(ae=>{const ee=j(ae);Z[ee]=(Z[ee]||0)+1});const ie=(ae,ee)=>{const ke=_itemRarityForName(ae,e),Te=ke?` item-rarity-${ke.slug}`:"",Ne=ee>1?`${ae} \xD7${ee}`:ae;return`<span class="party-trait-tag${Te}">${escHtml(Ne)}</span>`},pe=Object.entries(Z).map(([ae,ee])=>ie(ae,ee)).join(""),re=_rosterPcs.find(ae=>ae.entity_id===e.id),_e=_isGm()||re&&currentUser?.email&&currentUser.email===re.assigned_email,fe=y.currencies||{},$e=ae=>ae.toLocaleString(),Ee=(ae,ee,ke)=>ae>0?`<span class="coin-group"><span class="coin-badge ${ee}">${$e(ae)}</span><span class="coin-label">${ke}</span></span>`:"",Ae=_e&&(fe.pp||fe.gp||fe.ep||fe.sp||fe.cp)?[Ee(fe.pp||0,"coin-pp","pp"),Ee(fe.gp||0,"coin-gp","gp"),Ee(fe.ep||0,"coin-ep","ep"),Ee(fe.sp||0,"coin-sp","sp"),Ee(fe.cp||0,"coin-cp","cp")].filter(Boolean).join(""):"";return Ce("Items",pe)+Ce("Coins",Ae)})()}
120      <div class="pill-section-gap"></div>
121      ${Ce("Feats",(y.feats||[]).filter(j=>!O.includes(j)).map(j=>Q(j,!1)).join(""))}
122      ${Array.isArray(e.key_spells)&&e.key_spells.length?(()=>{const j=partySpellUsage[String(e.id)]||{},Z=Math.max(0,...Object.values(j).map(Number)),ie=e.key_spells.map(pe=>{const re=j[pe]||0,_e=Z>0&&re===Z,fe=re>0?`<span class="spell-use-count">${re}</span>`:"";return`<span class="party-trait-tag${_e?" key-spell-fav":""}">${escHtml(pe)}${fe}</span>`}).join("");return Ce("Abilities",ie)})():""}
123      ${h}
124      ${(()=>{const j=partyBestQuotes[String(e.id)];if(!j||!j.length)return"";const Z=j[Math.floor(Math.random()*j.length)],ie=Z.context?`<span class="pc-best-quote-ctx">${escHtml(Z.context)}</span>`:"";return`<hr class="pc-quote-divider" />
125          <div class="pc-best-quote" data-entity-quotes="${escHtml(JSON.stringify(j))}" title="Make a shareable quote card"><span>&ldquo;<em>${escHtml(Z.quote)}</em>&rdquo;</span>${ie}</div>`})()}
126      </div>
127    </div>
128  </div>`}function _applyAutoFailedQuests(e){const t=e.auto_failed_quests;if(!t||!t.length)return;const n={};t.forEach(s=>
128{n[s.id]=s}),allQuests=allQuests.map(s=>n[s.id]||s);const a=t.map(s=>s.title).join(", ");showToast(`\u274C Quest${t.length>1?"s":""} failed: ${a}`,5e3),_questsSurfaceVisible()&&loadQuests()}let _treasuryDraftItems=[];function _fmtRelTime(e){if(!e)return"";const t=Date.now()-new Date(e).getTime();return t<6e4?"just now":t<36e5?`${Math.floor(t/6e4)}m ago`:t<864e5?`${Math.floor(t/36e5)}h ago`:new Date(e).toLocaleDateString(void 0,{month:"short",day:"numeric"})}function _openTreasuryModal(){const e=partyTreasury||{pp:0,gp:0,ep:0,sp:0,cp:0,items:[]};_treasuryDraftItems=JSON.parse(JSON.stringify(e.items||[])),document.getElementById("treasury-input-pp").value=e.pp||0,document.getElementById("treasury-input-gp").value=e.gp||0,document.getElementById("treasury-input-ep").value=e.ep||0,document.getElementById("treasury-input-sp").value=e.sp||0,document.getElementById("treasury-input-cp").value=e.cp||0;const t=document.getElementById("treasury-item-select"),n=allEntities.filter(a=>a.type==="item").sort((a,s)=>a.name.localeCompare(s.name));t.innerHTML='<option value="">(pick from codex)</option>'+n.map(a=>`<option value="${a.id}" data-name="${escHtml(a.name)}">${escHtml(a.name)}</option>`).join(""),_renderTreasuryItems(),document.getElementById("party-treasury-modal").classList.remove("hidden")}function _closeTreasuryModal(){document.getElementById("party-treasury-modal").classList.add("hidden")}function _renderTreasuryItems(){const e=document.getElementById("treasury-items-list");if(!_treasuryDraftItems.length){e.innerHTML='<p class="placeholder">No items in bag.</p>';return}e.innerHTML=_treasuryDraftItems.map((t,n)=>`<div class="treasury-item-row">
129      <span class="party-trait-tag" style="flex:1">${escHtml(t.name)}</span>
130      <input type="number" class="treasury-qty-input" value="${t.quantity}" min="1" data-idx="${n}" style="width:52px">
131      <button class="btn-ghost btn-xs treasury-remove-btn" data-idx="${n}">\u2715</button>
132    </div>`).join(""),e.querySelectorAll(".treasury-qty-input").forEach(t=>{t.addEventListener("change",n=>{const a=parseInt(n.target.dataset.idx);_treasuryDraftItems[a].quantity=Math.max(1,parseInt(n.target.value)||1)})}),e.querySelectorAll(".treasury-remove-btn").forEach(t=>{t.addEventListener("click",n=>{const a=parseInt(n.target.dataset.idx);_treasuryDraftItems.splice(a,1),_renderTreasuryItems()})})}function _addTreasuryItem(){const e=document.getElementById("treasury-item-select"),t=Math.max(1,parseInt(document.getElementById("treasury-item-qty").value)||1),n=parseInt(e.value);if(!n)return;const a=e.options[e.selectedIndex].dataset.name,s=_treasuryDraftItems.find(i=>i.entity_id===n);s?s.quantity+=t:_treasuryDraftItems.push({name:a,entity_id:n,quantity:t}),e.value="",document.getElementById("treasury-item-qty").value=1,_renderTreasuryItems()}async function _createAndAddTreasuryItem(){const e=document.getElementById("treasury-new-item-name"),t=document.getElementById("treasury-new-item-status"),n=(e.value||"").trim();if(!n){t.textContent="Enter an item name first.";return}t.textContent="Creating\u2026";try{const a=await api("POST",`/api/campaigns/${currentCampaign.id}/entities`,{name:n,type:"item",description:"",tags:[]});allEntities.push(a);const s=document.getElementById("treasury-item-select"),i=allEntities.filter(r=>r.type==="item").sort((r,c)=>r.name.localeCompare(c.name));s.innerHTML='<option value="">(pick from codex)</option>'+i.map(r=>`<option value="${r.id}" data-name="${escHtml(r.name)}">${escHtml(r.name)}</option>`).join("");const o=_treasuryDraftItems.find(r=>r.entity_id===a.id);o?o.quantity+=1:_treasuryDraftItems.push({name:a.name,entity_id:a.id,quantity:1}),e.value="",t.textContent=`\u2713 "${a.name}" created and added.`,_renderTreasuryItems()}catch(a){t.textContent="Error: "+a.message}}async function _saveTreasury(){const e={pp:parseInt(document.getElementById("treasury-input-pp").value)||0,gp:parseInt(document.getElementById("treasury-input-gp").value)||0,ep:parseInt(document.getElementById("treasury-input-ep").value)||0,sp:parseInt(document.getElementById("treasury-input-sp").value)||0,cp:parseInt(document.getElementById("treasury-input-cp").value)||0,items:_treasuryDraftItems},t=document.getElementById("treasury-save-btn");t.disabled=!0,t.textContent="Saving\u2026";try{partyTreasury=await api("PUT",`/api/campaigns/${currentCampaign.id}/party-treasury`,e),_closeTreasuryModal()}catch(n){showToast("Failed to save treasury: "+n.message,4e3)}finally{t.disabled=!1,t.textContent="Save"}}function _tfSheetImportDesc(){return`Upload a PDF or image of a filled ${typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()?"Daggerheart":"D&D 5e"} character sheet, or a Foundry / Forge VTT character export (JSON). Threadfall will extract the stats automatically.`}let _importEntityId=null,_importParsedResult=null,_importFileObjectURL=null;const _IMPORT_SECTION_DEFS=[{key:"identity",label:"Identity",fields:[{key:"name",label:"Name",type:"text"},{key:"level",label:"Level",type:"number"},{key:"classes",label:"Classes",type:"text",hint:"e.g. Fighter 3, Rogue 2"},{key:"species",label:"Species",type:"text"}
132,{key:"background",label:"Background",type:"text"},{key:"alignment",label:"Alignment",type:"text"}]},{key:"combat",label:"Combat",fields:[{key:"hp",label:"Max HP",type:"text"},{key:"ac",label:"AC",type:"number"},{key:"speed",label:"Speed (ft)",type:"number"},{key:"initiative",label:"Initiative",type:"number"},{key:"prof_bonus",label:"Prof Bonus",type:"number"},{key:"hit_dice",label:"Hit Dice",type:"text"}]},{key:"ability_scores",label:"Ability Scores",special:"abilities"},{key:"skills_saves",label:"Saves & Perception",fields:[{key:"passive_perception",label:"Passive Perception",type:"number"},{key:"passive_investigation",label:"Passive Investigation",type:"number"},{key:"passive_insight",label:"Passive Insight",type:"number"}]},{key:"spellcasting",label:"Spellcasting",fields:[{key:"spellcasting_ability",label:"Casting Ability",type:"text"},{key:"spell_save_dc",label:"Spell Save DC",type:"number"},{key:"spell_attack_bonus",label:"Spell Attack",type:"number"}]},{key:"proficiencies",label:"Proficiencies & Languages",fields:[{key:"languages",label:"Languages",type:"text",hint:"Comma-separated"},{key:"armor_weapon_profs",label:"Armor / Weapons",type:"text",hint:"Comma-separated"},{key:"tool_profs",label:"Tool Proficiencies",type:"text",hint:"Comma-separated"},{key:"feats",label:"Feats",type:"text",hint:"Comma-separated"}]},{key:"personality",label:"Personality",fields:[{key:"personality_traits",label:"Traits",type:"textarea"},{key:"ideals",label:"Ideals",type:"textarea"},{key:"bonds",label:"Bonds",type:"textarea"},{key:"flaws",label:"Flaws",type:"textarea"}]}],_ABILITY_KEYS=["STR","DEX","CON","INT","WIS","CHA"];function _openImportReview(e){try{currentCampaign&&localStorage.setItem("tf_roll20_import_cid",String(currentCampaign.id))}catch{}typeof _tfRoll20RefreshBookmarklet=="function"&&_tfRoll20RefreshBookmarklet(),_importEntityId=e,_importParsedResult=null,_importFileObjectURL&&(URL.revokeObjectURL(_importFileObjectURL),_importFileObjectURL=null);const t=e?allEntities.find(o=>o.id===e):null;document.getElementById("import-review-title").textContent=t?`Re-import: ${t.name}`:"Import Character",document.getElementById("import-reimport-warn").classList.toggle("hidden",!t?.pc_import_at),document.getElementById("import-pick-error").classList.add("hidden"),document.getElementById("import-file-input").value="",document.getElementById("import-camera-input").value="",document.getElementById("import-review-save").textContent=e?"Apply to Character":"Create Character",document.querySelectorAll(".import-pick-option--last").forEach(function(o){o.classList.remove("import-pick-option--last");var r=o.querySelector(".import-pick-last-badge");r&&r.remove()});const n=document.getElementById("import-pick-last-note"),a=_importLastUsedSource(e),s=a&&_IMPORT_LAST_USED_TILE_ID[a.src],i=s?document.getElementById(s):null;if(i&&(i.classList.add("import-pick-option--last"),i.insertAdjacentHTML("beforeend",'<span class="import-pick-last-badge">Last used</span>')),a){const o=_pcAgoCompact(a.whenIso),r=o?`, ${o}`:"";n.textContent=t?`${t.name} last came from ${a.label}${r}.`:`Your last import in this campaign used ${a.label}${r}.`,n.classList.remove("hidden")}else n.textContent="",n.classList.add("hidden");_importShowStep("pick"),document.getElementById("import-review-modal").classList.remove("hidden")}function _closeImportReview(){document.getElementById("import-review-modal").classList.add("hidden"),_importFileObjectURL&&(URL.revokeObjectURL(_importFileObjectURL),_importFileObjectURL=null),typeof _tfRoll20Teardown=="function"&&_tfRoll20Teardown("import-window-closed")}function _importShowStep(e){["pick","parsing","review"].forEach(t=>{document.getElementById(`import-step-${t}`).classList.toggle("hidden",t!==e)})}function _isFoundryJsonFile(e){return e&&(e.type==="application/json"||/\.json$/i.test(e.name||""))}async function _importFoundryJson(e,t){const n=new FormData;n.append("file",e),t&&n.append("entity_id",t);const a=await fetch(`/api/campaigns/${currentCampaign.id}/foundry-import`,{method:"POST",headers:{Authorization:`Bearer ${localStorage.getItem("tf_jwt")}`},body:n});if(!a.ok){const i=await a.json().catch(()=>({detail:"Import failed"}));throw new Error(i.detail||"Import failed")}const s=await a.json();return allEntities.some(i=>i.id===s.id)?allEntities=allEntities.map(i=>i.id===s.id?s:i):allEntities=[...allEntities,s],s}const TF_SHEET_LONG_EDGE=2576,TF_SHEET_MAX_BYTES=4*1024*1024;async function _tfFitSheetImage(e){if(!e.type.startsWith("image/")||e.size<=TF_SHEET_MAX_BYTES)return e;try{const t=await createImageBitmap(e,{imageOrientation:"from-image"});let n=t.width,a=t.height;const s=Math.min(1,TF_SHEET_LONG_EDGE/Math.max(n,a));n=Math.round(n*s),a=Math.round(a*s);const i=document.createElement("canvas");i.width=n,i.height=a;const o=i.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,n,a),o.drawImage(t,0,0,n,a),t.close?.();const r=l=>new File([l],e.name.replace(/\.[^.]+$/,"")+".jpg",{type:"image/jpeg"});let c=null;for(const l of[.92,.85,.75,.6]){const d=await new Promise(u=>i.toBlob(u,"image/jpeg",l));if(d){if(d.size<=TF_SHEET_MAX_BYTES)return r(d);(!c||d.size<c.size)&&(c=d)}}return c&&c.size<e.size?r(c):e}catch{return e}}async function _importHandleFile(e){if(!e)return;if(_isFoundryJsonFile(e)){_importShowStep("parsing");try{const s=await _importFoundryJson(e,_importEntityId);_closeImportReview();const i=(()=>{try{return JSON.parse(s.dnd_beyond_data)}catch{return{}}})();showToast(`\u{1F4C4} ${s.foundry_created?"Created":"Updated"} ${s.name} from Foundry VTT${i.level?` \xB7 Level ${i.level}`:""}`)}catch(s){_importShowStep("pick"),document.getElementById("import-pick-error").textContent="\u274C "+s.message,document.getElementById("import-pick-error").classList.remove("hidden")}return}
132if(!["application/pdf","image/jpeg","image/png","image/gif","image/webp"].includes(e.type)&&!e.name.endsWith(".pdf")){document.getElementById("import-pick-error").textContent="Unsupported file type. Upload a PDF, image, or Foundry VTT JSON.",document.getElementById("import-pick-error").classList.remove("hidden");return}if(e=await _tfFitSheetImage(e),e.size>TF_SHEET_MAX_BYTES){document.getElementById("import-pick-error").textContent=e.type==="application/pdf"?"That PDF is too large to upload. Export it under 4 MB, or photograph the sheet instead.":"Still too large after resizing. It needs to be under 4 MB.",document.getElementById("import-pick-error").classList.remove("hidden");return}e.type.startsWith("image/")&&(_importFileObjectURL&&URL.revokeObjectURL(_importFileObjectURL),_importFileObjectURL=URL.createObjectURL(e)),_importShowStep("parsing");const n=new FormData;n.append("file",e);const a=localStorage.getItem("tf_jwt");try{const s=await fetch(`/api/campaigns/${currentCampaign.id}/parse-sheet`,{method:"POST",headers:{Authorization:`Bearer ${a}`},body:n});if(!s.ok){const i=await s.json().catch(()=>({detail:"Unknown error"}));throw new Error(i.detail||"Parsing failed")}_importParsedResult=await s.json(),_importRenderReview(),_importShowStep("review")}catch(s){_importShowStep("pick"),document.getElementById("import-pick-error").textContent="\u274C "+s.message,document.getElementById("import-pick-error").classList.remove("hidden")}}function _importRenderReview(){const{fields:e={},confidence:t={}}=_importParsedResult||{},n=document.getElementById("import-review-warning");if(n){const i=Object.values(t),o=i.filter(r=>r&&r!=="high").length;n.classList.toggle("hidden",!(i.length>=4&&o/i.length>.3))}const a=document.getElementById("import-preview-pane");_importFileObjectURL?a.innerHTML=`
133      <p class="import-preview-label">Sheet Preview</p>
134      <img src="${_importFileObjectURL}" class="import-preview-thumb" alt="Sheet preview" />
135    `:a.innerHTML='<div class="import-preview-no-preview">\u{1F4C4}<span>PDF uploaded, no preview available</span></div>';const s=document.getElementById("import-form-pane");if(s.innerHTML="",typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()){_importRenderReviewDh(s,e);return}_IMPORT_SECTION_DEFS.forEach(i=>{const o=document.createElement("div");if(o.className="import-section",o.innerHTML=`<div class="import-section-title">${i.label}</div>`,i.special==="abilities"){const r=e.ability_scores||{},c=t.ability_scores||"medium",l=document.createElement("div");l.className="import-ability-grid",_ABILITY_KEYS.forEach(d=>{const u=r[d]?.score??"",p=r[d]?.mod,m=p!==void 0?`${p>=0?"+":""}${p}`:"\u2014";l.innerHTML+=`
136          <div class="import-ability-cell">
137            <label>${d}</label>
138            <input type="number" data-ab="${d}" value="${u}" min="1" max="30" />
139            <span class="import-ability-mod" id="import-ab-mod-${d}">${m}</span>
140          </div>`}),o.appendChild(l),l.querySelectorAll("input[data-ab]").forEach(d=>{d.addEventListener("input",()=>{const u=parseInt(d.value),p=d.dataset.ab;document.getElementById(`import-ab-mod-${p}`).textContent=isNaN(u)?"\u2014":`${Math.floor((u-10)/2)>=0?"+":""}${Math.floor((u-10)/2)}`})})}else{const r=document.createElement("div");r.className="import-fields-grid",i.fields.forEach(c=>{let l=e[c.key];const d=t[c.key]||(l!==void 0?"medium":"missing"),u=Array.isArray(l)?l.join(", "):l??"",p=document.createElement("div");p.className=`import-field conf-${d}`;const m=d!=="high"?`<span class="conf-badge conf-badge--${d}">${d}</span>`:"",f=c.type==="textarea"?`<textarea data-key="${c.key}" rows="2">${escHtml(String(u))}</textarea>`:`<input type="${c.type}" data-key="${c.key}" value="${escHtml(String(u))}" ${c.hint?`placeholder="${escHtml(c.hint)}"`:""} />`;p.innerHTML=`
141          <span class="import-field-label">${c.label}${m}</span>
142          ${f}`,r.appendChild(p)}),o.appendChild(r)}s.appendChild(o)})}function _importRenderReviewDh(e,t){if(!_importEntityId){const n=document.createElement("div");n.className="pse-section",n.innerHTML=`<label class="pse-label"><span class="pc-stats-field-lbl">Name</span><input type="text" data-dh="name" class="form-input" value="${esc
142Html(String(t.name??""))}" /></label>`,e.appendChild(n)}document.querySelectorAll("#pc-stats-edit-modal .pse-section.daggerheart-only").forEach(n=>{if(!n.querySelector('[id^="dhse-"]'))return;const a=n.cloneNode(!0);a.querySelectorAll('[id^="dhse-"]').forEach(s=>{s.dataset.dh=s.id.slice(5),s.removeAttribute("id")}),e.appendChild(a)}),_dhFormFill(n=>e.querySelector(`[data-dh="${n}"]`),t)}function _importCollectFields(){const e={},t=document.getElementById("import-form-pane");t.querySelectorAll("[data-key]").forEach(a=>{const s=a.dataset.key,i=a.value.trim();if(!i)return;if(["classes","languages","armor_weapon_profs","tool_profs","feats","resistances","immunities","vulnerabilities","skill_proficiencies","advantage_skills"].includes(s))e[s]=i.split(",").map(r=>r.trim()).filter(Boolean);else if(a.type==="number"){const r=parseFloat(i);isNaN(r)||(e[s]=r)}else e[s]=i});const n={};return t.querySelectorAll("[data-ab]").forEach(a=>{const s=a.dataset.ab,i=parseInt(a.value);isNaN(i)||(n[s]={score:i,mod:Math.floor((i-10)/2)})}),Object.keys(n).length&&(e.ability_scores=n),e}async function _importSave(){const e=document.getElementById("import-review-save"),t=document.getElementById("import-review-status");e.disabled=!0,t.textContent="Saving\u2026";const n=document.getElementById("import-form-pane"),a=new Set(Array.from(n?.querySelectorAll("[data-key]")||[]).map(o=>o.dataset.key));n?.querySelector("[data-ab]")&&a.add("ability_scores");const s={..._importParsedResult?.fields||{}};if(a.forEach(o=>{delete s[o]}),Object.assign(s,_importCollectFields()),n?.querySelector("[data-dh]")){const o=c=>n.querySelector(`[data-dh="${c}"]`);_DH_FORM_KEYS.forEach(c=>{delete s[c]}),Object.assign(s,_dhFormRead(o));const r=o("name");if(r){delete s.name;const c=String(r.value||"").trim();c&&(s.name=c)}}const i=_importParsedResult?.source_type||"import";try{const o=localStorage.getItem("tf_jwt");let r,c=_importEntityId;if(!c){const d=String(s.name||"").trim().toLowerCase(),u=d?(allEntities||[]).filter(p=>p&&p.type==="pc"&&String(p.name||"").trim().toLowerCase()===d):[];if(u.length>1)throw new Error(`Multiple PCs named '${String(s.name).trim()}' exist, open the one to update and import from its sheet panel.`);u.length===1&&(c=u[0].id)}if(c){const d=await fetch(`/api/campaigns/${currentCampaign.id}/entities/${c}/apply-parsed-sheet`,{method:"POST",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},body:JSON.stringify({fields:s,source_type:i})});if(!d.ok)throw new Error((await d.json().catch(()=>({}))).detail||"Save failed");r=await d.json(),allEntities=allEntities.map(u=>u.id===r.id?r:u)}else{const d=s.name||"Unnamed Character",u=await api("POST",`/api/campaigns/${currentCampaign.id}/entities`,{name:d,type:"pc"}),p=await fetch(`/api/campaigns/${currentCampaign.id}/entities/${u.id}/apply-parsed-sheet`,{method:"POST",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},body:JSON.stringify({fields:s,source_type:i})});if(!p.ok)throw new Error((await p.json().catch(()=>({}))).detail||"Save failed");r=await p.json(),allEntities=[...allEntities,r]}_closeImportReview();const l=(()=>{try{return JSON.parse(r.dnd_beyond_data)}catch{return{}}})();showToast(`\u{1F4C4} ${c?"Updated":"Created"} ${r.name}${l.level?` \xB7 Level ${l.level}`:""}`)}catch(o){t.textContent="\u274C "+o.message,e.disabled=!1}}document.getElementById("import-review-close").addEventListener("click",_closeImportReview),document.getElementById("import-pick-cancel").addEventListener("click",_closeImportReview),document.getElementById("import-review-cancel").addEventListener("click",_closeImportReview),document.getElementById("import-review-back").addEventListener("click",()=>_importShowStep("pick")),document.getElementById("import-review-save").addEventListener("click",_importSave);let _importModalMousedownOnBackdrop=!1;document.getElementById("import-review-modal").addEventListener("mousedown",e=>{_importModalMousedownOnBackdrop=e.target===document.getElementById("import-review-modal")}),document.getElementById("import-review-modal").addEventListener("click",e=>{e.target===document.getElementById("import-review-modal")&&_importModalMousedownOnBackdrop&&_closeImportReview()}),document.getElementById("import-file-input").addEventListener("change",e=>{_importHandleFile(e.target.files[0])}
142),document.getElementById("import-camera-input").addEventListener("change",e=>{_importHandleFile(e.target.files[0])}),document.getElementById("import-pick-file-label").addEventListener("click",e=>{}),document.getElementById("import-pick-camera-label").addEventListener("click",e=>{});const TF_ROLL20_ORIGIN="https://app.roll20.net",TF_DDB_ORIGIN="https://www.dndbeyond.com",TF_IMPORT_BOOKMARK_NAME="TF";function _tfImportBookmarkSystem(){let e=!1,t=!1,n=!1,a=!1;return(allEntities||[]).forEach(s=>{if(s.type!=="pc"||!s.dnd_beyond_data)return;let i={};try{i=JSON.parse(s.dnd_beyond_data)||{}}catch{i={}}if(Object.keys(i).length){if(a=!0,i.roll20_id){e=!0;return}if((s.dnd_beyond_url||"").indexOf("dndbeyond.com")!==-1){t=!0;return}n=!0}}),a?e&&!t&&!n?"roll20":t&&!e&&!n?"ddb":null:null}function _tfImportBookmarkLabel(e){const t=typeof _isGm=="function"&&_isGm(),n=_tfImportBookmarkSystem();let a;return n==="roll20"?a=t?"from R20 tabletop to TF":"from my R20 sheet to TF":n==="ddb"?a=t?"from D&DB campaign to TF":"from my D&DB sheet to TF":a=t?"from tabletop or campaign to TF":"from my sheet to TF",`${e} \xB7 ${a}`}function _tfImportPanelHeading(){return typeof _isGm=="function"&&_isGm()?"Drag this to your bookmarks bar, then click it on your Roll20 tabletop or D&D Beyond campaign page to import every player character, or on any character sheet for one.":"Drag this to your bookmarks bar, then click it on your character sheet."}function _tfDdbCharIdFromUrl(e){if(!e)return null;const t=/\/characters?\/(\d+)/.exec(String(e));return t?t[1]:null}function _tfRoll20RefreshBookmarklet(){const e=document.getElementById("import-roll20-bookmarklet");if(!e)return;const t=location.origin,n=currentCampaign?currentCampaign.id:null,a=currentCampaign&&currentCampaign.name||"",s=(typeof allEntities<"u"&&Array.isArray(allEntities)?allEntities:[]).filter(r=>r&&r.type==="pc").map(r=>{let c=null;try{c=r.dnd_beyond_data?JSON.parse(r.dnd_beyond_data):null}catch{}return{id:r.id,name:r.name||"",ddb:_tfDdbCharIdFromUrl(r.dnd_beyond_url)||null,r20:c&&c.roll20_id?String(c.roll20_id):null}});e.href="javascript:(function(){if(location.host!=='app.roll20.net'&&location.host!=='www.dndbeyond.com'){alert('Drag this button to your bookmarks bar, then click it on Roll20 or D&D Beyond.');return;}window.__tfR20={origin:'"+t+"',cid:'"+(n||"")+"',name:"+JSON.stringify(a)+",pcs:"+JSON.stringify(s)+"};fetch('"+t+"/static/roll20_export.js?v='+Date.now()).then(function(r){return r.text()}).then(function(t){(new Function(t))()}).catch(function(){alert('Could not load the Threadfall importer. Check your connection and try again.')});})();";const i=a?_tfImportBookmarkLabel(a):TF_IMPORT_BOOKMARK_NAME;e.textContent=i,e.title=a?`Bookmark for ${a}. Drag to the bookmarks bar.`:"Drag to the bookmarks bar.";const o=document.querySelector("#import-roll20-panel .r20-setup-heading");o&&(o.textContent=_tfImportPanelHeading())}_tfRoll20RefreshBookmarklet();const _IMPORT_SOURCE_PANELS={roll20:"import-roll20-panel",demiplane:"import-demiplane-panel"};function _importOpenSourcePanel(e){const t=_IMPORT_SOURCE_PANELS[e],n=t&&document.getElementById(t);if(!n)return;
142Object.values(_IMPORT_SOURCE_PANELS).forEach(s=>{s!==t&&document.getElementById(s)?.classList.add("hidden")}),n.classList.remove("hidden");const a=_importEntityId?allEntities.find(s=>s.id===_importEntityId):null;if(e==="roll20"){const s=document.getElementById("pse-ddb-url");s&&(s.value=a?.dnd_beyond_url||""),document.getElementById("pse-ddb-status")?.classList.add("hidden")}else if(e==="demiplane"){_editStatsEntityId=_importEntityId;const s=(()=>{try{return JSON.parse(a?.dnd_beyond_data||"{}")}catch{return{}}})(),i=document.getElementById("dhse-demiplane-url");i&&(i.value=(s.system==="daggerheart"?s.demiplane_url:"")||""),document.getElementById("dhse-demiplane-status")?.classList.add("hidden")}}function _importTogglePanel(e){const t=_IMPORT_SOURCE_PANELS[e],n=t&&document.getElementById(t);if(!n)return;const a=!n.classList.contains("hidden");Object.values(_IMPORT_SOURCE_PANELS).forEach(s=>document.getElementById(s)?.classList.add("hidden")),a||_importOpenSourcePanel(e)}document.getElementById("import-pick-roll20-btn")?.addEventListener("click",()=>_importTogglePanel("roll20")),document.getElementById("import-pick-ddb-btn")?.addEventListener("click",()=>_importTogglePanel("roll20")),document.getElementById("import-pick-demiplane-btn")?.addEventListener("click",()=>_importTogglePanel("demiplane")),document.getElementById("import-vtt-file-input")?.addEventListener("change",e=>{_importHandleFile(e.target.files[0])}),document.getElementById("import-ddb-save-btn")?.addEventListener("click",async()=>{const e=document.getElementById("pse-ddb-url"),t=document.getElementById("pse-ddb-status"),n=document.getElementById("import-ddb-save-btn"),a=(i,o)=>{t&&(t.textContent=i,t.classList.remove("hidden"),t.classList.toggle("error",!!o))};if(!_importEntityId||!currentCampaign){a("Create the character first, then add a D&D Beyond link.",!0);return}const s=(e?.value||"").trim()||null;n.disabled=!0,n.textContent="Saving\u2026";try{const i=await api("PATCH",`/api/campaigns/${currentCampaign.id}/entities/${_importEntityId}`,{dnd_beyond_url:s});allEntities=allEntities.map(o=>o.id===i.id?i:o),a("Saved.")}catch(i){a(i.message,!0)}finally{n.disabled=!1,n.textContent="Save"}});const _PSE_SKILL_NAMES=["Acrobatics","Animal Handling","Arcana","Athletics","Deception","History","Insight","Intimidation","Investigation","Medicine","Nature","Perception","Performance","Persuasion","Religion","Sleight of Hand","Stealth","Survival"],_PSE_SKILL_ABBR={Acrobatics:"Acro.","Animal Handling":"Animal.",Arcana:"Arcana",Athletics:"Athl.",Deception:"Decep.",History:"Hist.",Insight:"Insight",Intimidation:"Intim.",Investigation:"Inves.",Medicine:"Med.",Nature:"Nature",Perception:"Percep.",Performance:"Perf.",Persuasion:"Persua.",Religion:"Relig.","Sleight of Hand":"Sleight.",Stealth:"Stealth",Survival:"Surv."},_PSE_STATS=["STR","DEX","CON","INT","WIS","CHA"];function _pseSkillId(e){return"pse-skill-"+e.toLowerCase().replace(/ /g,"-")}function _pseSkillAdvId(e){return"pse-sadv-"+e.toLowerCase().replace(/ /g,"-")}const _DH_FORM_TRAITS=["agility","strength","finesse","instinct","presence","knowledge"],_DH_FORM_KEYS=["level","class","subclass","ancestry","community","classes","traits","evasion","armor_score","thresholds","hp_max","stress_max","hope_max","scars","experiences","domain_cards","weapons","gold"];function _dhFormFill(e,t){t=t||{};const n=(s,i)=>{const o=e(s);o&&(o.value=i??"")},a=t.traits||{};n("level",t.level),n("class",t.class),n("subclass",t.subclass),n("ancestry",t.ancestry),n("community",t.community),_DH_FORM_TRAITS.forEach(s=>n(s,a[s])),n("evasion",t.evasion),n("armor",t.armor_score),n("thr-major",t.thresholds?.major),n("thr-severe",t.thresholds?.severe),n("hp",t.hp_max),n("stress",t.stress_max),n("hope",t.hope_max),n("scars",t.scars),n("experiences",(t.experiences||[]).map(s=>{if(!(s&&typeof s=="object"))return String(s);const i=Number(s.mod);return`${s.name}${s.mod!=null&&isFinite(i)?` ${i<0?"-":"+"}${Math.abs(i)}`:""}`}).join("; ")),n("domain-cards",(t.domain_cards||[]).map(String).join(", ")),n("weapons",(t.weapons||[]).map(String).join("; ")),n("gold",t.gold)}function _dhFormRead(e){const t=w=>{const $=e(w);return $?String($.value||"").trim():""},n=w=>{const $=parseInt(t(w),10);return isNaN($)?null:$},a=w=>{const $=t(w);if(!$)return[];if($.includes(";"))return $.split(";").map(S=>
142S.trim()).filter(Boolean);const E=[];let k=0,C="";for(const S of $){if(S==="("||S==="["?k++:(S===")"||S==="]")&&(k=Math.max(0,k-1)),S===","&&k===0){E.push(C.trim()),C="";continue}C+=S}return C.trim()&&E.push(C.trim()),E.filter(Boolean)},s={system:"daggerheart"},i=n("level");i!==null&&(s.level=i);const o=t("class");o&&(s.class=o);const r=t("subclass");r&&(s.subclass=r);const c=t("ancestry");c&&(s.ancestry=c);const l=t("community");l&&(s.community=l),o&&(s.classes=[`${o}${i!==null?" "+i:""}${r?" ("+r+")":""}`]);const d={};_DH_FORM_TRAITS.forEach(w=>{const $=n(w);$!==null&&(d[w]=$)}),Object.keys(d).length&&(s.traits=d);const u=n("evasion");u!==null&&(s.evasion=u);const p=n("armor");p!==null&&(s.armor_score=p);const m={},f=n("thr-major");f!==null&&(m.major=f);const g=n("thr-severe");g!==null&&(m.severe=g),Object.keys(m).length&&(s.thresholds=m);const h=n("hp");h!==null&&(s.hp_max=h);const v=n("stress");v!==null&&(s.stress_max=v);const b=n("hope");b!==null&&(s.hope_max=b);const y=n("scars");y!==null&&(s.scars=y),s.experiences=a("experiences").map(w=>{const $=w.match(/^(.*?)(?:\s*\+\s*|\s+[-−]\s*)(\d+)$/);if(!$||!$[1].trim())return{name:w,mod:null};const E=!/\+\s*\d+$/.test(w);return{name:$[1].trim(),mod:(E?-1:1)*parseInt($[2],10)}}),s.domain_cards=a("domain-cards"),s.weapons=a("weapons");const _=t("gold");return _&&(s.gold=_),s}let _editStatsEntityId=null;function _openEditStatsModal(e){_editStatsEntityId=e;const t=allEntities.find(p=>p.id===e),n=(()=>{try{return JSON.parse(t?.dnd_beyond_data||"{}")}catch{return{}}})(),a=n.ability_scores||{};if(document.getElementById("pc-stats-edit-title").textContent=`Edit Stats: ${t?.name||""}`,typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()){const p=n&&n.system==="daggerheart"?n:{};_dhFormFill(f=>document.getElementById(`dhse-${f}`),p),document.getElementById("dhse-demiplane-url").value=p.demiplane_url??"",document.getElementById("dhse-demiplane-status").classList.add("hidden"),document.getElementById("pc-stats-edit-error").classList.add("hidden"),document.getElementById("pc-stats-edit-save-btn").disabled=!1,document.getElementById("pc-stats-edit-save-btn").textContent="Save Stats";const m=typeof _isGm=="function"&&_isGm()||!!(currentUser&&currentUser.is_admin);document.querySelector("#pc-stats-edit-modal .tf-danger-zone")?.classList.toggle("hidden",!m),document.getElementById("pc-stats-edit-modal").classList.remove("hidden");return}document.getElementById("pse-level").value=n.level??"",document.getElementById("pse-classes").value=(n.classes||[]).join(", "),document.getElementById("pse-species").value=n.species??"",document.getElementById("pse-background").value=n.background??"",document.getElementById("pse-hp").value=n.hp??"",document.getElementById("pse-ac").value=n.ac??"",document.getElementById("pse-speed").value=n.speed??"",document.getElementById("pse-initiative").value=n.initiative??"",document.getElementById("pse-prof-bonus").value=n.prof_bonus??"";for(const[p,m]of[["STR","pse-str"],["DEX","pse-dex"],["CON","pse-con"],["INT","pse-int"],["WIS","pse-wis"],["CHA","pse-cha"]])document.getElementById(m).value=a[p]?.score??"";document.getElementById("pse-passive-perception").value=n.passive_perception??"",document.getElementById("pse-passive-investigation").value=n.passive_investigation??"",document.getElementById("pse-passive-insight").value=n.passive_insight??"";const s=document.getElementById("pse-saves-grid"),i=n.save_proficiencies||{};s.innerHTML=_PSE_STATS.map(p=>`<label class="pse-save-cb"><input type="checkbox" id="pse-save-${p.toLowerCase()}"${i[p]?" checked":""}> ${p}</label>`).join("");const o=document.getElementById("pse-skills-grid"),r=n.skills||{},c=new Set((n.advantage_skills||[]).map(p=>p.toLowerCase()));o.innerHTML=_PSE_SKILL_NAMES.map(p=>{const m=_pseSkillId(p),f=_pseSkillAdvId(p),g=r[p]!==void 0?r[p]:"",h=c.has(p.toLowerCase());return`<div class="pse-skill-cell">
143      <span class="pse-skill-name">${_PSE_SKILL_ABBR[p]}</span>
144      <input type="number" id="${m}" class="pse-skill-input form-input" value="${g}" placeholder="\u2014" />
145      <label class="pse-skill-adv" title="Advantage on ${p}"><input type="checkbox" id="${f}"${h?" checked":""}> A</label>
146    </div>`}).join(""),document.getElementById("pse-resistances").value=(n.resistances||[]).join(", "),document.getElementById("pse-immunities").value=(n.immunities||[]).join(", "),document.getElementById("pse-vulnerabilities").value=(n.vulnerabilities||[]).join(", ");const l=n.senses||{};
146document.getElementById("pse-senses").value=Array.isArray(l)?l.join(", "):Object.entries(l).map(([p,m])=>m>0?`${p} ${m}`:p).join(", "),document.getElementById("pse-languages").value=(n.languages||[]).join(", "),document.getElementById("pse-armor-weapon-profs").value=(n.armor_weapon_profs||[]).join(", "),document.getElementById("pse-tool-profs").value=(n.tool_profs||[]).join(", "),document.getElementById("pse-feats").value=(n.feats||[]).join(", "),document.getElementById("pse-magic-items").value=(n.magic_items||[]).join(", ");const d=n.currencies||{};for(const p of["pp","gp","ep","sp","cp"])document.getElementById(`pse-${p}`).value=d[p]!==void 0?d[p]:"";document.getElementById("pse-ddb-url").value=t?.dnd_beyond_url||"",document.getElementById("pse-ddb-status").classList.add("hidden"),document.getElementById("pc-stats-edit-error").classList.add("hidden"),document.getElementById("pc-stats-edit-save-btn").disabled=!1,document.getElementById("pc-stats-edit-save-btn").textContent="Save Stats";const u=typeof _isGm=="function"&&_isGm()||!!(currentUser&&currentUser.is_admin);document.querySelector("#pc-stats-edit-modal .tf-danger-zone")?.classList.toggle("hidden",!u),document.getElementById("pc-stats-edit-modal").classList.remove("hidden")}document.getElementById("dhse-demiplane-btn")?.addEventListener("click",async()=>{const e=document.getElementById("dhse-demiplane-url"),t=document.getElementById("dhse-demiplane-status"),n=document.getElementById("dhse-demiplane-btn"),a=(e?.value||"").trim(),s=_editStatsEntityId,i=(o,r)=>{t&&(t.textContent=o,t.classList.remove("hidden"),t.classList.toggle("error",!!r))};if(!a){i("Paste the character's public Demiplane address first.",!0);return}if(!(!s||!currentCampaign)){n.disabled=!0,n.textContent="Importing\u2026";try{const o=d=>api("POST",`/api/campaigns/${currentCampaign.id}/entities/${s}/demiplane-import`,d?{url:a,confirm_name_mismatch:!0}:{url:a});let r;try{r=await o(!1)}catch(d){if(!d||d.status!==409)throw d;if(!await confirmDialog({title:"Different character",message:(d.message||"That link is for a different character.")+`
147
148This cannot be undone.`,confirmLabel:"Import anyway",danger:!0})){i("Import cancelled.");return}r=await o(!0)}allEntities=allEntities.map(d=>d.id===r.entity.id?r.entity:d),typeof _closeImportReview=="function"&&_closeImportReview(),_openEditStatsModal(s);const c=(r.filled||[]).filter(d=>d!=="demiplane_url").length,l=r.missing||[];i(l.length?`Imported ${c} field${c===1?"":"s"}. Still needed (not in the shared page): ${l.join(", ")}.`:`Imported ${c} field${c===1?"":"s"}.`)}catch(o){i(o&&o.message||"Import failed.",!0)}finally{n.disabled=!1,n.textContent="Import"}}}),document.getElementById("pc-stats-delete-btn")?.addEventListener("click",async()=>{const e=_editStatsEntityId;if(!e||!currentCampaign)return;const t=allEntities.find(i=>i.id===e),n=t?.name||"this character",a=`/api/campaigns/${currentCampaign.id}/entities/${e}`;let s={has_history:!!t?.last_seen_session_id,sessions:0};try{s=await api("GET",`${a}/usage`)}catch{}if(s.has_history){const i=s.sessions>0?`${n} appears in ${s.sessions} session${s.sessions===1?"":"s"} of the chronicle.`:`${n} has campaign history.`;if(await confirmDialog({title:`Delete ${n}?`,message:`${i} If they died or left the party, Mark Departed keeps their story in the campaign. Deleting erases ${n} and every reference to them, permanently.`,confirmLabel:"Mark Departed instead",cancelLabel:"Delete permanently\u2026",danger:!1})){try{const c=await api("PUT",a,{is_inactive:!0,is_dead:!1});allEntities=allEntities.map(l=>l.id===c.id?c:l),typeof _applyAutoFailedQuests=="function"&&_applyAutoFailedQuests(c),document.getElementById("pc-stats-edit-modal").classList.add("hidden"),showToast(`\u{1F44B} ${c.name} has departed.`)}catch(c){showToast("Failed: "+c.message)}return}if(!await confirmDialog({title:`Delete ${n} permanently?`,message:`This removes ${n} and all their data from the campaign. This cannot be undone.`,confirmLabel:"Delete permanently",cancelLabel:"Cancel",danger:!0}))return}else if(!await confirmDialog({title:`Delete ${n}?`,message:`This permanently removes ${n} and all their data. This cannot be undone.`,confirmLabel:"Delete permanently",cancelLabel:"Cancel",danger:!0}))return;try{await api("DELETE",a),allEntities=allEntities.filter(i=>i.id!==e),document.getElementById("pc-stats-edit-modal").classList.add("hidden"),showToast(`${n} deleted.`)}catch(i){showToast("Failed: "+i.message)}}),document.getElementById("pc-stats-edit-save-btn").addEventListener("click",async()=>{const e=document.getElementById("pc-stats-edit-save-btn"),t=document.getElementById("pc-stats-edit-error");e.disabled=!0,e.textContent="Saving\u2026",t.classList.add("hidden");const n=k=>{const C=parseInt(document.getElementById(k).value);return isNaN(C)?null:C},a=k=>document.getElementById(k).value.trim(),s=k=>{const C=a(k);return C?C.split(",").map(S=>
148S.trim()).filter(Boolean):[]},i={};if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()){Object.assign(i,_dhFormRead(k=>document.getElementById(`dhse-${k}`)));try{const k=await api("PATCH",`/api/campaigns/${currentCampaign.id}/entities/${_editStatsEntityId}/sheet-overrides`,i);allEntities=allEntities.map(C=>C.id===k.id?k:C),document.getElementById("pc-stats-edit-modal").classList.add("hidden"),typeof _codex2RerenderIfOpen=="function"&&_codex2RerenderIfOpen(),showToast(`\u270F\uFE0F Stats updated for ${k.name}`)}catch(k){t.textContent=k.message,t.classList.remove("hidden"),e.disabled=!1,e.textContent="Save Stats"}return}const o=n("pse-level");o!==null&&(i.level=o);const r=s("pse-classes");r.length&&(i.classes=r);const c=a("pse-species");c&&(i.species=c);const l=a("pse-background");l&&(i.background=l);const d=a("pse-hp");d&&(i.hp=d);const u=n("pse-ac");u!==null&&(i.ac=u);const p=n("pse-speed");p!==null&&(i.speed=p);const m=n("pse-initiative");m!==null&&(i.initiative=m);const f=n("pse-prof-bonus");f!==null&&(i.prof_bonus=f);const g={};for(const[k,C]of[["STR","pse-str"],["DEX","pse-dex"],["CON","pse-con"],["INT","pse-int"],["WIS","pse-wis"],["CHA","pse-cha"]]){const S=n(C);S!==null&&(g[k]={score:S,mod:Math.floor((S-10)/2)})}Object.keys(g).length&&(i.ability_scores=g);const h=n("pse-passive-perception");h!==null&&(i.passive_perception=h);const v=n("pse-passive-investigation");v!==null&&(i.passive_investigation=v);const b=n("pse-passive-insight");b!==null&&(i.passive_insight=b);const y={};for(const k of _PSE_STATS)document.getElementById(`pse-save-${k.toLowerCase()}`).checked&&(y[k]="proficiency");i.save_proficiencies=y;const _={},w=[];for(const k of _PSE_SKILL_NAMES){const C=n(_pseSkillId(k));C!==null&&(_[k]=C),document.getElementById(_pseSkillAdvId(k)).checked&&w.push(k)}Object.keys(_).length&&(i.skills=_),i.advantage_skills=w,i.resistances=s("pse-resistances"),i.immunities=s("pse-immunities"),i.vulnerabilities=s("pse-vulnerabilities");const $=a("pse-senses");if($){const k={};$.split(",").forEach(C=>{if(C=C.trim(),!C)return;const S=C.match(/^(.+?)\s+(\d+)\s*(?:ft\.?|feet?)?$/i);S?k[S[1].trim()]=parseInt(S[2]):k[C]=0}),i.senses=k}else i.senses={};i.languages=s("pse-languages"),i.armor_weapon_profs=s("pse-armor-weapon-profs"),i.tool_profs=s("pse-tool-profs"),i.feats=s("pse-feats"),i.magic_items=s("pse-magic-items");const E={};for(const k of["pp","gp","ep","sp","cp"]){const C=n(`pse-${k}`);C!==null&&(E[k]=C)}Object.keys(E).length&&(i.currencies=E);try{const k=document.getElementById("pse-ddb-url").value.trim()||null,C=allEntities.find(q=>q.id===_editStatsEntityId);if(k!==(C?.dnd_beyond_url||null)){const q=await api("PATCH",`/api/campaigns/${currentCampaign.id}/entities/${_editStatsEntityId}`,{dnd_beyond_url:k});allEntities=allEntities.map(I=>I.id===q.id?q:I)}const S=await api("PATCH",`/api/campaigns/${currentCampaign.id}/entities/${_editStatsEntityId}/sheet-overrides`,i);allEntities=allEntities.map(q=>q.id===S.id?S:q),document.getElementById("pc-stats-edit-modal").classList.add("hidden"),typeof _codex2RerenderIfOpen=="function"&&_codex2RerenderIfOpen();try{const q=document.getElementById("tab-party2");q&&!q.classList.contains("hidden")&&typeof renderParty2=="function"&&renderParty2()}catch{}showToast(`\u270F\uFE0F Stats updated for ${S.name}`)}catch(k){t.textContent=k.message,t.classList.remove("hidden"),e.disabled=!1,e.textContent="Save Stats"}});function renderPartyInventoryCard(){if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()||partyTreasury===null)return"";const e=partyTreasury,t=e.pp||0,n=e.gp||0,a=e.ep||0,s=e.sp||0,i=e.cp||0,o=Math.floor(t*10+n+s/10+i/100+a/2),r=b=>b.toLocaleString(),c=(b,y,_)=>b>0?`<span class="coin-group"><span class="coin-badge ${y}">${r(b)}</span><span class="coin-label">${_}</span></span>`:"",l=t||n||a||s||i,d=e.items||[],u=l?[c(t,"coin-pp","pp"),c(n,"coin-gp","gp"),c(a,"coin-ep","ep"),c(s,"coin-sp","sp"),c(i,"coin-cp","cp")].filter(Boolean).join("")+`<span class="coin-total">\u2248 <span class="coin-badge coin-gp">${r(o)}</span><span class="coin-label">gp</span></span>`:d.length?'<span class="placeholder">No coins on hand.</span>':'<span class="placeholder">Empty: use \u270F\uFE0F Edit to add coins &amp; items.</span>',p=d.length?d.map(b=>`<span class="party-trait-tag">${escHtml(b.quantity>1?`${b.name} \xD7${b.quantity}`:b.name)}</span>`).join(""):"",m=PARTY_TREASURY_IMAGE_URL?`<img src="${PARTY_TREASURY_IMAGE_URL}" alt="Party Treasury" style="width:100%;height:100%;object-fit:cover;border-radius:var(--radius-lg)">`:'<div class="party-pc-portrait-placeholder" style="font-size:3rem;color:var(--color-gold)">\u{1F4B0}</div>',f='<button class="btn-ghost btn-xs" id="open-treasury-edit-btn">\u270F\uFE0F Edit</button>',g='<button class="btn-ghost btn-xs" id="open-loot-splitter-btn">\u2694 Split</button>',h=e.log||[],v=h.length?`<button class="btn-ghost btn-xs" id="open-treasury-log-btn">\u{1F5D2}
148 Log (${h.length})</button>`:"";return`<span class="form-section-label party-section-label">\u{1F4B0} ${escHtml(_partyLabel())} Treasury</span>
149  <div class="tf-card tf-card--md party-pc-card party-treasury-card">
150    <div class="party-pc-portrait party-treasury-portrait">${m}</div>
151    <div class="party-pc-stats">
152      <div class="party-pc-name-row" style="margin-bottom:0.3rem;justify-content:flex-end">
153        <div class="party-pc-actions">${f}${g}${v}</div>
154      </div>
155      <div class="party-currency-row">${u}</div>
156      ${p?`<div class="pill-section-gap"></div>
157      <div class="ddb-row party-tags-row"><span class="ddb-row-label">Inventory</span><span class="party-tags-wrap">${p}</span></div>`:""}
158    </div>
159  </div>`}function _classifyTallyRow(e){const t=(e&&e.name||"").trim();if(!t)return"factions";const n=t.toLowerCase(),a=(allEntities||[]).find(i=>(i.name||"").toLowerCase()===n?!0:(i.aliases||[]).some(r=>String(r).toLowerCase()===n));if(a)return a.type==="pc"?"pcs":a.type==="item"||a.type==="location"?"items":a.type==="npc"?"npcs":(a.type==="faction","factions");if(/,| - /.test(t))return"npcs";const s=t.split(/\s+/).pop().toLowerCase();return/(?:ss|us|is|oose|eese)$/.test(s)?"npcs":/s$/.test(s)?"factions":"npcs"}function _openTreasuryLogModal(){const e=partyTreasury;if(!e)return;const t=document.getElementById("treasury-log-modal-body"),n=e.log||[];if(!n.length){showToast("No transactions yet.");return}t.innerHTML=n.map(a=>`
160    <div class="treasury-log-entry">
161      <span class="treasury-log-actor">${escHtml(a.actor_name)}</span>
162      <span class="treasury-log-summary">${escHtml(a.summary)}</span>
163      <span class="treasury-log-time">${_fmtRelTime(a.logged_at)}</span>
164    </div>`).join(""),document.getElementById("treasury-log-modal").classList.remove("hidden")}let _lootSplitResult=null;function _openLootSplitter(){const e=partyTreasury||{pp:0,gp:0,sp:0,cp:0};document.getElementById("ls-pp").value=e.pp||0,document.getElementById("ls-gp").value=e.gp||0,document.getElementById("ls-sp").value=e.sp||0,document.getElementById("ls-cp").value=e.cp||0;const t=allEntities.filter(n=>n.type==="pc"&&!n.is_dead&&!n.is_inactive).length;document.getElementById("ls-players").value=t||1,document.getElementById("ls-result").classList.add("hidden"),document.getElementById("ls-apply-btn").classList.add("hidden"),_lootSplitResult=null,document.getElementById("loot-splitter-modal").classList.remove("hidden")}function _lootSplitMath(e,t,n,a,s){const i=Math.floor(e/s);let o=e%s,r=o>0?(s-o)*10:0,c=0;r>t&&(c=o,o=0,r=0);const l=t-r,d=Math.floor(l/s),u=l%s;return{pp:e,gp:t,sp:n,cp:a,N:s,basePp:i,remPp:o,baseGp:d,makeupGp:r,remGp:u,leftoverPp:c}}function _calcLootSplit(){const e=Math.max(0,parseInt(document.getElementById("ls-pp").value)||0),t=Math.max(0,parseInt(document.getElementById("ls-gp").value)||0),n=Math.max(0,parseInt(document.getElementById("ls-sp").value)||0),a=Math.max(0,parseInt(document.getElementById("ls-cp").value)||0),s=Math.max(1,parseInt(document.getElementById("ls-players").value)||1),{basePp:i,remPp:o,baseGp:r,remGp:c,leftoverPp:l}=_lootSplitMath(e,t,n,a,s),d=o,u=s-o;_lootSplitResult={pp:e,gp:t,sp:n,cp:a,N:s,basePp:i,remPp:o,baseGp:r,remGp:c,leftoverPp:l};const p=b=>b.toLocaleString(),m=(b,y)=>{const _=[];return b&&_.push(`<span class="coin-group"><span class="coin-badge coin-pp">${p(b)}</span><span class="coin-label">pp</span></span>`),y&&_.push(`<span class="coin-group"><span class="coin-badge coin-gp">${p(y)}</span><span class="coin-label">gp</span></span>`),_.length?_.join(""):'<span style="color:var(--text-3)">\u2014</span>'},f=()=>{const b=[];return l&&b.push(`<span class="coin-group"><span class="coin-badge coin-pp">${p(l)}</span><span class="coin-label">pp</span></span>`),c&&b.push(`<span class="coin-group"><span class="coin-badge coin-gp">${p(c)}</span><span class="coin-label">gp</span></span>`),n&&b.push(`<span class="coin-group"><span class="coin-badge coin-sp">${p(n)}</span><span class="coin-label">sp</span></span>`),a&&b.push(`<span class="coin-group"><span class="coin-badge coin-cp">${p(a)}</span><span class="coin-label">cp</span></span>`),b.length?b.join(""):'<span style="color:var(--text-3)">nothing</span>'},g=(b,y,_)=>`<div class="ls-row">
165      <span class="ls-count">\u{1F464} ${p(b)} player${b!==1?"s":""}</span>
166      <span class="ls-coins">${m(y,_)}</span>
167    </div>`;let h="";d>0&&u>0?h=g(d,i+1,r)+g(u,i,r+10):h=g(s,i+(o>0?1:0),r);const v=document.getElementById("ls-result");v.innerHTML=`
168    <div class="ls-section-label form-section-label">Per Player</div>
169    ${h}
170    <div class="ls-section-label form-section-label" style="margin-top:0.75rem">\u{1F4B0} Treasury Keeps</div>
171    <div class="ls-row ls-row--treasury"><span class="ls-coins">${f()}</span></div>`,v.classList.remove("hidden"),document.getElementById("ls-apply-btn").classList.remove("hidden")}async function _applyLootSplit(){if(!_lootSplitResult||!partyTreasury)return;const{remGp:e,sp:t,cp:n,leftoverPp:a}=_lootSplitResult,s=document.getElementById("ls-apply-btn");s.disabled=!0,s.textContent="Applying\u2026";try{const i={pp:a||0,gp:e,ep:partyTreasury.ep||0,sp:t,cp:n,items:partyTreasury.items||[]};partyTreasury=await api("PUT",`/api/campaigns/${currentCampaign.id}/party-treasury`,i),document.getElementById("loot-splitter-modal").classList.add("hidden"),typeof renderLivePcSidebar=="function"&&livePcSidebarPcs.length>0&&renderLivePcSidebar(),showToast("\u{1F4B0} Treasury updated: loot distributed!")}catch(i){showToast("Failed to apply: "+i.message)}finally{s.disabled=!1,s.textContent="Save Treasury's Share"}}let _bursarCid=null,_bursarWired=!1;function _bursarMinKey(e){return`tf_live_bursar_min_${e}`}function _renderLiveBursar(){const e=document.getElementById("live-bursar-body"),t=currentCampaign&&currentCampaign.id,n=document.getElementById("live-bursar");if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()){n?.classList.add("hidden"),e&&(e.innerHTML=""),_bursarCid=null;return}if(n?.classList.remove("hidden"),!e||t==null||_bursarCid===t)return;_bursarCid=t;let a=!1;try{a=localStorage.getItem(_bursarMinKey(t))==="1"}catch{}const s=(allEntities||[]).filter(r=>r.type==="pc"&&!r.is_dead&&!r.is_inactive).length||1,i=r=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${r?"M6 15l6-6 6 6":"M6 9l6 6 6-6"}"/></svg>`;e.innerHTML=`
172    <div class="bursar-coin-grid">
173      ${["pp","gp","sp","cp"].map(r=>`<label class="ls-coin-label"><span class="coin-badge coin-${r} ls-coin-badge">${r.toUpperCase()}</span><input id="bursar-${r}" type="number" min="0" inputmode="numeric" class="bursar-coin-input" value="0" aria-label="${r==="pp"?"Platinum":r==="gp"?"Gold":r==="sp"?"Silver":"Copper"} pieces"></label>`).join("")}
174    </div>
175    <div class="bursar-players-row">
176      <span class="bursar-players-lbl pc-stats-field-lbl">Players</span>
177      <span class="lv-pass-dc-field"><input type="number" inputmode="numeric" id="bursar-players" class="lv-pass-dc-input" value="${s}" min="1" max="20" aria-label="Number of players"><span class="lv-pass-dc-steppers"><button type="button" class="lv-pass-dc-step" data-step="1" aria-label="More players">${i(!0)}</button><button type="button" class="lv-pass-dc-step" data-step="-1" aria-label="Fewer players">${i(!1)}</button></span></span>
178      <button type="button" id="bursar-calc-btn" class="btn-new btn-xs bursar-calc-btn">Split</button>
179    </div>
180    <div id="bursar-result" class="loot-split-result bursar-result hidden"></div>`,e.classList.toggle("hidden",a);const o=document.getElementById("live-bursar-collapse-btn");o&&(o.textContent=a?"\u25B8":"\u25BE"),e.querySelectorAll(".lv-pass-dc-step").forEach(r=>r.addEventListener("click",()=>{const c=document.getElementById("bursar-players"),l=parseInt(c.value,10);c.value=String(Math.min(20,Math.max(1,(isNaN(l)?s:l)+parseInt(r.dataset.step,10))))})),document.getElementById("bursar-calc-btn")?.addEventListener("click",_bursarCalc),e.querySelectorAll("input").forEach(r=>r.addEventListener("keydown",c=>{c.key==="Enter"&&(c.preventDefault(),_bursarCalc())})),_bursarWired||(_bursarWired=!0,o?.addEventListener("click",()=>{const r=document.getElementById("live-bursar-body"),c=!r.classList.contains("hidden");r.classList.toggle("hidden",c);const l=document.getElementById("live-bursar-collapse-btn");if(l&&(l.textContent=c?"\u25B8":"\u25BE"),_bursarCid!=null)try{localStorage.setItem(_bursarMinKey(_bursarCid),c?"1":"0")}catch{}}))}function _bursarCalc(){const e=m=>Math.max(0,parseInt(document.getElementById(m)?.value,10)||0),t=e("bursar-pp"),n=e("bursar-gp"),a=e("bursar-sp"),s=e("bursar-cp"),i=Math.max(1,Math.min(20,parseInt(document.getElementById("bursar-players")?.value,10)||1)),o=_lootSplitMath(t,n,a,s,i),r=m=>
180m.toLocaleString(),c=m=>{const f=m.filter(([g])=>g).map(([g,h])=>`<span class="coin-group"><span class="coin-badge coin-${h}">${r(g)}</span><span class="coin-label">${h}</span></span>`);return f.length?f.join(""):'<span class="bursar-none">\u2014</span>'},l=(m,f)=>`<div class="ls-row"><span class="bursar-row-lbl">${m}</span><span class="ls-coins">${c(f)}</span></div>`;let d="";o.remPp>0?d=l(`${r(o.remPp)} player${o.remPp!==1?"s":""}`,[[o.basePp+1,"pp"],[o.baseGp,"gp"]])+l(`${r(i-o.remPp)} player${i-o.remPp!==1?"s":""}`,[[o.basePp,"pp"],[o.baseGp+10,"gp"]]):d=l(`Each of ${r(i)}`,[[o.basePp,"pp"],[o.baseGp,"gp"]]);const u=`<div class="ls-row"><span class="ls-coins">${c([[o.leftoverPp,"pp"],[o.remGp,"gp"],[a,"sp"],[s,"cp"]])}</span></div>`,p=document.getElementById("bursar-result");p&&(p.innerHTML=`<div class="ls-section-label form-section-label">Per player</div>${d}
181    <div class="ls-section-label form-section-label bursar-leftover-lbl">Left over: undivided</div>${u}`,p.classList.remove("hidden"))}async function _refreshPartyData(){if(!currentCampaign){partySpellUsage={},partyBestQuotes={},campaignTally=null,_rosterPcs=[];return}const e=currentCampaign.id;_refreshSpellUsage();const[t,n,a]=await Promise.allSettled([api("GET",`/api/campaigns/${e}/party-quotes`),api("GET",`/api/campaigns/${e}/campaign-tally`),api("GET",`/api/campaigns/${e}/roster`)]);_campaignStale(e)||(partyBestQuotes=t.status==="fulfilled"?t.value||{}:{},campaignTally=n.status==="fulfilled"?n.value??null:null,_rosterPcs=a.status==="fulfilled"?a.value||[]:[])}async function _refreshPartyQuotes(){if(!currentCampaign){partyBestQuotes={};return}try{partyBestQuotes=await api("GET",`/api/campaigns/${currentCampaign.id}/party-quotes`)||{}}catch{partyBestQuotes={}}}async function _refreshCampaignTally(){if(!currentCampaign){campaignTally=null;return}try{campaignTally=await api("GET",`/api/campaigns/${currentCampaign.id}/campaign-tally`)}catch{campaignTally=null}}async function _refreshSpellUsage(){if(!currentCampaign){partySpellUsage={};return}const e=currentCampaign.id;try{const t=await api("GET",`/api/campaigns/${e}/spell-usage`)||{};if(_campaignStale(e))return;partySpellUsage=t}catch{if(_campaignStale(e))return;partySpellUsage={}}}async function renderQuotesPanel(e){e.innerHTML='<p class="placeholder placeholder--padded">Loading quotes\u2026</p>';try{const S=await api("GET",`/api/campaigns/${currentCampaign.id}/party-quotes`);S&&(partyBestQuotes=S)}catch{e.innerHTML='<p class="placeholder placeholder--padded">Could not load quotes.</p>';return}const t=Object.entries(partyBestQuotes);if(!t.length){e.innerHTML='<p class="placeholder placeholder--padded">No quotes yet. Generate session recaps to extract memorable quotes, or click + Add Quote.</p>';return}const n=_isGm(),a=!!currentUser?.is_admin,s=async()=>{await _refreshPartyQuotes(),renderQuotesPanel(e)},i=partyBestQuotes.party||[],o=t.filter(([S])=>S!=="party").map(([S,q])=>({entity:allEntities.find(I=>I.id===parseInt(S)),quotes:q,eid:parseInt(S)})).filter(S=>S.entity),r=_splitPcsByLifecycle(),c=new Set(r.active.map(S=>S.id)),l=new Set(r.fallenDeparted.map(S=>S.id)),d=(S,q)=>S.entity.name.localeCompare(q.entity.name),u=o.filter(S=>c.has(S.entity.id)).sort(d),p=o.filter(S=>l.has(S.entity.id)).sort(d),m=o.filter(S=>S.entity.type==="companion");m.forEach(S=>{const q=S.entity.parent_entity_id?allEntities.find(I=>I.id===S.entity.parent_entity_id):null;S._parent=q,S._parentStatus=q?q.is_dead||q.is_inactive?"fallen":"active":"orphan"});const f=m.filter(S=>S._parentStatus==="active").sort(d),g=m.filter(S=>S._parentStatus==="fallen").sort(d),h=m.filter(S=>S._parentStatus==="orphan").sort(d);function v(S,q){const I=[],T=new Set;return S.forEach(P=>{I.push(P),q.forEach(B=>{B._parent&&B._parent.id===P.eid&&(I.push(B),T.add(B.eid))})}),q.forEach(P=>{T.has(P.eid)||I.push(P)}),I}const b=v(u,f),y=v(p,g),_=o.filter(S=>S.entity.type!=="pc"&&S.entity.type!=="companion").concat(h).sort(d),w=(S,q,I=!1)=>`
182    <div class="quotes-item pc-quote-row" data-quote-id="${S.id}">
183      <div class="quotes-item-text"><span>&ldquo;<em>${linkifyText(escHtml(S.quote),allEntities,null)}</em>&rdquo;</span></div>
184      ${S.context?`<div class="quotes-item-ctx">${escHtml(S.context)}</div>`:""}
185      ${S.session_title?`<div class="quotes-item-session">\u2014 ${escHtml(S.session_title)}</div>`:""}
186      ${S.id?_qcShareBtn(`"${(S.quote||"").trim()}" \u2014 ${S.speaker_name||""}${S.context?" ("+S.context+")":""}`):""}
187      ${(n||a)&&S.id?`<span class="pc-quote-actions">
188        ${n?`
189        <button class="btn-ghost pc-quote-edit-btn" data-quote-id="${S.id}
189" data-quote="${escHtml(S.quote)}" data-context="${escHtml(S.context||"")}" data-session-id="${S.session_id!=null?S.session_id:""}" title="Edit quote">\u270E</button>
190        <button class="btn-ghost pc-quote-reassign-btn" data-quote-id="${S.id}" title="Reassign quote">\u2197</button>
191        <button class="btn-ghost pc-quote-delete-btn" data-quote-id="${S.id}" title="Delete quote">\u2715</button>`:""}
192        ${S.session_id!=null?`
193        <button class="btn-ghost pc-quote-qotn-btn${_qcQotnQuoteIds.has(String(S.id))?" is-on":""}" data-quote-id="${S.id}" title="${_qcQotnQuoteIds.has(String(S.id))?"Remove Quote of the Night award":"Award Quote of the Night"}"><svg class="pc-quote-qotn-glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6c3.6 0 6.6 0.9 7.9 1.9v6c0 5.9-3.2 10.3-7.9 12.9-4.7-2.6-7.9-7-7.9-12.9v-6C5.4 3.5 8.4 2.6 12 2.6z" fill="none" stroke="currentColor" stroke-width="1.7"/><text x="12" y="15" text-anchor="middle" style="font:700 11px Georgia,serif; fill:currentColor;">&#8220;</text></svg></button>`:""}
194      </span>`:""}
195    </div>`,$=({entity:S,quotes:q,eid:I},T=!1)=>{const P=T?{icon:"\u2694",color:"var(--accent-teal)",label:_partyLabel()}:ENTITY_CFG[S.type]||ENTITY_CFG.npc,B=expandedEntityId===I,H=T?_partyLabel():S.name;return`
196      <div class="tf-card tf-card--sm tf-card--watermarked entity-row${B?" expanded":""}" data-id="${I}" data-quotes-row="1">
197        <div class="entity-row-hdr">
198          <div class="entity-icon entity-icon-tinted" style="--entity-color:${P.color}">${T?P.icon:entityGlyphRef(S.type,"")}</div>
199          <div class="entity-meta">
200            <span class="entity-name">${escHtml(H)}</span>
201            <span class="entity-badge entity-badge-tinted" style="--entity-color:${P.color}">${P.label}</span>
202          </div>
203          <span class="quotes-count-badge">${q.length}</span>
204          <span class="entity-chevron">${B?"\u25B2":"\u25BC"}</span>
205        </div>
206        ${B?`
207        <div class="entity-detail">
208          <div class="quotes-list">${q.map(A=>w(A,T?null:I,T)).join("")}</div>
209        </div>`:""}
210      </div>`},E=i.length?$({entity:null,quotes:i,eid:"party"},!0):"",k=S=>`<span class="form-section-label quotes-group-hdr">${S}</span>`,C=[];(b.length||i.length)&&(C.push(k("Active Party")),C.push(E),C.push(b.map(S=>$(S)).join(""))),y.length&&(C.push(k("Fallen and Departed Party")),C.push(`<div class="quotes-fallen-group">${y.map(S=>$(S)).join("")}</div>`)),_.length&&(C.push(k("NPCs")),C.push(_.map(S=>
210$(S)).join(""))),e.innerHTML=`<div class="quotes-panel">${C.join("")}</div>`,typeof _qcApplyCardBadges=="function"&&_qcApplyCardBadges(),e.querySelectorAll(".entity-row[data-quotes-row]").forEach(S=>{const q=S.querySelector(".entity-row-hdr");q&&q.addEventListener("click",async I=>{if(I.target.closest(".pc-quote-actions, .pc-quote-edit-form, .pc-quote-reassign-picker"))return;const T=S.dataset.id,P=T==="party"?"party":parseInt(T),B=expandedEntityId!==P;if(expandedEntityId=B?P:null,await renderQuotesPanel(e),B){const H=`.entity-row[data-quotes-row][data-id="${CSS.escape(String(P))}"]`,A=e.querySelector(H);A&&A.scrollIntoView({behavior:"smooth",block:"start"})}})}),(n||a)&&e.querySelectorAll(".entity-row[data-quotes-row].expanded").forEach(S=>{const q=S.dataset.id,I=q==="party"?null:parseInt(q);_rewireQuoteButtons(S,I,s)})}const DISCORD_GLYPH='<svg class="tf-discord-mark" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.369A19.79 19.79 0 0 0 15.432 3a.074.074 0 0 0-.078.037c-.21.375-.444.864-.608 1.249a18.27 18.27 0 0 0-5.487 0 12.6 12.6 0 0 0-.617-1.25.077.077 0 0 0-.078-.036A19.74 19.74 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.1 13.1 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.009c.12.099.246.198.373.292a.077.077 0 0 1-.006.127 12.3 12.3 0 0 1-1.873.891.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.331c-1.182 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>';function _renderPendingRevealSection(){if(!_isGm())return"";const e=(allEntities||[]).filter(o=>o&&o.pending_reveal);if(!e.length)return"";e.sort((o,r)=>{const c=o.created_at?new Date(o.created_at).getTime():o.id||0;return(r.created_at?new Date(r.created_at).getTime():r.id||0)-c});const t={npc:"NPC",location:"Location",item:"Item",faction:"Faction",pc:"PC",deity:"Deity"},n=_discordRevealReady(),a=e.map(o=>{const r=_entityImageUrl(o),c=thumbUrl(r),l=t[o.type]||(o.type||"").toUpperCase(),d=c?`<img class="pending-reveal-thumb" src="${escHtml(c)}" alt="">`:'<div class="pending-reveal-thumb pending-reveal-thumb--empty" aria-hidden="true">\u2014</div>',u=_tfRevealCastHtml(o,n);return`
211      <div class="pending-reveal-card" data-pending-id="${o.id}">
212        ${d}
213        <div class="pending-reveal-body">
214          <div class="pending-reveal-type">${escHtml(l)}</div>
215          <div class="pending-reveal-name">${escHtml(o.name)}</div>
216          <button type="button" class="pending-reveal-tease" data-tease-edit="${o.id}" title="Choose what players see of this entry before you reveal it">${_teaseParts(o).length?escHtml("Players see: "+_teaseParts(o).join(", ")):"Players see nothing yet"}<span class="pending-reveal-tease-cta">Edit what players see</span></button>
217        </div>
218        ${u}
219        <button type="button" class="pending-reveal-btn" data-pending-reveal-id="${o.id}">Reveal</button>
220      </div>
221    `}).join(""),s=n?"Announce every reveal in this batch on Discord (off by default, avoids channel spam)":"Link Discord (reveal, announce, or recap channel) to announce reveals";return`
222    <div class="pending-reveal-section">
223      <div class="pending-reveal-hdr">
224        <span class="form-section-label pending-reveal-eyebrow">Unrevealed: only you can see these</span>
225        ${e.length>=2?`<div class="pending-reveal-all-cluster">
226        <label class="pending-reveal-cast pending-reveal-cast--batch${n?"":" is-disabled"}" title="${escHtml(s)}">
227          <input type="checkbox" class="pending-reveal-cast-chk" id="pending-reveal-cast-all"
228                 aria-label="Announce this batch on Discord"${n?"":" disabled"}>
229          <span class="pending-reveal-cast-ico" aria-hidden="true">${DISCORD_GLYPH}</span>
230          <span class="pending-reveal-cast-txt">Announce on Discord</span>
231        </label>
232        <button type="button" class="pending-reveal-all-btn">Reveal all ${e.length}</button>
233      </div>`:""}
234      </div>
235      <div class="pending-reveal-grid">${a}</div>
236    </div>
237  `}function _wirePendingRevealButtons(e){if(!e)return;const t=e.querySelector(".pending-reveal-all-btn");t&&t.addEventListener("click",async n=>{n.stopPropagation();const a=(allEntities||[]).filter(r=>r&&r.pending_reveal).map(r=>r.id);if(!a.length||!currentCampaign||!currentCampaign.id)return;const s=e.querySelector("#pending-reveal-cast-all"),i=!!(s&&s.checked&&!s.disabled);if(await confirmDialog({title:`Reveal all ${a.length} entries?`,message:"Players will immediately see all of these in the Codex. This cannot be undone.",confirmLabel:"Reveal all",cancelLabel:"Cancel"})){t.disabled=!0,t.textContent="Revealing\u2026";for(const r of a)try{const c=await api("POST",`/api/campaigns/${currentCampaign.id}/entities/${r}/reveal`,{broadcast:i}),l=allEntities.findIndex(d=>d.id===r);l!==-1&&(allEntities[l]={...allEntities[l],...c,pending_reveal:!1})}catch{}typeof showToast=="function"&&showToast(`Revealed ${a.length} to players${i?", announced on Discord":""}.`),renderCompendium()}}),e.querySelectorAll(".pending-reveal-card").forEach(n=>{n.setAttribute("role","button"),n.setAttribute("tabindex","0"),n.setAttribute("title","Open this entry");const a=()=>{const s=parseInt(n.getAttribute("data-pending-id"));s&&navigateToEntity(s)};n.addEventListener("click",s=>{s.target.closest("button")||s.target.closest(".pending-reveal-cast")||a()}),n.addEventListener("keydown",s=>{(s.key==="Enter"||s.key===" ")&&!s.target.closest("button")&&!s.target.closest(".pending-reveal-cast")&&(s.preventDefault(),a())})}),e.querySelectorAll("[data-tease-edit]").forEach(n=>n.addEventListener("click",a=>{a.stopPropagation();const s=parseInt(n.dataset.teaseEdit,10);s&&(navigateToEntity(s),setTimeout(()=>{const i=document.querySelector(`.cx2-tease-row[data-tease-for="${s}"]`);if(!i)return;i.scrollIntoView({block:"center"}),i.classList.add("is-flash"),setTimeout(()=>i.classList.remove("is-flash"),1800);const o=i.querySelector("input");o&&o.focus({preventScroll:!0})},900))})),e.querySelectorAll("[data-pending-reveal-id]").forEach(n=>{n.addEventListener("click",async a=>{a.stopPropagation();const s=parseInt(n.getAttribute("data-pending-reveal-id"));if(!s||!currentCampaign||!currentCampaign.id)return;const i=n.closest(".pending-reveal-card")?.querySelector(".pending-reveal-cast-chk"),o=!!(i&&i.checked&&!i.disabled);n.disabled=!0;const r=n.textContent;n.textContent="Revealing\u2026";try{await _tfRevealEntity(s,o)}catch(c){typeof showToast=="function"&&showToast("Reveal failed: "+(c.message||"Unknown")),n.disabled=!1,n.textContent=r}})})}function _tfRevealCastHtml(e,t){const n=t&&!e.revealed_at;return`
238        <label class="pending-reveal-cast${t?"":" is-disabled"}" title="${escHtml(t?"Announce this reveal in your Discord reveal channel":"Link Discord (reveal, announce, or recap channel) to announce reveals")}">
239          <input type="checkbox" class="pending-reveal-cast-chk" data-cast-for="${e.id}"
240                 aria-label="Announce this reveal on Discord"${n?" checked":""}${t?"":" disabled"}>
241          <span class="pending-reveal-cast-ico" aria-hidden="true">${DISCORD_GLYPH}</span>
242        </label>`}async function _tfRevealEntity(e,t){const n=currentCampaign&&currentCampaign.id,a=await api("POST",`/api/campaigns/${n}/entities/${e}/reveal`,{broadcast:!!t});if(_campaignStale(n))return a;const s=allEntities.findIndex(i=>i.id===e);return s!==-1&&(allEntities[s]={...allEntities[s],...a,pending_reveal:!1}),typeof showToast=="function"&&showToast(t?"Revealed to players, announced on Discord.":"Revealed to players."),renderCompendium(),a}function _cx2CodexTitle(){return currentCampaign&&(currentCampaign.codex_title||"").trim()||"The Codex"}function _cx2DefaultCodexLead(){return`Every soul, place, faction, and relic ${_at2PartyProse()} has met on the long road, catalogued as it was found.`}function _cx2RenderGlossaryFrontispiece(){const e=document.getElementById("codex-frontispiece");if(!e)return;if(!currentCampaign){e.innerHTML="";return}const t=escHtml(currentCampaign.name||"Campaign"),n=typeof _isGm=="function"&&_isGm(),a=v=>(v||"").trim().toLowerCase().replace(/\s+/g," "),s=escHtml(_cx2CodexTitle()),i=a(_cx2CodexTitle())===a("The Codex")?"Codex":`${t} \xB7 Codex`,o=escHtml((currentCampaign.codex_lead||"").trim()||_cx2DefaultCodexLead()),r=(allEntities||[]).filter(v=>v&&v.type!=="quote"&&!v.pending_reveal),c=r.filter(v=>["pc","npc","companion","deity"].includes(v.type)).length,l=v=>r.filter(b=>b.type===v).length,d=l("location"),u=l("faction"),p=l("item"),m=[`<span><b>${r.length}</b> ${r.length===1?"entry":"entries"}</span>`,c?`<span class="dot"></span><span><b>${c}</b> soul${c!==1?"s":""}</span>`:"",d?`<span class="dot"></span><span><b>${d}</b> place${d!==1?"s":""}</span>`:"",u?`<span class="dot"></span><span><b>${u}</b> faction${u!==1?"s":""}</span>`:"",p?`<span class="dot"></span><span><b>${p}</b> relic${p!==1?"s":""}</span>
242`:""].filter(Boolean).join(""),f=n?'<button class="chr2-edit-pen" data-cxgloss-edit="title" title="Edit title" aria-label="Edit codex title">\u270E</button>':"",g=n?'<button class="chr2-edit-pen" data-cxgloss-edit="lead" title="Edit lead" aria-label="Edit codex lead">\u270E</button>':"";e.innerHTML=`<header class="index-head codex-fp">
243    <div class="index-eyebrow">${i}</div>
244    <div class="chr2-field-row chr2-field-title"><h1 class="index-title" id="cxgloss-title-el">${s}</h1>${f}</div>
245    <div class="chr2-field-row chr2-field-lead"><p class="index-lead" id="cxgloss-lead-el">${o}</p>${g}</div>
246    <div class="index-stats">${m}</div>
247    <div class="ornament index-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div>
248  </header>`,n&&(e.querySelector('[data-cxgloss-edit="title"]')?.addEventListener("click",()=>_cxGlossEditField("title")),e.querySelector('[data-cxgloss-edit="lead"]')?.addEventListener("click",()=>_cxGlossEditField("lead")));const h=document.getElementById("set-record-btn");h&&(h.classList.toggle("hidden",!n),h.onclick=()=>{try{openForTheRecord({})}catch{}}),_cx2RenderCodexHero()}const _CODEX_DEFAULT_HERO="/static/codex-default-hero.jpg?v=1492";function _cx2RenderCodexHero(){const e=document.getElementById("codex-hero");if(!e)return;const t=document.getElementById("codex-hero-art"),n=(currentCampaign&&currentCampaign.codex_image_url||"").trim()||_CODEX_DEFAULT_HERO;t&&(t.style.backgroundImage=`url('${encodeURI(n)}')`),e.classList.remove("hidden");const a=document.getElementById("codex-hero-edit");if(a){const s=typeof _isGm=="function"&&_isGm();a.classList.toggle("hidden",!s),a.onclick=s?_cxGlossPickHeroImage:null}}function _cxGlossPickHeroImage(){typeof _campLibOpen=="function"&&_campLibOpen({target:"codex",title:"Codex illustration",subtitle:"Pick an image for the Codex frontispiece. It joins your campaign image library.",onPick:e=>{currentCampaign&&e&&e.url&&(currentCampaign.codex_image_url=e.url),_cx2RenderCodexHero()}})}function _cxGlossEditField(e){const t=e==="title",n=document.getElementById(t?"cxgloss-title-el":"cxgloss-lead-el");if(!n||n.dataset.editing==="1")return;n.dataset.editing="1";const a=n.closest(".chr2-field-row");a&&a.classList.add("editing");const s=t?"codex_title":"codex_lead",i=n.innerHTML,o=()=>{n.innerHTML=i,n.dataset.editing="",a&&a.classList.remove("editing")},r=document.createElement(t?"input":"textarea");r.className=t?"chr2-inline-input chr2-title-input":"chr2-inline-textarea",r.value=(currentCampaign&&currentCampaign[s]||"").trim(),t||(r.rows=3);const c=document.createElement("div");c.className="chr2-editor-actions",c.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const l=document.createElement("div");l.className="chr2-field-editor",l.appendChild(r),l.appendChild(c),n.innerHTML="",n.appendChild(l),r.focus(),t&&r.select(),c.querySelector(".chr2-ed-cancel").addEventListener("click",o),c.querySelector(".chr2-ed-save").addEventListener("click",async()=>{let d=r.value.trim();t||(d=d.replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim()),await _chr2SaveCampaignField(s,d)&&(n.dataset.editing="",a&&a.classList.remove("editing"),_cx2RenderGlossaryFrontispiece())}),r.addEventListener("keydown",d=>{d.key==="Escape"?(d.preventDefault(),o()):d.key==="Enter"&&(t||d.ctrlKey||d.metaKey)&&(d.preventDefault(),c.querySelector(".chr2-ed-save").click())})}function renderCompendium(){_cx2RenderGlossaryFrontispiece(),typeof _ensureSearchClears=="function"&&_ensureSearchClears(),_codexModeAuto&&(codexMode=liveSession?"live":"archive");const e=document.getElementById("codex-mode-btn");e&&(e.textContent=_CODEX_MODE_LABELS[codexMode]||_CODEX_MODE_LABELS.archive,e.classList.toggle("active",codexMode!=="archive"),e.classList.toggle("hidden",(allEntities||[]).length===0));const t=document.getElementById("entity-list"),n=compendiumTypeFilter==="quotes";if(document.getElementById("add-entity-btn").classList.toggle("hidden",n),document.getElementById("add-quote-btn").classList.toggle("hidden",!n||!_isGm()),n){renderQuotesPanel(t);return}const a=filteredEntities(),s=_renderPendingRevealSection();if(a.length===0){const m=(allEntities||[]).length===0?`<div class="codex-empty-first">
249           <p class="placeholder placeholder--padded">Your Codex is empty. It fills itself from your sessions.</p>
250           <button type="button" class="tf2-onboard-pill codex-empty-cta">Start your first session \u2192</button>
251         </div>`:'<p class="placeholder placeholder--padded">No entries match. Try a different filter, or add one with <strong>\uFF0B Add</strong>.</p>';t.innerHTML='<div id="qc-pending-art-mount"></div>'+s+m;const f=t.querySelector(".codex-empty-cta");f&&f.addEventListener("click",()=>document.getElementById("tab-btn-sessions")?.click()),_wirePendingRevealButtons(t),_q
251cRefreshPendingArt();return}const i=allSessions.filter(p=>!p.is_session_zero),o=i.length,r={};allSessions.forEach(p=>{if(p.is_session_zero){r[p.id]=0;return}const m=i.findIndex(f=>f.id===p.id);r[p.id]=o-m});let c,l;if(compendiumSort==="session"){const p={};a.forEach(m=>{const f=m.last_seen_session_id,g=f!=null?r[f]??f:-1,h=f!=null?allSessions.find(y=>y.id===f):null,v=h?h.is_session_zero?"Session 0: "+(h.title||"Session Zero"):`Session ${g}: ${h.title||""}`.trimEnd():"Unseen",b=f??"__unseen__";p[b]||(p[b]={label:v,sNum:g,quests:[]}),p[b].quests.push(m)}),c=Object.keys(p).sort((m,f)=>{const g=p[m].sNum,h=p[f].sNum;return m==="__unseen__"?1:f==="__unseen__"?-1:h-g}),l=Object.fromEntries(c.map(m=>[m,p[m].quests])),renderCompendium._sessionLabels=Object.fromEntries(c.map(m=>[m,p[m].label]))}else if(compendiumSort==="edited"){const p=Date.now(),m=864e5,f=7*m,g={today:[],week:[],older:[],never:[]};a.forEach(v=>{const b=v.updated_at?new Date(v.updated_at).getTime():null;b?p-b<m?g.today.push(v):p-b<f?g.week.push(v):g.older.push(v):g.never.push(v)});const h=(v,b)=>{const y=v.updated_at?new Date(v.updated_at).getTime():0;return(b.updated_at?new Date(b.updated_at).getTime():0)-y};Object.values(g).forEach(v=>v.sort(h)),c=["today","week","older","never"].filter(v=>g[v].length>0),l=Object.fromEntries(c.map(v=>[v,g[v]])),renderCompendium._sessionLabels={today:"Today",week:"This Week",older:"Earlier",never:"Never Edited"}}else compendiumTypeFilter==="faction"?(l={organization:[],creature:[]},a.forEach(p=>l[p.faction_group==="creature"?"creature":"organization"].push(p)),Object.values(l).forEach(p=>p.sort((m,f)=>sortKey(m.name).localeCompare(sortKey(f.name)))),c=["organization","creature"].filter(p=>l[p].length),renderCompendium._sessionLabels={organization:"Organizations",creature:"Species & Creatures"}):(renderCompendium._sessionLabels=null,l={},a.forEach(p=>{const m=sortKey(p.name)[0]?.toUpperCase()||"#",f=/[A-Z]/.test(m)?m:"#";l[f]||(l[f]=[]),l[f].push(p)}),c=Object.keys(l).sort((p,m)=>p==="#"?1:m==="#"?-1:p.localeCompare(m)));if(t.innerHTML="",_isGm()){const p=document.createElement("div");p.id="qc-pending-art-mount",t.insertBefore(p,t.firstChild)}const d=_renderPendingRevealSection();if(d){const p=document.createElement("div");for(p.innerHTML=d;p.firstChild;)t.appendChild(p.firstChild)}const u=p=>{const m=ENTITY_CFG[p.type]||ENTITY_CFG.location,f=_entityImageUrl(p),g=thumbUrl(p.thumb_matte_url||f),h=expandedEntityId===p.id,v=document.createElement("div"),b=!!p.is_dead,y=p.type==="pc"&&!!p.is_inactive,_=b||y,w=p.type==="companion"&&p.parent_entity_id?allEntities.find(P=>P.id===p.parent_entity_id):null,$=p.type==="pc"?allEntities.filter(P=>P.type==="companion"&&P.parent_entity_id===p.id):[];v.className="tf-card tf-card--sm tf-card--watermarked entity-row"+(h?" expanded":"")+(b?" is-dead entity-row--dead":"")+(y?" is-departed entity-row--inactive":""),v.dataset.id=p.id;const E=b?p.type==="pc"?'<span class="pc-dead-badge">\u{1F480} Fallen</span>':'<span class="pc-dead-badge">\u{1F480} Dead</span>':"",k=Date.now()-864e5,C=!p.reveal_masked&&(p.created_at&&new Date(p.created_at).getTime()>k||p.revealed_at&&new Date(p.revealed_at).getTime()>k),S=!p.reveal_masked&&p.updated_at&&new Date(p.updated_at).getTime()>k&&(!p.created_at||new Date(p.updated_at).getTime()>new Date(p.created_at).getTime()+5e3),q=function(){return`<svg class="tf-card-thumb-glyph" viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-${p.type==="companion"?"companion":p.type==="pc"?"pc":p.type==="location"?"location":p.type==="faction"?"faction":p.type==="item"?"item":p.type==="spell"?"spell":p.type==="deity"?"deity":p.type==="lore"?"lore":"npc"}"/></svg>`}(),I=(()=>{const P=(p.snippet||"").trim()||(()=>{const A=(p.description||"").replace(/^#{1,6}\s+.+$/gm,"").replace(/\*\*/g,"").trim();return A?A.split(/(?<=[.!?])\s+/)[0]||A:""})();if(!P)return"";const B=P.replace(/^\s*—\s*/,""),H=B.split(/\s+/).filter(Boolean);return H.length<=4?B.replace(/[,.;:]+$/,""):H.slice(0,4).join(" ").replace(/[,.;:]+$/,"")+"\u2026"})(),T=[E,y?'<span class="pc-left-badge">\u{1F44B} Left Party</span>':"",w?`<span class="pc-companion-of-badge">\u{1F43E} ${escHtml(w.name)}</span>`:"",C?'<span class="entity-new-badge">New</span>':"",S?'<span class="entity-updated-badge">Updated</span>':"",(()=>{const P=_itemRarityFor(p);return P?`<span class="entity-badge item-rarity-${P.slug}">${P.label}</span>`:""}
251)()].filter(Boolean).join("");return v.innerHTML=`
252      <div class="entity-row-hdr tf-card-head">
253        <div class="entity-icon entity-icon-tinted tf-card-thumb tf-card-thumb--sm${_?" entity-icon--dead":""}"
254             style="--entity-color:${m.color}; --thumb-color:${m.color}">
255          ${g?`<img src="${g}" alt="" />`:q}
256        </div>
257        <div class="entity-meta">
258          <span class="entity-name tf-card-title">${escHtml(p.name)}</span>
259          ${I?`<span class="entity-snippet tf-card-sub">\u2014 ${escHtml(I)}</span>`:""}
260          ${T}
261        </div>
262        <!-- v1137: spec .tf-badge--ghost \u2014 clear pill, no per-type color
263             tint. The type identity is conveyed by the thumb's tinted
264             glyph (left); the badge label is just a secondary text marker
265             so the row reads cleaner / less busy than the prior colored
266             eyebrow chip. Dropping the legacy .entity-badge class here
267             too \u2014 it was a later-cascade override of .tf-badge that
268             would force the wrong font-size/weight. -->
269        ${_hasBardicTake(p)?_BARDIC_QUILL_IND:""}<span class="tf-badge tf-badge--ghost">${m.label}</span>
270        <span class="entity-chevron">${h?"\u25B2":"\u25BC"}</span>
271      </div>
272      ${h?`
273      <div class="entity-detail">
274        <div class="entity-detail-body">
275          ${_buildEntityContextHeader(p)}
276          ${f?`<img src="${escHtml(f)}" class="entity-full-img${_?" entity-full-img--dead":""}" alt="${escHtml(p.name)}" />`:""}
277          ${Array.isArray(p.aliases)&&p.aliases.length?`<div class="alias-chips alias-chips--ro">${p.aliases.map(P=>`<span class="alias-chip-ro">${escHtml(P)}</span>`).join("")}</div>`:""}
278          ${_spellMetaPills(p)}${_itemMetaPills(p)}
279          <div class="entity-desc">${p.description?_renderEntityDescription(p.description,p.type,p.id):'<em class="entity-desc-empty">No description.</em>'}</div>
280          ${$.length?`
281          <div class="compendium-companions-section">
282            <span class="ddb-row-label">\u{1F43E} Companions</span>
283            <div class="companion-chips">
284              ${$.map(P=>{const B=thumbUrl(_entityImageUrl(P));return`<button class="btn-ghost companion-chip-btn" data-id="${P.id}">
285                  ${B?`<img src="${escHtml(B)}" class="companion-chip-img" alt="" />`:entityGlyphRef("companion","companion-chip-glyph")}
286                  <span>${escHtml(P.name)}</span>
287                </button>`}).join("")}
288            </div>
289          </div>`:""}
290          <div class="entity-crosslinks-mount" data-entity-id="${p.id}"></div>
291          <div class="entity-detail-actions">
292            <button class="btn-new edit-btn" data-id="${p.id}">Edit</button>
293            <button class="btn-new merge-btn" data-id="${p.id}">Merge</button>
294            ${_isGm()&&p.type==="pc"?`<button class="btn-new add-companion-btn" data-pc-id="${p.id}">\u{1F43E} Add Companion</button>`:""}
295            ${_isGm()&&p.type==="pc"?y?`<button class="btn-new toggle-inactive-btn" data-id="${p.id}" data-inactive="true">\u{1F44B} Restore to Party</button>`:`<button class="btn-new toggle-dead-btn" data-id="${p.id}" data-dead="${b}">${b?"\u{1F480} Revive":"\u{1F480} Mark Fallen"}</button>
296                   <button class="btn-new toggle-inactive-btn" data-id="${p.id}" data-inactive="false">\u{1F44B} Left Party</button>`:""}
297            ${_isGm()&&p.type==="npc"?`<button class="btn-new toggle-dead-btn" data-id="${p.id}" data-dead="${b}">${b?"\u{1F480} Revive":"\u2620\uFE0F Mark Dead"}</button>`:""}
298            ${_isGm()&&p.type==="pc"?`<button class="btn-new rebuild-chronicle-btn" data-id="${p.id}" title="Regenerate this PC's description from per-session chronicle blurbs">\u{1F504} Rebuild Chronicle</button>`:""}
299            <button class="btn-new btn-new--danger delete-btn" data-id="${p.id}">Delete</button>
300          </div>
301        </div>
302      </div>`:""}
303    `,v};if(codexMode==="live"||codexMode==="new"||codexMode==="updated"||codexMode==="bardic"||codexMode==="with-images"){const p=Date.now()-864e5;let m=[],f="";if(codexMode==="live"){const h=_sessionCtx();m=a.map(v=>({entity:v,...getEntityRelevanceScore(v,h)})).filter(v=>v.score>0).sort((v,b)=>{if(b.score!==v.score)return b.score-v.score;const y=_=>_CODEX_TYPE_PRI[_.type]??99;return y(v.entity)!==y(b.entity)?y(v.entity)-y(b.entity):sortKey(v.entity.name).localeCompare(sortKey(b.entity.name))}).map(v=>v.entity),f="No entities in play yet. Open or reference entities to build context, or switch to All Entries."}else if(codexMode==="new"){const h=v=>Math.max(v.created_at?new Date(v.created_at).getTime():0,v.revealed_at?new Date(v.revealed_at).getTime():0);m=a.filter(v=>h(v)>p).sort((v,b)=>h(b)-h(v)),f="No entities created in the last 24 hours."}else codexMode==="updated"?(m=a.filter(h=>{if(!h.updated_at)return!1;const v=new Date(h.updated_at).getTime();if(v<=p)return!1;if(h.created_at){const b=new Date(h.created_at).getTime();if(v<=b+5e3)return!1}return!0}).sort((h,v)=>new Date(v.updated_at).getTime()-new Date(h.updated_at).getTime()),f="No entities updated in the last 24 hours."):codexMode==="with-images"?(m=a.filter(h=>h.image_url).sort((h,v)=>sortKey(h.name).localeCompare(sortKey(v.name))),f="No entries have art yet. Add images via the C
303odex image editor."):(m=a.filter(_hasBardicTake).sort((h,v)=>sortKey(h.name).localeCompare(sortKey(v.name))),f="No entries carry the Bard\u2019s Take yet.");if(!m.length){t.innerHTML=`<p class="placeholder placeholder--padded">${f}</p>`;return}const g=document.createElement("div");g.className="alpha-entries-grid",m.forEach(h=>g.appendChild(u(h))),t.appendChild(g)}else c.forEach(p=>{const m=document.createElement("div");m.className="form-section-label alpha-letter-hdr",m.textContent=renderCompendium._sessionLabels?renderCompendium._sessionLabels[p]:p,t.appendChild(m);const f=document.createElement("div");f.className="alpha-entries-grid",l[p].forEach(g=>f.appendChild(u(g))),t.appendChild(f)});_wirePendingRevealButtons(t),_qcRefreshPendingArt(),t.querySelectorAll(".entity-row-hdr").forEach(p=>{p.addEventListener("click",()=>{const m=p.closest(".entity-row");if(!m)return;const f=parseInt(m.dataset.id);if(f&&typeof renderCodexEntry=="function"){_trackEntityView(f),renderCodexEntry(f);return}_trackEntityView(f),expandedEntityId=expandedEntityId===f?null:f,renderCompendium()})}),expandedEntityId!=null&&typeof _renderEntityCrosslinks=="function"&&_renderEntityCrosslinks(expandedEntityId),t.querySelectorAll(".edit-btn").forEach(p=>p.addEventListener("click",m=>{m.stopPropagation(),openEntityModal(parseInt(p.dataset.id))})),t.querySelectorAll(".merge-btn").forEach(p=>p.addEventListener("click",m=>{m.stopPropagation(),openMergeModal(parseInt(p.dataset.id))})),t.querySelectorAll(".delete-btn").forEach(p=>p.addEventListener("click",async m=>{if(m.stopPropagation(),!!await confirmDialog({title:"Delete Entity",message:`Delete "${allEntities.find(f=>f.id===parseInt(p.dataset.id))?.name}"?`,confirmLabel:"Delete"}))try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/entities/${p.dataset.id}`),allEntities=allEntities.filter(f=>f.id!==parseInt(p.dataset.id)),expandedEntityId===parseInt(p.dataset.id)&&(expandedEntityId=null),renderCompendium()}catch(f){showToast("Delete failed: "+f.message,6e3)}})),t.querySelectorAll(".rebuild-chronicle-btn").forEach(p=>p.addEventListener("click",async m=>{m.stopPropagation(),await _entityRebuildChronicle(parseInt(p.dataset.id),p)&&renderCompendium()}
303)),t.querySelectorAll(".companion-chip-btn").forEach(p=>p.addEventListener("click",m=>{m.stopPropagation(),navigateToEntity(parseInt(p.dataset.id))})),t.querySelectorAll(".add-companion-btn").forEach(p=>p.addEventListener("click",m=>{m.stopPropagation(),openEntityModal(null,parseInt(p.dataset.pcId))})),t.querySelectorAll(".toggle-dead-btn").forEach(p=>p.addEventListener("click",async m=>{m.stopPropagation();const f=parseInt(p.dataset.id),g=p.dataset.dead==="true";try{const h=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${f}`,{is_dead:!g});allEntities=allEntities.map(v=>v.id===h.id?h:v),renderCompendium(),showToast(g?`${h.name} has been revived.`:`${h.name} marked as ${h.type==="pc"?"fallen":"dead"}.`)}catch(h){showToast("Failed: "+h.message)}})),t.querySelectorAll(".toggle-inactive-btn").forEach(p=>p.addEventListener("click",async m=>{m.stopPropagation();const f=parseInt(p.dataset.id),g=p.dataset.inactive==="true",h=allEntities.find(b=>b.id===f),v=`/api/campaigns/${currentCampaign.id}/entities/${f}`;try{if(g){const _=await api("PUT",v,{is_inactive:!1});allEntities=allEntities.map(w=>w.id===_.id?_:w),renderCompendium(),showToast(`\u{1F44B} ${_.name} has rejoined the party.`);return}const b=await confirmDialog({title:"Mark as Departed",message:`Mark ${h?.name||"this character"} as having left the party?`,confirmLabel:"Mark Departed",cancelLabel:"Cancel",danger:!1,textInput:{label:"What drew them away? (in the story, optional)",rows:2,maxlength:300,placeholder:"A title reclaimed, a vow taken, a road the party wouldn\u2019t walk\u2026"},checkboxes:[{key:"temporary",label:"Temporary: they may yet return",defaultChecked:!1}]});if(!b.confirmed)return;const y=await api("PUT",v,{is_inactive:!0,is_dead:!1,ledger:{departed_temporary:!!(b.checked&&b.checked.temporary)}});if(allEntities=allEntities.map(_=>_.id===y.id?y:_),(b.text||"").trim())try{await api("PUT",`${v}/last-words`,{last_words:b.text.trim()})}catch{}_applyAutoFailedQuests(y),renderCompendium(),showToast(`\u{1F44B} ${y.name} has left the party.`)}catch(b){showToast("Failed: "+b.message)}}))}function _buildEntityContextHeader(e){const t=_sessionCtx(),n=[];if(e.last_seen_session_id){const o=allSessions.find(r=>r.id===e.last_seen_session_id);if(o){const r=getSessionNum(o),c=o.is_session_zero?"Session 0":`Session ${r}`,l=o.title?`: ${escHtml(o.title)}`:"";n.push(`<div class="ech-row"><span class="ech-label form-section-label">Last Seen</span><span class="ech-value">${c}${l}</span></div>`)}}const a=e.name.toLowerCase(),s=allQuests.filter(o=>o.status==="active"&&((o.title||"").toLowerCase().includes(a)||(o.description||"").toLowerCase().includes(a)));if(s.length){const o=s.map(r=>escHtml(r.title)).join(", ");n.push(`<div class="ech-row"><span class="ech-label form-section-label">Quest</span><span class="ech-value ech-quest">${o}</span></div>`)}if(t.sessionId){const o=allEntities.filter(r=>r.id!==e.id&&r.last_seen_session_id===t.sessionId&&!_hiddenFromViewer(r)).sort((r,c)=>sortKey(r.name).localeCompare(sortKey(c.name))).slice(0,6);if(o.length){const r=o.map(c=>`<button class="ech-entity-btn entity-link" data-id="${c.id}" data-type="${c.type}">${escHtml(c.name)}</button>`).join("");n.push(`<div class="ech-row"><span class="ech-label form-section-label">Also in Play</span><span class="ech-value ech-also">${r}</span></div>`)}}const{reasons:i}=getEntityRelevanceScore(e,t);return i.length&&n.push(`<div class="ech-row"><span class="ech-label form-section-label">Why Now</span><span class="ech-value">${_reasonBadgesHtml(i)}</span></div>`),n.length?`<div class="entity-ctx-header">
304    <div class="form-section-label entity-ctx-label">IN CURRENT CONTEXT</div>
305    ${n.join("")}
306  </div>`:""}function _openQuoteReassignPicker(e,t,n,a,s=null){const i=f=>allEntities.filter(g=>g.type===f&&g.id!==n).sort((g,h)=>g.name.localeCompare(h.name)),o=f=>f.map(g=>`<option value="${g.id}">${escHtml(g.name)}</option>`).join(""),c=[{label:"PCs",list:i("pc")},{label:"Companions",list:i("companion")},{label:"NPCs",list:i("npc")},{label:"Deities",list:i("deity")},{label:"Factions",list:i("faction")}].filter(f=>f.list.length).map(f=>`<optgroup label="${f.label}">${o(f.list)}</optgroup>`).join(""),l=()=>{const f=document.createElement("button");return f.className="btn-ghost pc-quote-reassign-btn",f.dataset.quoteId=String(t),f.title="Reassign this quote",f.textContent="\u2197",f.addEventListener("click",g=>{g.stopPropagation(),_openQuoteReassignPicker(f,t,n,a,s)}),f},d=document.createElement("span");d.className="pc-quote-reassign-picker",d.innerHTML=`
307    <select class="pc-quote-reassign-sel">
308      <option value="">Reassign to\u2026</option>
309      <optgroup label="${escHtml(_partyLabel())}"><option value="party">${escHtml(_partyLabel())}</option></optgroup>
310      ${c}
311    </select>
312    <button class="btn-ghost pc-quote-reassign-ok" disabled title="Confirm reassignment">\u2713</button>
313    <button class="btn-ghost pc-quote-reassign-cancel" title="Cancel">\u2715</button>`,e.replaceWith(d);const u=d.querySelector(".pc-quote-reassign-sel"),p=d.querySelector(".pc-quote-reassign-ok"),m=d.querySelector(".pc-quote-reassign-cancel");u.addEventListener("change",()=>{p.disabled=!u.value}),p.addEventListener("click",async()=>{const f=u.value,g=f==="party",h=g?null:parseInt(f),v=g?{new_entity_id:null,speaker_name:_partyLabel()}:{new_entity_id:h};p.disabled=!0,p.textContent="\u2026";try{await api("PUT",`/api/campaigns/${currentCampaign.id}/quotes/${t}`,v),expandedEntityId=g?"party":h,s&&(s(),a&&setTimeout(()=>{const b=g?'.entity-row[data-quotes-row][data-id="party"]':`.entity-row[data-quotes-row][data-id="${CSS.escape(String(h))}"]`;a.querySelector(b)?.scrollIntoView({behavior:"smooth",block:"start"})},50)),await _refreshPartyQuotes(),showToast("Quote reassigned.")}catch(b){showToast("Failed: "+b.message),d.replaceWith(l())}}),m.addEventListener("click",()=>{d.replaceWith(l())})}function _openQuoteEditForm(e,t,n,a,s,i,o=null,r=null){const c=e.closest(".pc-quote-row");if(!c)return;const l=c.innerHTML,d=r!=null&&r!==""?String(r):"",u='<option value="">No session</option>'+(allSessions||[]).slice().sort((v,b)=>b.id-v.id).map(v=>`<option value="${v.id}"${String(v.id)===d?" selected":""}>${escHtml(v.title||"Session "+v.id)}</option>`).join("");c.innerHTML=`
314    <div class="pc-quote-edit-form">
315      <textarea class="pc-quote-edit-text" rows="2">${escHtml(n)}</textarea>
316      <input class="pc-quote-edit-ctx" type="text" placeholder="Context (optional)" value="${escHtml(a)}">
317      <select class="pc-quote-edit-session" title="Assign this quote to a session: makes it eligible for that session's summary">${u}</select>
318      <div class="pc-quote-edit-actions">
319        <button class="btn-ghost pc-quote-edit-save" title="Save">\u2713 Save</button>
320        <button class="btn-ghost pc-quote-edit-cancel" title="Cancel">\u2715</button>
321      </div>
322    </div>`;const p=c.querySelector(".pc-quote-edit-text"),m=c.querySelector(".pc-quote-edit-ctx"),f=c.querySelector(".pc-quote-edit-session"),g=c.querySelector(".pc-quote-edit-save"),h=c.querySelector(".pc-quote-edit-cancel");p.focus(),p.setSelectionRange(p.value.length,p.value.length),h.addEventListener("click",()=>{c.innerHTML=l,_rewireQuoteButtons(i,s,o)}),g.addEventListener("click",async()=>{const v=p.value.trim();if(!v){showToast("Quote text cannot be empty.");return}g.disabled=!0,g.textContent="\u2026";try{await api("PATCH",`/api/campaigns/${currentCampaign.id}/quotes/${t}`,{quote:v,context:m.value.trim()||null,session_id:f?parseInt(f.value)||0:void 0}),await _refreshPartyQuotes(),o&&o(),showToast("Quote saved.")}catch(b){showToast("Failed: "+b.message),c.innerHTML=l,_rewireQuoteButtons(i,s,o)}})}async function _qotnToggleAward(e,t=null){const n=parseInt(e.dataset.quoteId);if(!n||!currentCampaign)return;const a=_qcQotnQuoteIds.has(String(n));if(await confirmDialog(a?{title:"Remove Award",message:"Remove the Quote of the Night award from this quote?",confirmLabel:"Remove"}:{title:"Quote of the Night",message:"Award Quote of the Night to this quote? It becomes the crowned winner for its session, replacing any current winner.",confirmLabel:"Award"}))try{await api("POST",`/api/campaigns/${currentCampaign.id}/quote-night/award`,{quote_id:n,remove:a}),await _qcLoadCardedSet(),t&&t(),showToast(a?"Quote of the Night award removed.":"Quote of the Night awarded.")}catch(i){showToast("Award failed: "+(i.message||"Unknown"))}}function _rewireQuoteButtons(e,t,n=null){typeof _qcApplyCardBadges=="function"&&_qcApplyCardBadges(),e.querySelectorAll(".pc-quote-qotn-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),_qotnToggleAward(a,n)})}),e.querySelectorAll(".pc-quote-reassign-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),_openQuoteReassignPicker(a,parseInt(a.dataset.quoteId),t,e,n)})}),e.querySelectorAll(".pc-quote-edit-btn").forEach(a=>
322{a.addEventListener("click",s=>{s.stopPropagation(),_openQuoteEditForm(a,parseInt(a.dataset.quoteId),a.dataset.quote,a.dataset.context,t,e,n,a.dataset.sessionId)})}),e.querySelectorAll(".pc-quote-delete-btn").forEach(a=>{a.addEventListener("click",async s=>{if(s.stopPropagation(),!!await confirmDialog({title:"Delete Quote",message:"Delete this quote?",confirmLabel:"Delete"}))try{const i=a.dataset.quoteId;await api("DELETE",`/api/campaigns/${currentCampaign.id}/quotes/${i}`),await _refreshPartyQuotes(),n&&n();const o=async()=>{try{await api("POST",`/api/campaigns/${currentCampaign.id}/quotes/${i}/restore`),await _refreshPartyQuotes(),n&&n(),showToast("Quote restored.")}catch(r){showToast("Restore failed: "+(r.message||"Unknown"))}};typeof window.tfToast=="function"?window.tfToast({text:"Quote deleted.",duration:6e3,action:{label:"Undo",onClick:o}}):showToast("Quote deleted.")}catch(i){showToast("Failed: "+i.message)}})})}async function loadEntities(){_hideEntityPreview();const e=currentCampaign&&currentCampaign.id,t=!!(allEntities&&allEntities.length&&_entitiesLoadedCid===e);if(t)renderCompendium(),_applyMyCharTabVisibility();else{const a=document.getElementById("entity-list");a&&!a.querySelector(".entity-row")&&(a.innerHTML=_renderSkeletonCards(6,"sm"))}const n=async()=>{try{const a=await Promise.all([_apiAllPages(`/api/campaigns/${e}/entities`),allQuests.length===0?api("GET",`/api/campaigns/${e}/quests`):Promise.resolve(null)]);if(!currentCampaign||currentCampaign.id!==e)return;allEntities=a[0],_entitiesLoadedCid=e,a[1]&&(allQuests=a[1]),renderCompendium(),_applyMyCharTabVisibility();const s=document.getElementById("tab-party2");if(s&&!s.classList.contains("hidden")&&typeof _party2Spec=="object"&&_party2Spec&&_party2LoadedCid===e&&!(_party2Spec.party_portrait||{}).image_url&&!s.querySelector(".p2-camp")&&!s.contains(document.activeElement))try{renderParty2({fromCache:!0,background:!0})}catch{}}catch(a){if(!t){const s=document.getElementById("entity-list");s&&(s.innerHTML=`<p class="error placeholder--padded">${escHtml(a.message)}</p>`)}}};t?n():await n()}let mergeEntityAId=null,mergeEntityBId=null;function openMergeModal(e){const t=typeof ENTITY_CFG<"u"?ENTITY_CFG:{};mergeEntityAId=e,mergeEntityBId=null;const n=allEntities.find(s=>s.id===e);if(!n)return;document.getElementById("merge-a-name").textContent=n.name;const a=t[n.type]||{};document.getElementById("merge-a-type").textContent=a.label||n.type,document.getElementById("merge-card-b-empty").classList.remove("hidden"),document.getElementById("merge-card-b-filled").classList.add("hidden"),document.getElementById("merge-survivor-row").classList.add("hidden"),document.getElementById("merge-confirm-btn").disabled=!0,document.getElementById("merge-error").classList.add("hidden"),document.getElementById("merge-loading").classList.add("hidden"),document.getElementById("merge-search-input").value="",document.getElementById("merge-search-results").innerHTML="",document.getElementById("merge-search-results").classList.add("hidden"),document.querySelectorAll('input[name="merge-survivor"]').forEach(s=>{s.value==="a"&&(s.checked=!0)}),document.getElementById("merge-modal").classList.remove("hidden"),document.getElementById("merge-search-input").focus()}function closeMergeModal(){document.getElementById("merge-modal").classList.add("hidden"),document.getElementById("merge-loading").classList.add("hidden"),document.getElementById("merge-confirm-btn").disabled=!0,document.getElementById("merge-cancel-btn").disabled=!1,mergeEntityAId=null,mergeEntityBId=null}document.getElementById("merge-search-input").addEventListener("input",()=>{const e=document.getElementById("merge-search-input").value.trim().toLowerCase(),t=document.getElementById("merge-search-results");if(e.length<1){t.classList.add("hidden"),t.innerHTML="";return}const n=allEntities.filter(a=>{if(a.id===mergeEntityAId||_hiddenFromViewer(a))return!1;const s=a.name.toLowerCase(),i=(Array.isArray(a.aliases)?a.aliases:[]).join(" ").toLowerCase();return s.includes(e)||i.includes(e)}).sort((a,s)=>{const i=a.name.toLowerCase().startsWith(e),o=s.name.toLowerCase().startsWith(e);return i!==o?i?-1:1:a.name.localeCompare(s.name)}).slice(0,20);if(n.length===0){t.innerHTML='<div class="merge-search-empty">No matches</div>',t.classList.remove("hidden");return}t.innerHTML=n.map(a=>{const s=ENTITY_CFG[a.type]||{};return`<div class="merge-search-item" data-id="${a.id}">
323      <span class="merge-search-name">${escHtml(a.name)}</span>
324      <span class="merge-search-type entity-badge-tinted" style="--entity-color:${s.color||"var(--text-3)"}">${s.label||a.type}</span>
325    </div>`}).join(""),t.classList.remove("hidden"),t.querySelectorAll(".merge-search-item").forEach(a=>{a.addEventListener("click",()=>selectMergeEntityB(parseInt(a.dataset.id)))})});function selectMergeEntityB(e){mergeEntityBId=e;const t=allEntities.find(s=>s.id===e);if(!t)return;const n=ENTITY_CFG[t.type]||{};document.getElementById("merge-b-name").textContent=t.name,document.getElementById("merge-b-type").textContent=n.label||t.type,document.getElementById("merge-card-b-empty").classList.add("hidden"),document.getElementById("merge-card-b-filled").classList.remove("hidden"),document.getElementById("merge-search-results").classList.add("hidden"),document.getElementById("merge-search-input").value="";const a=allEntities.find(s=>s.id===mergeEntityAId);document.getElementById("merge-survivor-a-label").textContent=a?.name||"",document.getElementById("merge-survivor-b-label").textContent=t.name,document.getElementById("merge-survivor-row").classList.remove("hidden"),document.getElementById("merge-confirm-btn").disabled=!1}document.getElementById("merge-confirm-btn").addEventListener("click",async()=>{if(!mergeEntityAId||!mergeEntityBId)return;const t=document.querySelector('input[name="merge-survivor"]:checked').value==="a"?mergeEntityAId:mergeEntityBId,n=document.getElementById("merge-error"),a=document.getElementById("merge-loading");n.classList.add("hidden"),document.getElementById("merge-confirm-btn").disabled=!0,document.getElementById("merge-cancel-btn").disabled=!0,a.classList.remove("hidden");try{const s=await api("POST",`/api/campaigns/${currentCampaign.id}/entities/${mergeEntityAId}/merge`,{merge_with_id:mergeEntityBId,survivor_id:t});if(allEntities=allEntities.filter(i=>i.id!==s.deleted_id).map(i=>i.id===s.survivor.id?s.survivor:i),expandedEntityId=s.survivor.id,closeMergeModal(),renderCompendium(),!_codex2Panel().classList.contains("hidden")&&(location.hash||"").startsWith("#codex/"))try{renderCodexEntry(s.survivor.id)}catch{}showToast(`Merged \u2192 "${s.survivor.name}"`),requestAnimationFrame(()=>{const i=document.querySelector(`.entity-row[data-id="${s.survivor.id}"]`);i&&i.scrollIntoView({behavior:"smooth",block:"start"})})}catch(s){a.classList.add("hidden"),n.textContent="Merge failed: "+s.message,n.classList.remove("hidden"),document.getElementById("merge-confirm-btn").disabled=!1,document.getElementById("merge-cancel-btn").disabled=!1}}),document.getElementById("merge-modal-close").addEventListener("click",closeMergeModal),document.getElementById("chr2-merge-close")?.addEventListener("click",_chr2CloseMergeModal),document.getElementById("chr2-merge-back")?.addEventListener("click",()=>{const e=document.getElementById("chr2-merge-modal");e&&(_chr2MergePick=null,e.querySelector("#chr2-merge-summary").classList.add("hidden"),e.querySelector("#chr2-merge-picker").classList.remove("hidden"),e.querySelector("#chr2-merge-back").classList.add("hidden"),e.querySelector("#chr2-merge-confirm").disabled=!0)}),document.getElementById("chr2-merge-confirm")?.addEventListener("click",_chr2MergeConfirm),document.getElementById("merge-cancel-btn").addEventListener("click",closeMergeModal);let _mergeModalMousedownOnBackdrop=!1;document.getElementById("merge-modal").addEventListener("mousedown",e=>{_mergeModalMousedownOnBackdrop=e.target===document.getElementById("merge-modal")}),document.getElementById("merge-modal").addEventListener("click",e=>{e.target===document.getElementById("merge-modal")&&_mergeModalMousedownOnBackdrop&&closeMergeModal()});let _compendiumSearchTimer;document.getElementById("compendium-search").addEventListener("input",e=>{compendiumQuery=e.target.value.trim(),clearTimeout(_compendiumSearchTimer),_compendiumSearchTimer=setTimeout(renderCompendium,150)}),document.getElementById("codex-sort-btn").addEventListener("click",()=>{compendiumSort=compendiumSort==="alpha"?"session":compendiumSort==="session"?"edited":"alpha";
325const e={alpha:"A\u2013Z",session:"Session",edited:"Edited"};document.getElementById("codex-sort-btn").textContent=e[compendiumSort],renderCompendium()}),document.getElementById("codex-mode-btn").addEventListener("click",()=>{const e=_CODEX_MODES.indexOf(codexMode);codexMode=_CODEX_MODES[(e+1)%_CODEX_MODES.length],_codexModeAuto=!1,renderCompendium()}),document.querySelectorAll(".type-filter-btn").forEach(e=>{e.addEventListener("click",()=>{document.querySelectorAll(".type-filter-btn").forEach(t=>t.classList.remove("active")),e.classList.add("active"),compendiumTypeFilter=e.dataset.type,renderCompendium()})}),document.getElementById("add-entity-btn").addEventListener("click",()=>openEntityModal(null)),document.getElementById("add-quote-btn").addEventListener("click",()=>{const e=document.getElementById("entity-list");if(e.querySelector(".add-quote-form"))return;const t=["pc","npc","companion","deity","faction"],n=allEntities.filter(l=>t.includes(l.type)&&!_hiddenFromViewer(l)).sort((l,d)=>t.indexOf(l.type)-t.indexOf(d.type)).map(l=>`<option value="${l.id}">${ENTITY_CFG[l.type]?.icon||""} ${escHtml(l.name)}</option>`).join(""),a=document.createElement("div");a.className="add-quote-form pc-quote-edit-form",a.innerHTML=`
326    <textarea class="pc-quote-edit-text" rows="3" placeholder="Enter quote\u2026"></textarea>
327    <select class="pc-quote-edit-ctx" style="margin-top:4px">
328      <option value="">(No speaker)</option>
329      ${n}
330    </select>
331    <input class="pc-quote-edit-ctx" type="text" placeholder="Context (optional)" style="margin-top:4px">
332    <div class="pc-quote-edit-actions">
333      <button class="btn-ghost pc-quote-edit-save">\u2713 Save</button>
334      <button class="btn-ghost pc-quote-edit-cancel">\u2715</button>
335    </div>`,e.prepend(a);const s=a.querySelector("textarea"),i=a.querySelectorAll("select")[0],o=a.querySelectorAll("input")[0],r=a.querySelector(".pc-quote-edit-save"),c=a.querySelector(".pc-quote-edit-cancel");s.focus(),c.addEventListener("click",()=>{a.remove()}),r.addEventListener("click",async()=>{const l=s.value.trim();if(!l){showToast("Quote text is required.");return}r.disabled=!0,r.textContent="\u2026";try{await api("POST",`/api/campaigns/${currentCampaign.id}/quotes`,{quote:l,speaker_entity_id:i.value?parseInt(i.value):null,context:o.value.trim()||null}),await _refreshPartyQuotes(),a.remove(),renderQuotesPanel(e),showToast("Quote saved.")}catch(d){showToast("Failed: "+d.message),r.disabled=!1,r.textContent="\u2713 Save"}})});let _efBatchMode=!1,_efBatchNames=[];function _efNameList(e){return e.length<=1?e[0]||"":e.slice(0,-1).join(", ")+" and "+e[e.length-1]}function _efSyncBatchChrome(){const e=document.getElementById("entity-form"),t=document.getElementById("ef-batch-note"),n=document.getElementById("entity-form-save"),a=document.getElementById("entity-form-cancel"),s=document.getElementById("entity-modal-title");if(!e||(e.classList.toggle("tf-ef-batch",_efBatchMode),n&&(n.textContent=_efBatchMode?"Save and add another":"Save Entity"),a&&(a.textContent=_efBatchMode&&_efBatchNames.length?"Done":"Cancel"),s&&_efBatchMode&&(s.textContent="Add your player characters"),!t))return;t.classList.toggle("hidden",!_efBatchMode);const i=document.getElementById("ef-name");if(_efBatchMode?(t.setAttribute("aria-live","polite"),i&&i.setAttribute("aria-describedby","ef-batch-note")):i&&i.removeAttribute("aria-describedby"),!_efBatchMode){t.textContent="";return}t.textContent=_efBatchNames.length?_efNameList(_efBatchNames)+(_efBatchNames.length===1?" is in.":" are in.")+" Add another, or choose Done when you are finished.":"A name is all you need to start. You can fill in the rest whenever you like."}function _efBatchFinish(){const e=_efBatchNames.length;if(_efBatchMode=!1,_efBatchNames=[],_efSyncBatchChrome(),e)try{renderCompendium()}catch{}if(e&&typeof _refreshPartyData=="function"&&_refreshPartyData().then(()=>{const t=document.getElementById("tab-party2");t&&t.offsetParent!==null&&typeof renderParty2=="function"&&renderParty2({cid:currentCampaign&&currentCampaign.id})}).catch(()=>{}),typeof _setupCardRefresh=="function")try{_setupCardRefresh()}catch{}}function toggleDdbRow(e){const t=e==="pc",n=e==="companion",a=e==="quote";document.getElementById("ef-name-row").classList.toggle("hidden",a),document.getElementById("ef-name").required=!a,document.getElementById("ef-description-row").classList.toggle("hidden",a),document.getElementById("ef-image-row").classList.toggle("hidden",a),document.getElementById("ef-alias-row").classList.toggle("hidden",a),document.getElementById("ef-quote-row").classList.toggle("hidden",!a),document.getElementById("ef-quote-speaker-row").classList.toggle("hidden",!a),document.getElementById("ef-name-label").textContent=t?"Character Name":n?"Companion Name":"Name",["ef-ddb-row","ef-player-name-row","ef-pronouns-row","ef-party-role-row","ef-noncombat-role-row"].forEach(s=>{const i=document.getElementById(s);i.classList.toggle("hidden",!t),i.style.display=""}),document.getElementById("ef-parent-pc-row").classList.toggle("hidden",!n)}let editingAliases=[];function renderAliasChips(){const e=document.getElementById("ef-alias-chips");
335e.innerHTML="",editingAliases.forEach((t,n)=>{const a=document.createElement("span");a.className="alias-chip",a.innerHTML=`${escHtml(t)}<button type="button" aria-label="Remove alias" data-idx="${n}">\u2715</button>`,a.querySelector("button").addEventListener("click",()=>{editingAliases.splice(n,1),renderAliasChips()}),e.appendChild(a)})}function addAliasFromInput(){const e=document.getElementById("ef-alias-input"),t=e.value.trim();t&&(editingAliases.includes(t)||(editingAliases.push(t),renderAliasChips()),e.value="",e.focus())}document.getElementById("ef-alias-add-btn").addEventListener("click",addAliasFromInput),document.getElementById("entity-form").addEventListener("keydown",e=>{if(e.key!=="Enter")return;const t=document.activeElement;if(!(!t||t.tagName==="TEXTAREA")){if(e.preventDefault(),_efBatchMode&&t.id==="ef-name"){t.value.trim()&&document.getElementById("entity-form").requestSubmit();return}if(t.id==="ef-alias-input"){addAliasFromInput(),t.blur();return}t.blur()}});function _extractPronouns(e){const t=(e||"").match(/\(([^)]+)\)\s*$/);if(!t)return"";const n=t[1].trim().toLowerCase(),a={"h/h":"he/him","he/h":"he/him","s/h":"she/her","she/h":"she/her","t/t":"they/them","they/t":"they/them","h/t":"he/they","s/t":"she/they","it/its":"it/its"};return!n.includes("/")||n.length>20?"":a[n]||t[1].trim()}function _discordRevealReady(){const e=_campDiscordConfig;return!!(e&&e.guild_id&&(e.reveal_channel_id||e.announce_channel_id||e.recap_channel_id))}function _syncPendingRevealRow(e,t){const n=document.getElementById("ef-pending-reveal-row"),a=document.getElementById("ef-pending-reveal"),s=document.getElementById("ef-pending-reveal-help"),i=document.getElementById("ef-pending-reveal-private");if(!(!n||!a)){if(a.checked=!!e?.pending_reveal,a.disabled=!1,i&&(i.checked=!!e?.pending_reveal_private),_efTeaseSeed(e),t==="quote"){n.classList.add("hidden");return}n.classList.remove("hidden"),s&&(s.innerHTML=_discordRevealReady()?"Players won\u2019t see this entry until you reveal it (from the Codex\u2019s Unrevealed list or with <code>/tf-reveal</code> in Discord). Your co-GMs can see it while it\u2019s hidden.":"Players won\u2019t see this entry until you reveal it from the Codex\u2019s Unrevealed list; it joins the Codex marked <b>New</b>. Your co-GMs can see it while it\u2019s hidden.",s.classList.remove("hidden")),_syncPendingRevealPrivateVisibility()}}function _syncPendingRevealPrivateVisibility(){const e=document.getElementById("ef-pending-reveal"),t=document.getElementById("ef-pending-reveal-private-label");if(!e||!t)return;const n=e.checked&&!e.disabled;t.classList.toggle("hidden",!n);const a=document.getElementById("ef-tease-block");a&&a.classList.toggle("hidden",!n),_efTeaseSync()}function _efTeaseEls(){return{block:document.getElementById("ef-tease-block"),pos:document.getElementById("ef-tease-position"),nameTrue:document.getElementById("ef-tease-name-true"),nameParty:document.getElementById("ef-tease-name-party"),photo:document.getElementById("ef-tease-photo"),alias:document.getElementById("ef-tease-alias"),warn:document.getElementById("ef-tease-name-warn")}}function _efTeaseSeed(e){const t=_efTeaseEls();if(!t.block)return;t.pos.checked=!!e?.tease_position,t.photo.checked=!!e?.tease_photo;const n=!!e?.tease_name;t.nameTrue&&(t.nameTrue.checked=n),t.nameParty&&(t.nameParty.checked=!n),t.alias.value=e?.tease_alias||"",_efTeaseSync()}function _efTeaseNameOn(){const e=document.getElementById("ef-tease-name-true");return!!(e&&e.checked)}function _efTeaseSync(){const e=_efTeaseEls();if(!e.block)return;const t=document.getElementById("ef-pending-reveal"),n=!!(t&&t.checked&&!t.disabled);e.alias.disabled=_efTeaseNameOn();let a=!1;if(n&&!_efTeaseNameOn()){const s=(document.getElementById("ef-name")?.value||"").trim(),i=document.getElementById("ef-description")?.value||"";if(s.length>1&&i)try{a=new RegExp("(?<![\\w])"+s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?![\\w])","i").test(i)}catch{a=i.toLowerCase().includes(s.toLowerCase())}}e.warn.classList.toggle("hidden",!a)}function _efTeaseWire(){const e=_efTeaseEls();if(!e.block)return;const t=document.getElementById("ef-pending-reveal");t?.addEventListener("change",()=>{if(t.checked){const n=editingEntityId?(allEntities||[]).find(a=>a.id===editingEntityId):null;!(n&&n.pending_reveal)&&!e.pos.checked&&!_efTeaseNameOn()&&!e.photo.checked&&(e.pos.checked=!0)}_efTeaseSync()}
335),[e.pos,e.nameTrue,e.nameParty,e.photo].forEach(n=>n&&n.addEventListener("change",_efTeaseSync)),document.getElementById("ef-description")?.addEventListener("input",_efTeaseSync),document.getElementById("ef-name")?.addEventListener("input",_efTeaseSync)}_efTeaseWire();function _companionExplainer(){return"A companion belongs to one PC: a familiar, pet, mount, or bonded ally. It shows on that PC's card and Codex page, and leaves the table if the PC falls or departs."}function openEntityModal(e,t=null,n=null,a={}){_efBatchMode=!!a.batchMode;const s=!document.getElementById("entity-modal").classList.contains("hidden");(!_efBatchMode||!s)&&(_efBatchNames=[]),editingEntityId=e;const i=e?allEntities.find(m=>m.id===e):null,o=!!(i&&n&&n!==i.type),r=(o?n:i?.type)||n||(t?"companion":"location");document.getElementById("entity-modal-title").textContent=o&&n==="companion"?"Set as a PC's Companion":i?`Edit ${i.type==="pc"?"PC":"Entity"}`:t?"Add Companion":n==="pc"?"New PC":"Add Entity",document.getElementById("ef-name").value=i?.name||"",document.getElementById("ef-type").value=r,document.getElementById("ef-description").value=i?.description||"",document.getElementById("ef-image-file").value="",_efOpenImageAfterCreate=!1,document.getElementById("ef-upload-status").classList.add("hidden"),document.getElementById("entity-form-error").classList.add("hidden"),document.getElementById("ef-ddb-url").value=i?.dnd_beyond_url||"",document.getElementById("ef-ddb-status").classList.add("hidden"),toggleDdbRow(r),document.getElementById("ef-player-name").value=i?.player_name||"",document.getElementById("ef-pronouns").value=i?.pronouns||_extractPronouns(i?.name||""),document.getElementById("ef-party-role").value=i?.party_role||"",document.getElementById("ef-noncombat-role").value=i?.noncombat_role||"";const c=document.getElementById("ef-img-preview");i?.image_url?(c.src=i.image_url,c.classList.remove("hidden")):(c.src="",c.classList.add("hidden")),_renderEntityModalGallery(i),editingAliases=Array.isArray(i?.aliases)?[...i.aliases]:[],document.getElementById("ef-alias-input").value="",renderAliasChips();const l=document.getElementById("ef-parent-pc-help");l&&(l.textContent=_companionExplainer());const d=document.getElementById("ef-parent-pc");d.innerHTML='<option value="">(Select PC)</option>',allEntities.filter(m=>m.type==="pc").forEach(m=>{const f=document.createElement("option");f.value=m.id,f.textContent=m.name,d.appendChild(f)}),d.value=i?.parent_entity_id??t??"";const u=document.getElementById("ef-quote-speaker");u.innerHTML='<option value="">(No speaker assigned)</option>';const p=["pc","npc","companion","deity","faction"];allEntities.filter(m=>p.includes(m.type)).sort((m,f)=>p.indexOf(m.type)-p.indexOf(f.type)).forEach(m=>{const f=document.createElement("option");f.value=m.id,f.textContent=`${ENTITY_CFG[m.type]?.icon||""} ${m.name}`,u.appendChild(f)}),document.getElementById("ef-quote-text").value=r==="quote"&&i?.description||"",u.value=r==="quote"&&i?.parent_entity_id||"",_syncPendingRevealRow(i,r),_efSyncBatchChrome(),document.getElementById("entity-modal").classList.remove("hidden"),r==="quote"?document.getElementById("ef-quote-text").focus():o&&r==="companion"?document.getElementById("ef-parent-pc").focus():document.getElementById("ef-name").focus()}function closeEntityModal(){document.getElementById("entity-modal").classList.add("hidden"),_efBatchMode&&_efBatchFinish()}document.addEventListener("keydown",e=>{if(e.key!=="Escape"||e.__tfModalEsc)return;const t=document.getElementById("entity-modal");!t||t.classList.contains("hidden")||typeof _tfTopModal=="function"&&_tfTopModal()!==t||(e.__tfModalEsc=!0,closeEntityModal())},!0),document.getElementById("entity-modal-close").addEventListener("click",closeEntityModal),document.getElementById("entity-form-cancel").addEventListener("click",closeEntityModal);let _entityModalMousedownOnBackdrop=!1;document.getElementById("entity-modal").addEventListener("mousedown",e=>{_entityModalMousedownOnBackdrop=e.target===document.getElementById("entity-modal")}),document.getElementById("entity-modal").addEventListener("click",e=>{e.target===document.getElementById("entity-modal")&&_entityModalMousedownOnBackdrop&&closeEntityModal()}),document.getElementById("ef-type").addEventListener("change",e=>{toggleDdbRow(e.target.value);const t=document.getElementById("ef-pending-reveal-row");t&&t.classList.toggle("hidden",e.target.value==="quote"),_syncPendingRevealPrivateVisibility()}),document.getElementById("ef-pending-reveal")?.addEventListener("change",function(){if(!this.checked){const e=document.getElementById("ef-pending-reveal-private");e&&(e.checked=!1)}_syncPendingRevealPrivateVisibility()}),document.getElementById("ef-image-file").addEventListener("change",()=>{const e=document.getElementById("ef-image-file").files[0],t=document.getElementById("ef-img-preview");if(e){const n=new FileReader;n.onload=a=>{t.src=a.target.result,t.classList.remove("hidden")},n.readAsDataURL(e)}});let _efOpenImageAfterCreate=!1,_efSubmitting=!1;function _efRefreshImagesFromCache(e){const t=(allEntities||[]).find(a=>a.id===e);if(!t)return;const n=document.getElementById("ef-img-preview");n&&(t.image_url?(n.src=t.image_url,n.classList.remove("hidden")):(n.src="",n.classList.add("hidden"))),_renderEntity
335ModalGallery(t)}document.getElementById("ef-add-image-btn").addEventListener("click",()=>{if(editingEntityId){openCx2ImageModal(editingEntityId,()=>_efRefreshImagesFromCache(editingEntityId));return}const e=document.getElementById("ef-name");if(document.getElementById("ef-type").value!=="quote"&&!e.value.trim()){const t=document.getElementById("entity-form-error");t.textContent="Give it a name first, then add or generate an image.",t.classList.remove("hidden"),e.focus();return}_efOpenImageAfterCreate=!0,_efSubmitting||document.getElementById("entity-form").requestSubmit()});function _renderEntityModalGallery(e){const t=document.getElementById("ef-image-gallery");if(!t)return;const n=e?.id,a=_entityImageGallery(e);if(!n||a.length<1){t.innerHTML="",t.classList.add("hidden");return}t.classList.remove("hidden"),t.innerHTML=a.map((s,i)=>`
336    <div class="ef-gallery-tile${i===0?" ef-gallery-tile--primary":""}" data-url="${escHtml(s)}">
337      <img src="${escHtml(s)}" alt="image ${i+1}" />
338      ${i===0?'<span class="ef-gallery-primary-badge" title="Primary">\u2605</span>':`<button type="button" class="ef-gallery-btn ef-gallery-btn--star" title="Make primary" data-eid="${n}" data-idx="${i}">\u2605</button>`}
339      <button type="button" class="ef-gallery-btn ef-gallery-btn--remove" title="Remove image" data-eid="${n}" data-idx="${i}">\xD7</button>
340    </div>
341  `).join(""),t.querySelectorAll(".ef-gallery-btn--star").forEach(s=>{s.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();const o=parseInt(s.dataset.idx);await _applyEntityImagesSet(n,_galleryReorderPrimary(a,o))})}),t.querySelectorAll(".ef-gallery-btn--remove").forEach(s=>{s.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();const o=parseInt(s.dataset.idx),r=a.filter((c,l)=>l!==o);await _applyEntityImagesSet(n,r)})})}function _galleryReorderPrimary(e,t){if(t<=0||t>=e.length)return[...e];const n=[...e],[a]=n.splice(t,1);return[a,...n]}async function _applyEntityImagesSet(e,t){try{const n=await api("POST",`/api/campaigns/${currentCampaign.id}/entities/${e}/images/set`,{image_urls:t}),{bake_queued:a,bake_processing:s,...i}=n;allEntities=allEntities.map(r=>r.id===i.id?i:r),_renderEntityModalGallery(i);const o=document.getElementById("ef-img-preview");return o&&(i.image_url?(o.src=i.image_url,o.classList.remove("hidden")):(o.src="",o.classList.add("hidden"))),renderCompendium(),a===!0}catch(n){return showToast("Image update failed: "+n.message),!1}}document.getElementById("entity-form").addEventListener("submit",async e=>{if(e.preventDefault(),_efSubmitting)return;_efSubmitting=!0;const t=document.getElementById("entity-form-error"),n=document.getElementById("ef-upload-status");t.classList.add("hidden");const a=e.target.querySelector("[type=submit]");a.disabled=!0;const s=editingEntityId?allEntities.find(p=>p.id===editingEntityId):null,i=document.getElementById("ef-type").value,o=document.getElementById("ef-ddb-url").value.trim()||null,r=document.getElementById("ef-parent-pc").value;let c="",l=null;if(i==="quote"){if(c=document.getElementById("ef-quote-text").value.trim(),!c){t.textContent="Quote text is required.",t.classList.remove("hidden"),a.disabled=!1,_efSubmitting=!1;return}const p=document.getElementById("ef-quote-speaker").value;l=p?parseInt(p):null}if(i==="companion"&&!r){t.textContent="Pick the PC this companion belongs to.",t.classList.remove("hidden"),a.disabled=!1,_efSubmitting=!1;return}const u={name:i==="quote"?c.length>60?c.slice(0,60).trimEnd()+"\u2026":c:document.getElementById("ef-name").value.trim(),type:i,description:i==="quote"?c:document.getElementById("ef-description").value.trim()||null,aliases:i==="quote"?[]:editingAliases,...i==="pc"?{dnd_beyond_url:o,player_name:document.getElementById("ef-player-name").value.trim()||null,pronouns:document.getElementById("ef-pronouns").value.trim()||null,party_role:document.getElementById("ef-party-role").value.trim()||null,noncombat_role:document.getElementById("ef-noncombat-role").value.trim()||null}:{},...i==="companion"?{parent_entity_id:r?parseInt(r):null}:{},...i==="quote"?{parent_entity_id:l}:{},...(()=>{if(i==="quote")return{};const p=document.getElementById("ef-pending-reveal");if(!p||p.disabled)return{};
341const m=document.getElementById("ef-pending-reveal-private"),f=document.getElementById("ef-tease-position"),g=document.getElementById("ef-tease-name-true"),h=document.getElementById("ef-tease-photo"),v=document.getElementById("ef-tease-alias");return{pending_reveal:!!p.checked,pending_reveal_private:!!(p.checked&&m&&m.checked),tease_position:!!(p.checked&&f&&f.checked),tease_name:!!(p.checked&&g&&g.checked),tease_photo:!!(p.checked&&h&&h.checked),tease_alias:v&&v.value.trim()||null}})()};try{let p=editingEntityId?await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${editingEntityId}`,u):await api("POST",`/api/campaigns/${currentCampaign.id}/entities`,u);const m=document.getElementById("ef-image-file"),f=Array.from(m.files||[]);if(f.length){for(let v=0;v<f.length;v++){n.textContent=f.length>1?`Uploading image ${v+1} of ${f.length}\u2026`:"Uploading image\u2026",n.classList.remove("hidden");try{p=await _tfUploadDirect(f[v],{paramsPath:`/api/campaigns/${currentCampaign.id}/upload-params`,kind:"entity",endpoint:`/api/campaigns/${currentCampaign.id}/entities/${p.id}/upload-image`})}catch(b){throw new Error("Image upload: "+(b&&b.message||"failed"))}_tfHandleAutoInline(p)}n.classList.add("hidden")}editingEntityId?allEntities=allEntities.map(v=>v.id===p.id?p:v):allEntities.push(p),_efBatchMode||(expandedEntityId=p.id);const g=v=>v==="pc"||v==="companion",h=!editingEntityId&&g(p.type)||!!s&&(s.type!==p.type||s.parent_entity_id!==p.parent_entity_id)&&(g(p.type)||g(s.type));if(s&&s.type!==p.type){const v=document.getElementById("tab-codex2");v&&(v._cx2RailCid=null)}if(_efBatchMode){_efBatchNames.push(p.name);try{_setupCardRefresh()}catch{}openEntityModal(null,null,"pc",{batchMode:!0});return}closeEntityModal();try{renderCompendium()}catch(v){console.error("renderCompendium after entity save failed",v)}try{_setupCardRefresh()}catch(v){console.error("_setupCardRefresh after entity save failed",v)}if(_efOpenImageAfterCreate&&!editingEntityId&&openCx2ImageModal(p.id),_efOpenImageAfterCreate=!1,h&&_refreshPartyData().then(()=>{const v=document.getElementById("tab-party2");v&&v.offsetParent!==null&&typeof renderParty2=="function"&&renderParty2({cid:currentCampaign&&currentCampaign.id})}).catch(()=>{}),_codex2RerenderIfOpen(f.length?p.id:null),p.description){const v=s?.description||null;api("POST",`/api/campaigns/${currentCampaign.id}/entities/${p.id}/sync-crosslinks`,{prev_description:v}).then(b=>{if(b.updated.length>0){for(const y of b.updated)allEntities=allEntities.map(_=>_.id===y.id?y:_);renderCompendium(),showToast(`\u{1F517} Updated ${b.updated.length} crosslinked ${b.updated.length===1?"entry":"entries"}`)}}).catch(()=>{})}}catch(p){t.textContent=p.message,t.classList.remove("hidden")}finally{a.disabled=!1,_efSubmitting=!1}});let pinsLocked=!1;function pinLockKey(){return`pin_locked_${currentCampaign?.id}`}function loadPinLockState(){pinsLocked=localStorage.getItem(pinLockKey())==="true"}const TF_UPLOAD_MAX_BYTES=10*1024*1024,TF_MAP_MAX_BYTES=TF_UPLOAD_MAX_BYTES;async function _tfResErr(e){const t=await e.text();try{return JSON.parse(t).detail||t}catch{return t}}async function _tfUploadDirect(e,{paramsPath:t,kind:n,endpoint:a,fields:s={},label:i="Image"}){if(!e)throw new Error("No file selected.");if(e.size>TF_UPLOAD_MAX_BYTES)throw new Error(`${i} must be 10 MB or smaller.`);const o=getStoredToken(),r=o?{Authorization:"Bearer "+o}:{},c=new FormData;c.append("kind",n);const l=await fetch(t,{method:"POST",credentials:"include",headers:r,body:c});if(!l.ok)throw new Error(await _tfResErr(l));const d=await l.json(),u=new FormData;u.append("file",e),u.append("api_key",d.api_key),u.append("timestamp",d.timestamp),u.append("signature",d.signature),u.append("folder",d.folder),d.transformation&&u.append("transformation",d.transformation);const p=await fetch(d.upload_url,{method:"POST",body:u});if(!p.ok)throw new Error("Cloudinary rejected the image, try a different file.");const m=await p.json(),f=new FormData;f.append("public_id",m.public_id),Object.entries(s).forEach(([h,v])=>{v!=null&&f.append(h,String(v))});const g=await fetch(a,{method:"POST",credentials:"include",headers:r,body:f});if(!g.ok)throw new Error(await _tfResErr(g));return await g.json()}async function _tfUploadMapDirect(e,t,{registerPath:n,fields:a={}}){return _tfUploadDirect(t,{paramsPath:`/api/campaigns/${e}/upload-params`,kind:"map",endpoint:`/api/campaigns/${e}${n}
341`,fields:a,label:"Map image"})}let _pinnedLogEntries=[];function makePin(e,t,n){const a=ENTITY_CFG[e.type]||ENTITY_CFG.location,s=thumbUrl(e.image_crop_url||e.image_url),i=window.MapStyle,o=e.type==="location",r=o&&e.ledger&&e.ledger.kind||null,c=r&&i?i.colorCss(r):a.color,l=o&&i?i.bucketOf(r):"other",d=i?i.pinInnerHtml({kind:o?r:null,portraitUrl:s||""}):`<div class="tf-pin-well">${s?`<img class="tf-pin-img" src="${s}" alt="" />`:""}</div>`,u=["tf-pin","tf-pin--"+l,s?"tf-pin--portrait":"",t?"tf-pin--locked":"",n?"tf-pin--hidden":""].filter(Boolean).join(" ");return L.divIcon({className:"",html:`<div class="${u}" style="--pin-color:${c}">${d}</div>`,iconSize:[32,42],iconAnchor:[16,41],popupAnchor:[0,-40]})}let reextractedLocations=[],_reextractMapId=null;async function _openReextractModal(e){_reextractMapId=e??null,reextractedLocations=[],document.getElementById("reextract-list").innerHTML="",document.getElementById("reextract-error").classList.add("hidden"),document.getElementById("reextract-warning").classList.add("hidden"),document.getElementById("reextract-actions").classList.add("hidden"),document.getElementById("reextract-loading").classList.remove("hidden"),document.getElementById("reextract-modal").classList.remove("hidden");try{const t=e?`/api/campaigns/${currentCampaign.id}/extract-locations?map_id=${e}`:`/api/campaigns/${currentCampaign.id}/extract-locations`,n=await api("POST",t);document.getElementById("reextract-loading").classList.add("hidden"),reextractedLocations=n.locations||[];const a=allEntities.filter(o=>o.type==="location").length,s=document.getElementById("reextract-list");if(reextractedLocations.length===0){s.innerHTML='<p class="placeholder placeholder--padded">No locations found.</p>';return}reextractedLocations.forEach((o,r)=>{const c=document.createElement("label");c.className="location-check-row",c.innerHTML=`
342        <input type="checkbox" name="reloc" value="${r}" checked />
343        <span class="loc-name">${escHtml(o.name)}</span>
344        <span class="loc-coords">(${o.x}, ${o.y})</span>
345      `,c.dataset.loc=JSON.stringify(o),s.appendChild(c)});const i=document.getElementById("reextract-warning");i.textContent="Charts the checked places on this map. Existing places with the same name are updated in place. Nothing is deleted.",i.classList.remove("hidden"),document.getElementById("reextract-actions").classList.remove("hidden")}catch(t){document.getElementById("reextract-loading").classList.add("hidden"),document.getElementById("reextract-error").textContent=t.message,document.getElementById("reextract-error").classList.remove("hidden")}}document.getElementById("reextract-modal-close").addEventListener("click",()=>{document.getElementById("reextract-modal").classList.add("hidden")}),document.getElementById("reextract-cancel-btn").addEventListener("click",()=>{document.getElementById("reextract-modal").classList.add("hidden")}),document.getElementById("reextract-confirm-btn").addEventListener("click",async()=>{const t=[...document.querySelectorAll("#reextract-list input[type=checkbox]:checked")].map(a=>JSON.parse(a.closest("label").dataset.loc)),n=document.getElementById("reextract-error");n.classList.add("hidden"),document.getElementById("reextract-confirm-btn").disabled=!0;try{const a=_reextractMapId??null,s=await api("POST",`/api/campaigns/${currentCampaign.id}/replace-locations`,{locations:t,map_id:a}),i=[...s.created||[],...s.updated||[]],o=new Map(i.map(c=>[c.id,c]));if(allEntities=allEntities.map(c=>o.get(c.id)||c).concat(i.filter(c=>!allEntities.some(l=>l.id===c.id))),typeof showToast=="function"){const c=i.length;let l=`Charted ${c} place${c===1?"":"s"} on this map.`;s.conflicts&&s.conflicts.length&&(l+=` ${s.conflicts.length} already pinned on another map, left untouched.`),showToast(l)}document.getElementById("reextract-modal").classList.add("hidden");const r=document.getElementById("tab-atlas2");if(r&&!r.classList.contains("hidden")&&typeof renderAtlas2=="function"){try{await renderAtlas2({cid:currentCampaign.id,mapId:a||_at2MapId})}catch{}try{await _at2RefreshInbox()}catch{}}}catch(a){n.textContent=a.message,n.classList.remove("hidden")}finally{document.getElementById("reextract-confirm-btn").disabled=!1}});function _renderEntityDescription(e,t,n){if(t!=="pc"){const i=[];for(const o of e.replace(/\*\*/g,"").split(`
346`)){const r=o.match(/^\s*#{2,4}\s+(.+?)\s*$/);if(r){i.push(`<span class="cx2-runin">${escHtml(r[1])}</span>`);continue}o.match(/^\s*#{1,6}\s+/)||i.push(linkifyText(escHtml(o),allEntities,n)+"<br>")}return i.join("").replace(/(<br>)+$/,"")}const a=e.replace(/\[\[/g,"").replace(/\]\]/g,"").split(`
347`),s=[];for(const i of a){const o=i.match(/^##\s+(.+)/);if(o)s.push(`<h4 class="session-beat-header entity-section-header">${escHtml(o[1])}</h4>`);else if(i.trim()==="")s.push("<br>");else{const r=escHtml(i).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");s.push(linkifyText(r,allEntities,n)+"<br>")}}return s.join("")}function _pickInsightfulSentence(e){if(!e)return"";const t=String(e).trim();if(!t)return"";const n=/\b(St|Mr|Mrs|Ms|Dr|Prof|Jr|Sr|vs|etc|Corp|Inc|Ltd|Rev|Gen|Sgt|Cpt|Lt|Col|Ft|Mt|Pt|Dept|Est|Approx)\./g,a=/\b([A-Za-z]\.){2,}/g,s=t.replace(/^#{1,6}\s+.+$/gm,"").replace(/\*\*/g,"").trim();if(!s)return"";const o=s.replace(n,"$1\0").replace(a,l=>l.replace(/\./g,"\0")).split(/(?<=[.!?])\s+/).map(l=>l.replace(/\x00/g,".").trim()).filter(l=>l.length>12);if(!o.length)return s;const r=o.filter(l=>l.length>=80&&l.length<=250);return(r.length?r:o).reduce((l,d)=>d.length>l.length?d:l)}function _bardicFlavorHtml(e){return escHtml(String(e||"")).replace(/~~([\s\S]+?)~~/g,'<s class="bardic-strike">$1</s>').replace(/\n/g,"<br>")}function _bardicByline(e){const t=(currentCampaign&&currentCampaign.bardic_companion_name||"").trim(),n=(currentCampaign&&currentCampaign.party_name||"").trim(),a=n?/s$/i.test(n)?n+"\u2019":n+"\u2019s":"";return t&&n?`${t}, ${a} Bardic Companion`:t?`${t}, Bardic Companion`:n?`${a} Bardic Companion`:e||"Bardic Companion"}function _cx2BardicCycle(e,t){t=t||"flavor";const n=t==="take"?"bardic_take_cycle":"bardic_cycle",a=t==="take"?"bardic_take":"bardic_flavor";let s=e&&e[n];if(typeof s=="string")try{s=JSON.parse(s)}catch{s=null}let i=s&&Array.isArray(s.entries)?s.entries.filter(c=>c&&typeof c.t=="string"):[],o=s&&Number.isInteger(s.idx)?s.idx:0,r=!!(s&&s.locked);return i.length||(e&&e[a]||"").trim()&&(i=[{t:e[a],src:"gm"}],o=0,r=!0),(o<0||o>=i.length)&&(o=Math.max(0,i.length-1)),{entries:i,idx:o,locked:r,count:i.length}}async function _codex2BardicCycleAction(e,t,n,a){const s=allEntities.find(i=>i.id===e);try{const i=await api("POST",`/api/campaigns/${currentCampaign.id}/entities/${e}/bardic-cycle`,{action:t,text:n,field:a||"flavor"});s&&i&&typeof i=="object"&&Object.assign(s,i)}catch(i){typeof showToast=="function"&&showToast("\u26A0 Bard\u2019s Take didn\u2019t update: "+(i&&i.message||"try again"))}_cx2RerenderEntry(e)}
347function renderSessionSummary(e,t,n,a,s){const o=e.replace(/\[\[/g,"").replace(/\]\]/g,"").split(`
348`),r=o.findIndex(A=>/^##\s+/.test(A)),c=A=>{let x=escHtml(A).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");return x=x.replace(/\[(\d{1,3})\]/g,'<sup class="tf-cite" data-cite-id="$1">$1</sup>'),linkifyText(x,t,null)};let l=r>0?o.slice(0,r).map(A=>A.trim()).filter(Boolean):[];if(!l.length&&a){const A=_pickInsightfulSentence(a);A&&(l=[A])}const d=[];if(r>=0){let A=null;for(let x=r;x<o.length;x++){const M=o[x].match(/^##\s+(.+)/);M?(A&&d.push(A),A={title:M[1],bodyLines:[]}):A&&A.bodyLines.push(o[x])}A&&d.push(A)}else d.push({title:null,bodyLines:o});const u=(Array.isArray(n)?n:[]).filter(A=>!(s&&A&&A.id!=null&&A.id===s.quote_id)),p=A=>(A||"").toLowerCase().replace(/['"`‘’ʼ“”]/g,"").replace(/\s+/g," ").trim(),m=d.map(A=>p((A.title||"")+" "+A.bodyLines.join(" "))),f=[];u.forEach((A,x)=>{if(!A||!A.quote)return;const M=p(A.speaker),D=M.split(" ")[0],O=p(A.quote),N=O.length>=12?O.slice(0,30):"";m.forEach((G,F)=>{let W=0;M.length>2&&G.includes(M)?W+=3:D.length>2&&G.includes(D)&&(W+=2),N&&G.includes(N)&&(W+=5),W>0&&f.push({qIdx:x,bIdx:F,score:W})})}),f.sort((A,x)=>x.score-A.score);const g=d.map(()=>null),h=new Set,v=new Set,b=3;let y=0;const _=A=>A&&A.speaker_id!=null?`id:${A.speaker_id}`:(p(A&&A.speaker)||"").split(/\s+/)[0]||"__none__",w=80,$=300,E=d.map(A=>A.bodyLines.join(" ").trim().split(/\s+/).filter(Boolean).length);let k=0;const C=E.map(A=>k+=A),S=k,q=(A,x)=>{let M=null;for(const D of f){if(g[D.bIdx]||h.has(D.qIdx))continue;const O=C[D.bIdx];if(!(O<A||O>x)&&(M||(M=D),!v.has(_(u[D.qIdx]))))return D}return M},I=A=>{A&&(g[A.bIdx]=u[A.qIdx],h.add(A.qIdx),v.add(_(u[A.qIdx])),y++)};if(S<$)I(q(0,S));else{const A=S-w,x=Math.min(b,Math.floor(S/$));for(let M=0;M<x;M++)I(q(M/x*A,(M+1)/x*A))}const T=(A,x)=>{const M=(A.speaker||"").trim(),D=M?`<span class="tf-pullquote-attr">\u2014 ${escHtml(M)}</span>`:"",O=(A.context||"").trim(),N=O?`<span class="tf-pullquote-ctx">${escHtml(O)}</span>`:"",G=`"${(A.quote||"").trim()}"${M?" \u2014 "+M:""}${O?" ("+O+")":""}`,F=x?'<span class="tf-qotn-crown">\u{1F3C6} Quote of the Night</span>':"",W=x&&A.id!=null?`<button type="button" class="tf-qotn-cardlink" data-qc-open="${A.id}">View the quote card \u2192</button>`:"";return`<div class="tf-pullquote${x?" tf-pullquote--qotn":""}"${A.id!=null?` data-quote-id="${A.id}"`:""}>${F}<span class="tf-pullquote-text">${escHtml(A.quote)}</span>${D}${N}${W}<button type="button" class="tf-quote-share" data-copy="${escHtml(G)}" title="Share this quote" aria-label="Share quote"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.3 13.4l7.4 4.2M15.7 6.4l-7.4 4.2"/></svg></button><button type="button" class="tf-quote-copy" data-copy="${escHtml(G)}" title="Copy this quote" aria-label="Copy quote">\u29C9</button>${A.id!=null?_p2ReactionBar({id:A.id}):""}</div>`},P=[];if(l.length){const A=l.map(c).join("<br>");P.push(`<div class="tf-recap-tldr">${A}</div>`)}const B=A=>A.replace(/^(\s*)<span class="entity-link"([^>]*)>([^<&])/,'$1$3<span class="entity-link"$2>');let H=!1;return d.forEach((A,x)=>{A.title&&P.push(`<h4 class="session-beat-header">${escHtml(A.title)}</h4>`);let M=!1;A.bodyLines.forEach(D=>{!M&&!D.trim()||(M=!0,!H&&D.trim()?(H=!0,P.push(`<p class="chapter-lead">${B(c(D))}</p>`)):P.push(c(D)+"<br>"))}),g[x]&&P.push(T(g[x]))}),s&&(s.quote||"").trim()&&P.push(T({id:s.quote_id,quote:s.quote,speaker:s.speaker,context:s.context},!0)),P.join("")}let _ltCache={ref:null,excludeMap:new Map};function linkifyText(e,t,n){if(!e||!t||!t.length||!_isGm()&&t.some(c=>c&&c.pending_reveal)&&(t=t.filter(c=>!_hiddenFromViewer(c)).map(_teaseView),!t.length))return e;_ltCache.ref!==t&&(_ltCache={ref:t,excludeMap:new Map});const a=n??null;let s=_ltCache.excludeMap.get(a);if(s&&(_ltCache.excludeMap.delete(a),_ltCache.excludeMap.set(a,s)),!s){const c="(?:'|&#39;|&apos;
348|\u2018|\u2019|\u02BC)",l=g=>g.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/['\u2018\u2019\u02bc]/g,c),d=[];for(const g of t){if(g.id===n||!g.name||!g.name.trim())continue;const h=g.name.trim(),v=h.replace(/\s*\(.*$/,"").trim(),b=new Set([h]);v&&v!==h&&b.add(v),Array.isArray(g.aliases)&&g.aliases.forEach(y=>{y&&y.trim().length>1&&b.add(y.trim())});for(const y of b)d.push({variant:y,entity:g})}if(!d.length)return e;d.sort((g,h)=>h.variant.length-g.variant.length);const u=g=>g.toLowerCase().replace(/&#39;/g,"'").replace(/&apos;/g,"'").replace(/[\u2018\u2019\u02bc]/g,"'"),p=new Map;for(const{variant:g,entity:h}of d){const v=u(g);p.has(v)?h.type==="pc"&&p.get(v).entity.type!=="pc"&&p.set(v,{variant:g,entity:h}):p.set(v,{variant:g,entity:h})}const m=[...p.values()].sort((g,h)=>h.variant.length-g.variant.length).map(({variant:g})=>l(g)),f=new RegExp(`(?<![\\w>])(${m.join("|")})(?![\\w<])`,"gi");for(s={variantMap:p,combinedRe:f,normalizeKey:u},_ltCache.excludeMap.set(a,s);_ltCache.excludeMap.size>16;)_ltCache.excludeMap.delete(_ltCache.excludeMap.keys().next().value)}const{variantMap:i,combinedRe:o,normalizeKey:r}=s;return e.replace(o,c=>{const l=i.get(r(c));if(!l)return c;const d=l.entity;return`<span class="entity-link" data-id="${d.id}" data-type="${d.type}" tabindex="0">${c}</span>`})}function _showEntityPreview(e){const t=_teaseView(allEntities.find(m=>m.id===e));if(!t||_hiddenFromViewer(t))return;_trackEntityView(e);const n=ENTITY_CFG[t.type]||ENTITY_CFG.location,a=thumbUrl(_entityImageUrl(t)),s=document.getElementById("entity-preview-drawer");if(!s)return;const i=document.getElementById("epd-icon");if(i)if(a)i.innerHTML=`<img src="${escHtml(a)}" alt="" class="epd-icon-img" />`;else{const m=new Set(["npc","location","faction","item","spell","deity","pc","companion","lore"]),f=` style="--entity-color:${n.color}"`;m.has(t.type)?i.innerHTML=`<span class="epd-glyph-badge"${f}><svg class="epd-glyph" viewBox="0 0 24 24"><use href="#tf-glyph-${t.type}"/></svg></span>`:i.innerHTML=`<span class="epd-glyph-badge epd-glyph-badge--emoji"${f}>${n.icon||"?"}</span>`}const o=document.getElementById("epd-name");o&&(o.textContent=t.name);const r=document.getElementById("epd-badge");r&&(r.textContent=n.label,r.style.color=n.color);const c=_sessionCtx(),l=_buildEntityContextHeader(t),d=t.description?linkifyText(escHtml(t.description.replace(/^#{1,6}\s+.+\n?/gm,"").trim()).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\n/g,"<br>"),allEntities,t.id):'<em style="color:var(--text-3)">No description.</em>',u=document.getElementById("epd-content");if(u){const m=_entityImageUrl(t);u.innerHTML=`
349      ${m?`<img src="${escHtml(m)}" class="epd-portrait" alt="${escHtml(t.name)}" />`:""}
350      ${l}
351      ${_spellMetaPills(t)}${_itemMetaPills(t)}
352      <p class="entity-desc" style="margin-top:0.5rem">${d}</p>`}const p=document.getElementById("epd-open-full");p&&(p.onclick=()=>{_hideEntityPreview(),navigateToEntity(e)}),s.classList.remove("hidden")}function _hideEntityPreview(){document.getElementById("entity-preview-drawer")?.classList.add("hidden")}function navigateToEntity(e){if(typeof renderCodexEntry=="function"&&(allEntities||[]).some(n=>n.id===e)){try{_trackEntityView(e)}catch{}renderCodexEntry(e);return}expandedEntityId=e;const t=allEntities.find(n=>n.id===e);t&&compendiumTypeFilter&&compendiumTypeFilter!==t.type&&(compendiumTypeFilter="",document.querySelectorAll(".type-filter-btn").forEach(n=>{n.classList.toggle("active",n.dataset.type==="")})),document.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),document.querySelectorAll(".tab-panel").forEach(n=>n.classList.add("hidden")),document.querySelector('[data-tab="compendium"]').classList.add("active"),document.getElementById("tab-compendium").classList.remove("hidden"),_scrollActiveTabIntoView(),renderCompendium(),requestAnimationFrame(()=>{const n=document.querySelector(`.entity-row[data-id="${e}"]`);n&&n.scrollIntoView({behavior:"smooth",block:"center"})})}let _codex2RenderCache={cid:null,byId:{}};function _codex2DeOps(e){if(!e)return e;const t=String(e).trim();if(!(t.startsWith("```")||t.startsWith('{"ops"')||t.charAt(0)==="{"&&t.indexOf('"ops"')>-1&&t.indexOf('"ops"')<200))return e;const a=[],s=/"(?:replace|text|find)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;let i;for(;i=s.exec(t);)a.push(i[1]);const o=t.match(/"(?:replace|text|find)"\s*:\s*"((?:[^"\\]|\\.)*)$/);o&&a.push(o[1]);const r=a.map(c=>
352c.replace(/\\"/g,'"').replace(/\\n/g," ").replace(/\\t/g," ").replace(/\\\\/g,"\\").trim()).filter(c=>c.length>20);return r.length?(r.sort((c,l)=>l.length-c.length),r[0]):""}function _codex2RenderProse(e,t){e=_codex2DeOps(e);const n=[];let a=[];const s=()=>{a.length&&(n.push(`<p>${linkifyText(escHtml(a.join(" ")).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>"),allEntities,t)}</p>`),a=[])};return(e||"").split(/\n/).forEach(i=>{const o=i.match(/^\s*#{2,4}\s+(.+?)\s*$/);o?(s(),n.push(`<span class="cx2-runin">${escHtml(o[1])}</span>`)):i.trim()?a.push(i.trim()):s()}),s(),n.join("")||'<p class="placeholder">No chronicle recorded yet.</p>'}let _codex2Premium=!1;async function _codex2GetSpec(e,t){if(_codex2RenderCache.cid!==e){_codex2RenderCache={cid:e,byId:{}};try{const n=await api("GET",`/api/campaigns/${e}/codex/render`);_codex2Premium=!!(n&&n.premium),(n.items||[]).forEach(a=>{_codex2RenderCache.byId[a.entity_id]=a})}catch{}}return _codex2RenderCache.byId[t]||null}let _chr2PlacementSpecs={key:null,byId:{},premium:!1};async function _chr2GetPlacementSpecs(e,t,n){const a=e+":"+t;if(n||_chr2PlacementSpecs.key!==a)try{const s=await api("GET",`/api/campaigns/${e}/sessions/${t}/inline/render`),i={key:a,byId:{},premium:!!(s&&s.premium)};(s.items||[]).forEach(o=>{i.byId[o.entity_id]=o}),_chr2PlacementSpecs=i}catch{_chr2PlacementSpecs.key!==a&&(_chr2PlacementSpecs={key:a,byId:{},premium:!1})}return _chr2PlacementSpecs.byId}function _chr2InvalidatePlacementSpecs(){_chr2PlacementSpecs={key:null,byId:{},premium:!1}}function _tfSpinnerSvg(){return'<span class="tf-spinner" aria-hidden="true"><svg viewBox="0 0 556 580" preserveAspectRatio="xMidYMin meet"><use href="#tf-mark"/><g class="tf-thread-g tf-thread--a"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--b"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--c"><line x1="322" y1="210" x2="322" y2="270"/></g></svg></span>'}function _cx2BakeOwed(e){if(!e)return!1;const t=e.baked_treatment;return t!=="dissolve"&&t!=="cutout"&&t!=="spot"||e.bake_status==="failed"||e.bake_status==="excluded"?!1:e.bake_status==="pending"||e.bake_status==="processing"||e.crop&&!e.crop_url?!0:!(e.has_matte&&(t!=="dissolve"||e.has_dist))}function _cx2BakeOverlayHtml(e){const t=e==="dissolve"?"Weaving the dissolve":"Cutting the figure free";return'<div class="cx2-bake-overlay">'+_tfSpinnerSvg()+'<span class="cx2-bake-label">'+t+"\u2026</span></div>"}function _cx2BakeFailTip(e){return"The "+(e==="dissolve"?"dissolve":e==="spot"?"spot treatment":"cutout")+" didn\u2019t finish, so this is your untreated image. Open the image editor and choose the treatment again to retry."}let _cx2BakeWatchers={};function _cx2BakeWatch(e,t,n){const a=n&&n.sessionId||null,s=e+":"+t+(a?":s"+a:"");if(_cx2BakeWatchers[s])return;const i=Date.now(),o=15*60*1e3,r=async()=>{const c=(location.hash||"")===`#codex/${e}/${t}`,l=/^#chronicle\//.test(location.hash||""),d=document.getElementById("tab-mychar"),u=!!(d&&!d.classList.contains("hidden")&&_myCharSelId===t);if(!(a?l:c||l||u)){delete _cx2BakeWatchers[s];return}if(Date.now()-i>o){delete _cx2BakeWatchers[s];return}try{let m;if(a?m=(await _chr2GetPlacementSpecs(e,a,!0))[t]||null:(_codex2RenderCache.cid=null,m=await _codex2GetSpec(e,t)),!_cx2BakeOwed(m)){if(u&&!c&&typeof _myCharEditing=="function"&&_myCharEditing()){_cx2BakeWatchers[s]=setTimeout(r,8e3);return}if(delete _cx2BakeWatchers[s],a||_codex2RerenderIfOpen(),_chr2RerenderIfOpen(),u)try{renderMyCharacterTab({keepScroll:!0})}catch{}m&&m.has_matte&&typeof showToast=="function"?showToast(m.baked_treatment==="dissolve"?"\u2726 Dissolve woven":"\u2726 Cutout ready"):m&&m.bake_status==="failed"&&typeof showToast=="function"&&showToast(_cx2BakeFailTip(m.baked_treatment),7e3);return}}catch{}_cx2BakeWatchers[s]=setTimeout(r,8e3)};_cx2BakeWatchers[s]=setTimeout(r,8e3)}function _codex2Panel(){let e=document.getElementById("tab-codex2");if(!e){e=document.createElement("div"),e.id="tab-codex2",e.className="tab-panel hidden",e.setAttribute("role","tabpanel");const t=document.getElementById("tab-compendium");t.parentNode.insertBefore(e,t.nextSibling)}return e}function _codex2Back(){(location.hash||"").startsWith("#codex/")&&history.replaceState(null,"",location.pathname+location.search),_codex2Panel().classList.add("hidden"),document.querySelectorAll(".tab-btn").forEach(e
352=>e.classList.remove("active")),document.querySelector('[data-tab="compendium"]')?.classList.add("active"),document.getElementById("tab-compendium")?.classList.remove("hidden");try{renderCompendium()}catch{}}function _codex2RerenderIfOpen(e){try{const t=document.getElementById("tab-codex2");if(!t||t.classList.contains("hidden"))return;const n=(location.hash||"").match(/^#codex\/(\d+)\/(\d+)$/);if(!n)return;const a=+n[2];e&&_codex2RenderCache&&_codex2RenderCache.byId&&delete _codex2RenderCache.byId[e],(allEntities||[]).some(s=>s.id===a)?renderCodexEntry(a):_codex2Back()}catch{}}function _codex2SetMatteShape(e,t,n){if(!e||!t)return;n=n||"right";const a=new Image;a.crossOrigin="anonymous",a.onload=function(){try{const s=a.naturalWidth,i=a.naturalHeight;if(!s||!i)return;const o=512,r=Math.min(1,o/Math.max(s,i)),c=Math.max(1,Math.round(s*r)),l=Math.max(1,Math.round(i*r)),d=document.createElement("canvas");d.width=c,d.height=l;const u=d.getContext("2d");u.drawImage(a,0,0,c,l);const p=u.getImageData(0,0,c,l).data,m=40,f=[];for(let v=0;v<=m;v++){const b=Math.min(l-1,Math.round(v/m*(l-1)));let y;if(n==="left"){y=1;for(let _=0;_<c;_++)if(p[(b*c+_)*4+3]>128){y=_/c;break}}else{y=0;for(let _=c-1;_>=0;_--)if(p[(b*c+_)*4+3]>128){y=(_+1)/c;break}}f.push(y)}const g=.03;let h;n==="left"?h="polygon(100% 0%, "+f.map((b,y)=>{const _=Math.min(f[Math.max(0,y-1)],b,f[Math.min(m,y+1)]);return(Math.max(0,_-g)*100).toFixed(1)+"% "+(y/m*100).toFixed(1)+"%"}).join(", ")+", 100% 100%)":h="polygon(0% 0%, "+f.map((b,y)=>{const _=Math.max(f[Math.max(0,y-1)],b,f[Math.min(m,y+1)]);return(Math.min(1,_+g)*100).toFixed(1)+"% "+(y/m*100).toFixed(1)+"%"}).join(", ")+", 0% 100%)",e.style.shapeOutside=h,e.style.shapeMargin="14px"}catch{}},a.src=t}function _codex2SetGeodesicShape(e,t,n,a){if(!e||!t)return;a=a||"right";const s=(n!=null?+n:.13)*255,i=new Image;i.crossOrigin="anonymous",i.onload=function(){try{const o=i.naturalWidth,r=i.naturalHeight;if(!o||!r)return;const c=512,l=Math.min(1,c/Math.max(o,r)),d=Math.max(1,Math.round(o*l)),u=Math.max(1,Math.round(r*l)),p=document.createElement("canvas");p.width=d,p.height=u;const m=p.getContext("2d");m.drawImage(i,0,0,d,u);const f=m.getImageData(0,0,d,u).data,g=40,h=[];for(let y=0;y<=g;y++){const _=Math.min(u-1,Math.round(y/g*(u-1)));let w;if(a==="left"){w=1;for(let $=0;$<d;$++)if(f[(_*d+$)*4]>s){w=$/d;break}}else{w=0;for(let $=d-1;$>=0;$--)if(f[(_*d+$)*4]>s){w=($+1)/d;break}}h.push(w)}const v=.02;let b;a==="left"?b="polygon(100% 0%, "+h.map((_,w)=>{const $=Math.min(h[Math.max(0,w-1)],_,h[Math.min(g,w+1)]);return(Math.max(0,$-v)*100).toFixed(1)+"% "+(w/g*100).toFixed(1)+"%"}).join(", ")+", 100% 100%)":b="polygon(0% 0%, "+h.map((_,w)=>{const $=Math.max(h[Math.max(0,w-1)],_,h[Math.min(g,w+1)]);return(Math.min(1,$+v)*100).toFixed(1)+"% "+(w/g*100).toFixed(1)+"%"}).join(", ")+", 0% 100%)",e.style.shapeOutside=b,e.style.shapeMargin="14px"}catch{}},i.src=t}const _CX2_TC={pc:"pc",companion:"pc",npc:"npc",location:"loc",faction:"faction",item:"item",spell:"item",deity:"lore",lore:"lore"},_CX2_GL={pc:"pc",companion:"companion",npc:"npc",location:"location",faction:"faction",item:"item",spell:"spell",deity:"deity",lore:"lore"};function _codex2EnsureShell(){const e=_codex2Panel();return e.querySelector(".cx2-shell")||(e.innerHTML='<div class="cx2-shell"><aside class="cx2-rail"></aside><div class="cx2-right"></div></div>'),e}function _codex2ApplyRailFilter(e){const t=e.querySelector(".cx2-rail-search"),n=(t&&t.value||"").toLowerCase(),a=e._type||"",s=e.querySelector(".cx2-rail-list");if(!s)return;const i=[...s.children];i.forEach(o=>{if(o.classList.contains("cx2-rail-item")){const r=!a||(a==="bardic"?o.dataset.bardic==="1":o.dataset.type===a);o.style.display=(!n||(o.dataset.name||"").toLowerCase().includes(n))&&r?"":"none"}});for(let o=0;o<i.length;o++){if(!i[o].classList.contains("cx2-rail-group"))continue;if(i[o].classList.contains("cx2-rail-group--toggle")&&i[o].classList.contains("is-collapsed")){i[o].style.display="";continue}let r=!1;for(let c=o+1;c<i.length&&!i[c].classList.contains("cx2-rail-group");c++)if(i[c].classList.contains("cx2-rail-item")&&i[c].style.display!=="none"){r=!0;break}i[o].style.display=r?"":"none"}}function _codex2LatestSession(){const e=(allEntities||[]).map(s=>s.last_seen_session_id).filter(s=>s!=null);if(!e.length)return null;const t=Math.max.apply(null,e);let n=null,a=null;try{if(typeof allSessions<"u"&&Array.isArray(allSessions)){const s=allSessions.slice().sort((o,r)=>(o.date||"").localeCompare(r.date||"")||o.id-r.id),i=s.findIndex(o=>o.id===t);i>=0&&(n=s[i].number!=null?s[i].number:i+1,a=s[i].title)}}catch{}return{id:t,num:n,title:a}}function _codex2InDegree(){const e=document.getElementById("tab-codex2"),t=currentCampaign&&currentCampaign.id;if(e&&e._cx2IndegCid===t&&e._cx2Indeg)return e._cx2Indeg;const n={};
352try{(allEntities||[]).forEach(a=>{const s=linkifyText(escHtml(a.description||""),allEntities,null),i=/data-id="(\d+)"/g;let o;const r=new Set;for(;o=i.exec(s);){const c=+o[1];c&&c!==a.id&&!r.has(c)&&(r.add(c),n[c]=(n[c]||0)+1)}})}catch{}return e&&(e._cx2Indeg=n,e._cx2IndegCid=t),n}function _codex2RailSetActive(e){const t=document.querySelector("#tab-codex2 .cx2-rail");if(!t)return;t.querySelectorAll(".cx2-rail-item").forEach(a=>a.classList.toggle("active",parseInt(a.dataset.eid)===e));const n=t.querySelector(".cx2-rail-item.active");n&&n.scrollIntoView({block:"nearest"})}const _REVEAL_EYE_SVG='<svg class="tf-reveal-eye" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 3.5c-3.2 0-5.9 2-6.8 4.5C2.1 10.5 4.8 12.5 8 12.5s5.9-2 6.8-4.5C13.9 5.5 11.2 3.5 8 3.5zm0 7.3a2.8 2.8 0 110-5.6 2.8 2.8 0 010 5.6z"/><path stroke="currentColor" stroke-width="1.4" stroke-linecap="round" d="M3 13.2 13 2.8"/></svg>';function _revealTagHtml(e){if(!e||!e.pending_reveal)return"";try{if(typeof _isGm=="function"&&!_isGm())return""}catch{}const t=_teaseSummary(e),n=t?"Unrevealed: staged for players. "+t+". They see that much now; reveal it to share the rest":"Unrevealed: only you (the GM) can see this; reveal it to share with players";return'<span class="tf-badge tf-badge--gold tf-reveal-tag" title="'+escHtml(n)+'">'+_REVEAL_EYE_SVG+"Unrevealed</span>"}function _hiddenFromViewer(e){return!!(e&&e.pending_reveal)&&!_isGm()&&!_teaseAny(e)}function _teaseAny(e){return!!(e&&(e.tease_position||e.tease_name||e.tease_photo))}function _teaseAlias(e){return(e&&e.tease_alias!=null?String(e.tease_alias).trim():"")||"Unknown"}function _teaseMasked(e){return!!(e&&e.pending_reveal)&&!_isGm()&&_teaseAny(e)}function _teaseScrub(e,t,n){if(!e||typeof e!="string")return e;const a=[],s=(t.name||"").trim();if(s.length>1){a.push(s);const i=s.replace(/\s*\(.*$/,"").trim();i.length>1&&i!==s&&a.push(i)}if((Array.isArray(t.aliases)?t.aliases:[]).forEach(i=>{i=(i||"").trim(),i.length>1&&a.push(i)}),!a.length)return e;a.sort((i,o)=>o.length-i.length);try{const i=new RegExp("(?<![\\w])(?:"+a.map(o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|")+")(?![\\w])","gi");return e.replace(i,n)}catch{return e}}function _teaseView(e){if(!e||!_teaseMasked(e)||e.reveal_masked)return e;const t=_teaseAlias(e),n={...e,reveal_masked:!0,tease_position:!!e.tease_position,tease_name:!!e.tease_name,tease_photo:!!e.tease_photo};if(delete n.tease_alias,delete n.pending_reveal_by,delete n.pending_reveal_private,!e.tease_name&&(n.name=t,n.aliases=[],n.player_name=null,n.dnd_beyond_data=null,n.dnd_beyond_url=null,["description","snippet","bardic_flavor","bardic_take"].forEach(a=>{n[a]&&(n[a]=_teaseScrub(n[a],e,t))}),n.ledger&&typeof n.ledger=="object")){const a={};Object.keys(n.ledger).forEach(s=>{const i=n.ledger[s];a[s]=typeof i=="string"?_teaseScrub(i,e,t):i}),n.ledger=a}if(e.tease_photo||(n.image_url=null,n.image_urls=[],n.image_crop_url=null,n.thumb_matte_url=null,n.image_source=null),!e.tease_position&&n.ledger&&typeof n.ledger=="object"){const a={...n.ledger};delete a.affiliation,delete a.leader,n.ledger=a}return n}function _teaseParts(e){if(!e||!e.pending_reveal||!_teaseAny(e))return[];const t=[e.tease_name?"true name":"known as "+_teaseAlias(e)];return e.tease_position&&t.push("position"),e.tease_photo&&t.push("portrait"),t}function _teaseSummary(e){const t=_teaseParts(e);return t.length?"Teasing "+t.join(", "):""}const _TF_SIL_SVG='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 21c1.2-4.2 4.1-6.3 7.5-6.3s6.3 2.1 7.5 6.3"/></svg>';function _tfSilhouetteHtml(e){return'<span class="tf-silhouette'+(e&&e.tease_position?" tf-silhouette--tinted":"")+'" aria-hidden="true">'+_TF_SIL_SVG+"</span>"}function _codex2RailItemHtml(e,t,n){const a=_CX2_TC[e.type]||"npc",s=_CX2_GL[e.type]||"npc";let i=(e.snippet||"").trim();if(e.type==="companion"&&e.parent_entity_id){const p=(allEntities||[]).find(m=>m.id===e.parent_entity_id);p&&p.name&&(i="Bonded to "+p.name+(i?" \xB7 "+i:""))}const o=typeof _entityImageUrl=="function"?_entityImageUrl(e):e.image_url,r=ENTITY_CFG[e.type]||ENTITY_CFG.npc,c='<sp
352an class="tf-badge tf-badge--ghost cx2-ri-type">'+escHtml(r.label||e.type||"")+"</span>",l=_hasBardicTake(e),d=!!(e.reveal_masked&&!e.tease_photo),u=!!(e.reveal_masked&&!e.tease_name);return'<div class="cx2-rail-item'+(e.id===t?" active":"")+(e.reveal_masked?" is-masked":"")+'" data-eid="'+e.id+'" data-name="'+escHtml(e.name||"")+'" data-type="'+escHtml(e.type||"")+'" data-bardic="'+(l?"1":"")+'"><div class="cx2-ri-thumb '+a+(o?" has-img":"")+(e.thumb_matte_url?" is-cutout":"")+(d?" cx2-ri-thumb--sil":"")+'">'+(d?_tfSilhouetteHtml(e):o?'<img src="'+escHtml(thumbUrl(e.thumb_matte_url||o)||o)+'" alt="" loading="lazy">':'<svg aria-hidden="true"><use href="#tf-glyph-'+s+'"/></svg>')+'</div><div style="min-width:0"><div class="cx2-ri-name'+(u?" cx2-ri-name--alias":"")+'">'+escHtml(e.name||"")+"</div>"+(i?'<div class="cx2-ri-meta">'+escHtml(i)+"</div>":"")+'</div><span class="cx2-ri-end">'+_revealTagHtml(e)+(l?_BARDIC_QUILL_IND:"")+c+"</span></div>"}function _codex2IsJunkFaction(e){if(!e||e.type!=="faction")return!1;const t=(e.name||"").trim();if(!t)return!1;const n=!!(e.description&&e.description.trim()),a=!!(e.image_url&&String(e.image_url).trim());return t===t.toLowerCase()&&!n&&!a}function _cx2RecentCollapsed(){const e=localStorage.getItem("tf_cx2_recent_collapsed");return e==="1"?!0:e==="0"?!1:window.innerWidth<=768}function _cx2SetRecentCollapsed(e){try{localStorage.setItem("tf_cx2_recent_collapsed",e?"1":"0")}catch{}}function _codex2RenderRail(e){const t=document.querySelector("#tab-codex2 .cx2-rail");if(!t)return;const n=typeof _isGm=="function"&&_isGm(),a=(allEntities||[]).filter(h=>!_codex2IsJunkFaction(h)&&(n||!_hiddenFromViewer(h))).map(_teaseView),s=(h,v)=>sortKey(h.name).localeCompare(sortKey(v.name)),i={};a.forEach(h=>{h.type&&(i[h.type]=(i[h.type]||0)+1)});const o=Object.keys(i).sort((h,v)=>i[v]-i[h]),r='<button class="cx2-rchip'+(t._type?"":" on")+'" data-type="">All<span class="cx2-rn">'+a.length+"</span></button>"+o.map(h=>'<button class="cx2-rchip'+(t._type===h?" on":"")+'" data-type="'+escHtml(h)+'">'+escHtml(h)+'<span class="cx2-rn">'+i[h]+"</span></button>").join(""),c=a.filter(_hasBardicTake).length,l=c?'<button class="cx2-rchip'+(t._type==="bardic"?" on":"")+'" data-type="bardic" title="Only entries with the Bard\u2019s Take">\u2712 Bard\u2019s Take<span class="cx2-rn">'+c+"</span></button>":"",d=_codex2LatestSession(),u=d?new Set(a.filter(h=>h.last_seen_session_id===d.id).map(h=>h.id)):new Set;let p="";if(u.size){const h=d&&d.num!=null?"in Session "+d.num:d&&d.title?escHtml(d.title):"latest session",v=_cx2RecentCollapsed();p+='<div class="cx2-rail-group cx2-rail-group--toggle'+(v?" is-collapsed":"")+'" data-group="recent" role="button" tabindex="0" aria-expanded="'+(v?"false":"true")+'" title="'+(v?"Show recently mentioned":"Hide recently mentioned")+'"><span class="cx2-rg-left"><span class="cx2-rg-chev" aria-hidden="true">'+(v?"\u25B8":"\u25BE")+'</span>Recently mentioned</span><span class="cx2-rgcount">'+h+"</span></div>",v||(p+=a.filter(b=>u.has(b.id)).sort(s).map(b=>_codex2RailItemHtml(b,e,!0)).join(""))}p+='<div class="cx2-rail-group" data-group="all"><span>All</span><span class="cx2-rgcount">'+a.length+"</span></div>"+a.slice().sort(s).map(h=>_codex2RailItemHtml(h,e,!1)).join(""),t.innerHTML='<div class="cx2-rail-head"><div class="cx2-rail-eyebrow">Codex &middot; '+a.length+' entries</div><input class="cx2-rail-search" data-clearable placeholder="Search the codex\u2026"><div class="cx2-rail-chips">'+r+l+'</div></div><div class="cx2-rail-list">'+p+"</div>";const m=t.querySelector(".cx2-rail-search");t._q&&(m.value=t._q),m.oninput=()=>{t._q=m.value,_codex2ApplyRailFilter(t)},typeof _ensureSearchClears=="function"&&_ensureSearchClears(),t.querySelectorAll(".cx2-rchip[data-type]").forEach(h=>h.onclick=()=>{t._type=h.dataset.type,t.querySelectorAll(".cx2-rchip[data-type]").forEach(v=>v.classList.toggle("on",v===h)),_codex2ApplyRailFilter(t)}),t.querySelectorAll(".cx2-rail-item").forEach(h=>h.onclick=()=>{const v=parseInt(h.dataset.eid);v&&renderCodexEntry(v)});const f=t.querySelector('.cx2-rail-group--toggle[data-group="recent"]');
352if(f){const h=()=>{_cx2SetRecentCollapsed(!_cx2RecentCollapsed()),_codex2RenderRail(e)};f.onclick=h,f.onkeydown=v=>{(v.key==="Enter"||v.key===" ")&&(v.preventDefault(),h())}}_codex2ApplyRailFilter(t);const g=t.querySelector(".cx2-rail-item.active");g&&g.scrollIntoView({block:"nearest"})}function _codex2IsGm(){try{return["gm","co_gm"].includes(currentRole)}catch{return!1}}function _codex2OwnsEntity(e){try{if(!e||!currentUser||!currentUser.email)return!1;let t=e;if(e.type==="companion"&&e.parent_entity_id&&(t=(allEntities||[]).find(a=>a.id===e.parent_entity_id)||e),t.type!=="pc")return!1;const n=(_rosterPcs||[]).find(a=>a.entity_id===t.id);return!!(n&&n.assigned_email&&n.assigned_email===currentUser.email)}catch{return!1}}function _codex2CanEdit(e){try{if(currentUser&&currentUser.is_admin)return!0}catch{}return _isReadOnlyRole()?!1:_codex2IsGm()||_codex2OwnsEntity(e)}function _cx2RerenderEntry(e){const t=document.getElementById("tab-mychar");if(t&&!t.classList.contains("hidden")&&_myCharSelId===e&&typeof renderMyCharacterTab=="function"){renderMyCharacterTab({keepScroll:!0});return}renderCodexEntry(e)}async function _codex2SaveField(e,t){const n=allEntities.find(i=>i.id===e),a=n&&n.description||null;let s=!1;try{const i=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${e}`,t);n&&Object.assign(n,i&&typeof i=="object"?i:t),s=!0}catch(i){n&&Object.assign(n,t),typeof showToast=="function"&&showToast("\u26A0 Change didn\u2019t save: "+(i&&i.message||"try again"))}_cx2RerenderEntry(e),s&&t&&typeof t.description=="string"&&api("POST",`/api/campaigns/${currentCampaign.id}/entities/${e}/sync-crosslinks`,{prev_description:a}).then(i=>{if(i&&i.updated&&i.updated.length>0){for(const o of i.updated)allEntities=allEntities.map(r=>r.id===o.id?o:r);try{renderCompendium()}catch{}typeof showToast=="function"&&showToast(`\u{1F517} Updated ${i.updated.length} crosslinked ${i.updated.length===1?"entry":"entries"}`)}}).catch(()=>{})}function _codex2InlineEdit(e,t){const n=document.createElement("div");n.className="cx2-edit-box";const a=document.createElement(t.multiline?"textarea":"input");a.className="cx2-edit-input",a.value=t.value||"",t.placeholder&&(a.placeholder=t.placeholder),t.multiline&&(a.rows=Math.min(22,Math.max(4,(t.value||"").split(`
353`).length+2)));let s=null;if(!t.multiline&&t.ledgerKey){const c=(allEntities||[]).find(d=>d.id===t.entityId),l=c?_cx2LedgerSuggestions(c.type,t.ledgerKey):[];l.length&&(s=document.createElement("datalist"),s.id=`cx2-dl-${t.ledgerKey}`,s.innerHTML=l.map(d=>`<option value="${escHtml(d)}"></option>`).join(""),a.setAttribute("list",s.id),a.setAttribute("autocomplete","off"))}const i=document.createElement("div");i.className="cx2-edit-bar";const o=document.createElement("button");o.type="button",o.className="cx2-edit-save",o.textContent="Save";const r=document.createElement("button");r.type="button",r.className="cx2-edit-cancel",r.textContent="Cancel",i.appendChild(o),i.appendChild(r),n.appendChild(a),s&&n.appendChild(s),n.appendChild(i),e.replaceWith(n),a.focus(),r.onclick=()=>_cx2RerenderEntry(t.entityId),o.onclick=()=>{if(typeof t.onSave=="function"){t.onSave(a.value);return}const c=t.ledgerKey?{ledger:{[t.ledgerKey]:a.value}}:{[t.field]:a.value};_codex2SaveField(t.entityId,c)},a.addEventListener("keydown",c=>{c.key==="Escape"&&_cx2RerenderEntry(t.entityId),c.key==="Enter"&&!t.multiline&&o.click()})}function _codex2AddPen(e,t,n,a,s,i,o){const r=e.querySelector(t);if(!r)return;r.classList.add("cx2-editable");const c=document.createElement("button");c.type="button",c.title="Edit",c.innerHTML="&#9998;",c.className=o?"cx2-edit-pen cx2-edit-pen--float":"cx2-edit-pen",c.onclick=l=>{l.stopPropagation(),_codex2InlineEdit(r,{entityId:n,field:a,value:i(),multiline:s})},o?r.insertBefore(c,r.firstChild):r.appendChild(c)}function _codex2QuoteCount(e){if(typeof allSessions>"u"||!Array.isArray(allSessions))return 0;let t=0;return allSessions.forEach(n=>(n.session_quotes||[]).forEach(a=>{a&&a.speaker_id===e.id&&t++})),t}function _codex2EntityQuotes(e,t){if(t=t||3,typeof allSessions>"u"||!Array.isArray(allSessions))return[];const n=[];if(allSessions.forEach(s=>{const i=typeof getSessionNum=="function"?getSessionNum(s):null;
353(s.session_quotes||[]).forEach(o=>{o&&o.speaker_id===e.id&&o.quote&&n.push({id:o.id,quote:o.quote,context:o.context||"",session_id:s.id,sessionNum:i})})}),n.sort((s,i)=>(s.sessionNum||0)-(i.sessionNum||0)),n.length<=t)return n;const a=[n[0]];return t>=3&&a.push(n[Math.floor(n.length/2)]),a.push(n[n.length-1]),a.slice(0,t)}async function _codex2GetAppearances(e,t){try{const n=await api("GET",`/api/campaigns/${e}/entities/${t}/appearances`);return n&&Array.isArray(n.sessions)?n.sessions:[]}catch{return[]}}function _codex2NavScrollFlash(e){e.scrollIntoView({behavior:"smooth",block:"start"}),e.classList.add("cx2-nav-flash"),setTimeout(()=>e.classList.remove("cx2-nav-flash"),1600)}function _codex2GoToThread(e){const t=document.querySelector('.tab-btn[data-tab="threads"]');if(!t)return;t.click();let n=0;const a=()=>{const s=document.querySelector(`#tab-threads [data-thread-id="${e}"]`);s?_codex2NavScrollFlash(s):n++<40&&setTimeout(a,120)};setTimeout(a,140)}function _codex2GoToSession(e){typeof renderChronicleRecap=="function"&&renderChronicleRecap(e)}const _CX2_LEDGER_FIELDS={pc:[{key:"player",label:"Player",auto:e=>e.player_name},{key:"race",label:()=>typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()?"Ancestry":"Species",auto:(e,t)=>t&&(t.ancestry||t.species)},{key:"community",label:"Community",auto:(e,t)=>t&&t.community,only:"daggerheart"},{key:"background",label:"Background",auto:(e,t)=>t&&t.background},{key:"origin",label:"Place of origin"},{key:"level",label:"Level",auto:(e,t)=>t&&t.level!=null?String(t.level):null},{key:"size",label:"Size"},{key:"token_scale",label:"Token scale"},{key:"body_plan",label:"Body plan"},{key:"patron",label:"Patron / god"}],npc:[{key:"species",label:"Species"},{key:"size",label:"Size"},{key:"token_scale",label:"Token scale"},{key:"body_plan",label:"Body plan"},{key:"occupation",label:"Occupation"},{key:"affiliation",label:"Affiliation"},{key:"origin",label:"Place of origin"},{key:"first_seen",label:"First seen",derive:"first"},{key:"last_seen",label:"Last seen",derive:"last"}],location:[{key:"kind",label:"Kind"},{key:"faction",label:"Controlling faction"},{key:"first_visited",label:"First visited",derive:"first"},{key:"residents",label:"Notable residents"}],item:[{key:"kind",label:"Kind",auto:e=>e.ref_item_kind||null},{key:"bearer",label:"Current bearer"},{key:"acquired_from",label:"Acquired from"}],faction:[{key:"kind",label:"Kind",storedFirst:!0,auto:e=>e.ref_faction_kind||(e.ref_faction_role?String(e.ref_faction_role).replace(/\b\w/g,t=>t.toUpperCase()):null)},{key:"leader",label:"Leader"},{key:"first_contact",label:"First contact",derive:"first"}],lore:[{key:"category",label:"Category"},{key:"source",label:"Source"},{key:"revealed_in",label:"Revealed in",derive:"first"}],deity:[{key:"pantheon",label:"Pantheon",auto:e=>e.ref_pantheon||null},{key:"worshippers",label:"Worshippers in party"},{key:"first_mentioned",label:"First mentioned",derive:"first"}],companion:[{key:"kind",label:"Kind"},{key:"size",label:"Size"},{key:"token_scale",label:"Token scale"},{key:"body_plan",label:"Body plan"},{key:"bonded_to",label:"Bonded to",auto:e=>{const t=(allEntities||[]).find(n=>n.id===e.parent_entity_id);return t?t.name:null}},{key:"acquired_in",label:"Acquired in",derive:"first"},{key:"status",label:"Status",auto:e=>e.is_dead?"Fallen":e.is_inactive?"Departed":"Living"}],spell:[{key:"school",label:"School",auto:e=>e.ref_spell_school?String(e.ref_spell_school).charAt(0).toUpperCase()+String(e.ref_spell_school).slice(1):null},{key:"level",label:"Level",auto:e=>e.ref_spell_level!=null?e.ref_spell_level===0?"Cantrip":"Level "+e.ref_spell_level:null}]};function _codex2EntityLedger(e){let t=e.ledger;if(typeof t=="string")try{t=JSON.parse(t)}catch{t={}}return t&&typeof t=="object"?t:{}}const _CX2_LEDGER_SEED={"location.kind":["City","Town","Village","Hamlet","Settlement","Region","Realm","Castle","Keep","Fortress","Tower","Temple","Shrine","Dungeon","Ruins","Cave","Forest","Mountain","Hill","River","Lake","Sea","Ocean","Coast","Island","Swamp","Marsh","Desert","Plains","Valley","Road","Bridge","Port","Tavern","Inn","Shop","Plane"],"item.kind":["Weapon","Armor","Shield","Wondrous item","Potion","Scroll","Ring","Rod","Staff","Wand","Trinket","Tool","Artifact","Treasure","Consumable"],"companion.kind":["Familiar","Animal companion","Mount","Steed","Beast","Construct","Sidekick","Hireling","Summon"],"pc.size":["Tiny","Small","Medium","Large","Huge","Gargantuan"],"npc.size":["Tiny","Small","Medium","Large","Huge","Gargantuan"],"companion.size":["Tiny","Small","Medium","Large","Huge","Gargantuan"],"pc.token_scale":["1.1","1.25","0.9"],"npc.token_scale":["1.1","1.25","0.9"],"companion.token_scale":["1.1","1.25","0.9"],"pc.body_plan":["Humanoid","Quadruped","Flying","Other"],"npc.body_plan":["Humanoid","Quadruped","Flying","Other"],"companion.body_plan":["Humanoid","Quadruped","Flying","Other"],"faction.kind":["Cult","Guild","Council","Government","Military","Order","Religious Order","Criminal Syndicate","Mercenary Company","Noble House","Clan","Tribe","Adventuring Party","Merchant Company","Secret Society","Court","Academic","Resistance","Aberration","Beast","Celestial","Construct","Dragon","Elemental","Fey","Fiend","Giant","Goblinoid","Humanoid","Lycanthrope","Monstrosity","Ooze","Plant","Undead","Dwarf","Elf","Gnome","Halfling","Orc"],"lore.category":["Legend","Prophecy","History","Religion","Myth","Rumor","Event","Custom"]};function _cx2LedgerSuggestions(e,t){const n=[];for(const o of allEntities||[]){if(o.type!==e)continue;const r=_codex2EntityLedger(o)[t];typeof r=="string"&&r.trim()&&n.push(r.trim())}const a=_CX2_LEDGER_SEED[`${e}.${t}`]||[],s=[],i=new Set;for(const o of[...n,...a]){const r=o.toLowerCase();i.has(r)||(i.add(r),s.push(o))}return s.slice(0,50)}function _cx2CapFirst(e){return e=String(e??""),e.replace(/^(\s*)(\p{L})/u,(t,n,a)=>n+a.toUpperCase())}const _CX2_STOP=/^(the|of|a|an|and|to|de|la|le|el|von|van|der|du)$/i,_CX2_POSS=/['’]s?$/;function _cx2Lev(e,t){const n=e.length,a=t.length;if(!n)return a;
353if(!a)return n;let s=Array.from({length:a+1},(i,o)=>o);for(let i=1;i<=n;i++){const o=[i];for(let r=1;r<=a;r++)o[r]=Math.min(s[r]+1,o[r-1]+1,s[r-1]+(e[i-1]===t[r-1]?0:1));s=o}return s[a]}function _cx2IsMishearing(e,t,n){const a=e.toLowerCase().replace(/[^a-z0-9]/g,"");if(a.length<3)return!0;const s=[n].concat(t.map(i=>i.replace(/[^a-z0-9]/g,""))).filter(i=>i&&i.length>=3);for(const i of s)if(i===a||i.startsWith(a)||a.startsWith(i)||Math.abs(a.length-i.length)<=3&&1-_cx2Lev(a,i)/Math.max(a.length,i.length)>=.6)return!0;return!1}function _cx2StripArticle(e){return String(e).replace(/^\s*(the|a|an)\s+/i,"").trim()}const _CX2_PERSON=new Set(["pc","npc","companion","deity"]);function _codex2KnownAs(e,t,n){const a=_CX2_PERSON.has(String(n||"").toLowerCase()),s=String(e||"").toLowerCase().trim(),i=s.split(/\s+/).filter(Boolean),o=new Set(i),r=s.replace(/[^a-z0-9]/g,""),c=[],l=new Set;return(Array.isArray(t)?t:[]).forEach(d=>{if(!d)return;const u=String(d).replace(/\s*\([^)]*\)\s*/g," ").trim(),p=u.toLowerCase();if(!p||p===s)return;const m=u.split(/\s+/);let f=0,g=m.length;for(;g>f&&o.has(m[g-1].toLowerCase());)g--;for(;f<g&&o.has(m[f].toLowerCase());)f++;const h=m.slice(f,g);let v=null;if(h.length===0)return;if(h.length<m.length){const y=h.map($=>$.toLowerCase()),_=_CX2_POSS.test(h[h.length-1]);if(a&&!_CX2_STOP.test(h[0])&&!_CX2_STOP.test(h[h.length-1])&&h.length<=3&&!y.some($=>o.has($))&&!_)v=h.join(" ");else if(_)if(p.includes(s))v=_cx2StripArticle(u);else return;else{if(i.length>=2&&p.includes(s))return;if(a)v=_cx2StripArticle(u);else if(p.includes(s))v=_cx2StripArticle(u);else return}}else{if(i.length>=2&&p.includes(s))return;v=_cx2StripArticle(u)}if(!v||_cx2IsMishearing(v,i,r))return;const b=v.toLowerCase();l.has(b)||(l.add(b),c.push(_cx2CapFirst(v)))}),c.sort((d,u)=>d.localeCompare(u)),c}const _CX2_LINK_FIELDS=new Set(["affiliation","faction","leader","bearer","origin","patron","residents","worshippers","acquired_from"]);function _cx2LinkNormKey(e){return String(e||"").toLowerCase().replace(/[‘’ʼ]/g,"'").replace(/^\s*(the|a|an)\s+/,"").replace(/[^a-z0-9'\s]/g," ").replace(/\s+/g," ").trim()}function _cx2Singularize(e){return e.endsWith("es")?e.slice(0,-2):e.endsWith("s")?e.slice(0,-1):e}let _cx2LinkIndex=null;function _cx2BuildLinkIndex(){if(_cx2LinkIndex&&_cx2LinkIndex.ref===allEntities)return _cx2LinkIndex;const e=new Map,t=(n,a)=>{if(!n||n.length<3)return;const s=e.get(n);(!s||a.type==="pc"&&s.type!=="pc")&&e.set(n,a)};return(allEntities||[]).forEach(n=>{if(!n||!n.name||n.type==="quote")return;const a=_cx2LinkNormKey(n.name);t(a,n),t(_cx2Singularize(a),n);let s=n.aliases;if(typeof s=="string")try{s=JSON.parse(s)}catch{s=[]}(Array.isArray(s)?s:[]).forEach(i=>{const o=_cx2LinkNormKey(i);t(o,n),t(_cx2Singularize(o),n)})}),_cx2LinkIndex={ref:allEntities,byKey:e},_cx2LinkIndex}function _cx2ResolveLink(e,t){const n=_cx2LinkNormKey(e);if(!n||n.length<3)return null;const a=_cx2BuildLinkIndex().byKey;let s=a.get(n)||a.get(_cx2Singularize(n));if(!s&&n.length>=5){let i=null,o=99;for(const[r,c]of a){if(Math.abs(r.length-n.length)>2)continue;const l=_cx2Lev(n,r);l<o&&l<=2&&1-l/Math.max(n.length,r.length)>=.86&&(i=c,o=l)}s=i}return s&&s.id!==t?s:null}function _cx2LinkifyLedgerValue(e,t){return String(e).split(/\s*,\s*/).map(a=>{const s=_cx2ResolveLink(a,t),i=escHtml(a);return s?`<span class="entity-link" data-id="${s.id}" data-type="${s.type}" tabindex="0">${i}</span>`:i}).join(", ")}function _codex2BuildLedger(e,t,n,a,s){let i=_CX2_LEDGER_FIELDS[e.type]||[{key:"first_seen",label:"First seen",derive:"first"},{key:"last_seen",label:"Last seen",derive:"last"}];s&&s.length&&(i=i.filter(m=>s.indexOf(m.key)===-1));{const m=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart();i=i.filter(f=>!f.only||f.only==="daggerheart"===m).map(f=>typeof f.label=="function"?Object.assign({},f,{label:f.label()}):f)}let o=e.dnd_beyond_data;if(typeof o=="string")try{o=JSON.parse(o)}catch{o=null}const r=_codex2EntityLedger(e),c=Array.isArray(r._hidden)?r._hidden:[],l=m=>m&&typeof getSessionNum=="function"?getSessionNum(m):m?m.id:null,d=m=>m?"Session "+l(m)+(m.title?" \xB7 "+m.title:""):null;let u="",p="";if(i.forEach(m=>{let f=null,g=!1;if(m.derive)f=d(m.derive==="first"?t:n);else{const b=r[m.key]!=null&&r[m.key]!==""?r[m.key]:null;if(m.storedFirst&&b!=null)f=b,g=!0;else if(m.auto)try{f=m.auto(e,o,t,n)}catch{}f==null||f===""?(f=b,g=!0):m.storedFirst&&(g=!0)}const h=escHtml(m.key),v=escHtml(m.label);if(f!=null&&f!==""){const b=g&&a,y=_cx2CapFirst(f),_=_CX2_LINK_FIELDS.has(m.ke
353y)?_cx2LinkifyLedgerValue(y,e.id):escHtml(y);u+=`<div class="cx2-ledger-row${b?" cx2-ledger-row--fillable":""}"${b?` data-lkey="${h}" data-llabel="${v}" title="Edit"`:""}><span class="cx2-ledger-key">${v}</span><span class="cx2-ledger-val">${_}</span></div>`}else g&&a&&(c.indexOf(m.key)!==-1?p+=`<div class="cx2-ledger-row cx2-ledger-hiddenrow"><span class="cx2-ledger-key">${v}</span><button type="button" class="cx2-led-show" data-lshow="${h}" title="Show field">Show</button></div>`:u+=`<div class="cx2-ledger-row cx2-ledger-row--fillable cx2-ledger-add" data-lkey="${h}" data-llabel="${v}"><span class="cx2-ledger-key">${v}</span><span class="cx2-ledger-addrow"><span class="cx2-ledger-add-cta">+ Add</span><button type="button" class="cx2-led-hide" data-lhide="${h}" title="Mark not applicable">Hide</button></span></div>`)}),p){const m=(p.match(/cx2-led-show/g)||[]).length;u+=`<button type="button" class="cx2-led-showhidden">+ ${m} hidden</button><div class="cx2-led-hidden-wrap hidden">${p}</div>`}return u}function _codex2HideLedgerField(e,t,n){const a=(allEntities||[]).find(o=>o.id===e),s=_codex2EntityLedger(a||{});let i=Array.isArray(s._hidden)?s._hidden.slice():[];n?i.indexOf(t)===-1&&i.push(t):i=i.filter(o=>o!==t),_codex2SaveField(e,{ledger:{_hidden:i}})}const _CX2_PILLS={pc:[],npc:[],location:[{key:"safety",label:"Safety"},{key:"status",label:"Status"}],item:[{key:"item_status",label:"Status"},{key:"rarity",label:"Rarity",auto:e=>{if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart())return null;try{const t=_itemRarityForName(e.name,e);return typeof t=="string"?t:t&&(t.label||t.name||t.slug)||null}catch{return null}}},{key:"attunement",label:"Attunement",only:"dnd5e"},{key:"cursed",label:"Cursed"}],faction:[{key:"faction_status",label:"Status"}],lore:[{key:"verified",label:"Verified"},{key:"plot_critical",label:"Plot-critical"}],deity:[{key:"morality",label:"Morality"},{key:"deity_activity",label:"Activity"}],companion:[]},_CX2_PILL_OPTIONS={safety:["Safe","Unsafe","Hostile"],status:["Active","Ruined","Destroyed","Sealed","Legend","Restored"],item_status:["Equipped","Inventory","Stashed","Lost","Consumed","Destroyed"],rarity:["Common","Uncommon","Rare","Very Rare","Legendary","Artifact"],attunement:["Attuned","Required","Not Required"],cursed:["Cursed"],faction_status:["Active","Defunct"],verified:["Verified","Rumor"],plot_critical:["Plot-critical"],morality:["Benevolent","Neutral","Malevolent"],deity_activity:["Active","Background","Dormant"]};function _codex2InlinePick(e,t){const n=document.createElement("span");if(n.className="cx2-pill-pick",(t.options||[]).forEach(a=>{const s=document.createElement("button");s.type="button",s.className="cx2-pick-opt"+(a===t.current?" on":""),s.textContent=a,s.onclick=i=>{i.stopPropagation(),_codex2SaveField(t.entityId,{ledger:{[t.ledgerKey]:a}})},n.appendChild(s)}),t.current){const a=document.createElement("button");a.type="button",a.className="cx2-pick-clear",a.title="Clear",a.innerHTML="&times;",a.onclick=s=>{s.stopPropagation(),_codex2SaveField(t.entityId,{ledger:{[t.ledgerKey]:""}})},n.appendChild(a)}e.replaceWith(n)}function _codex2BuildPills(e,t,n){const a=e.type,s=_codex2EntityLedger(e),i=[];(a==="pc"||a==="npc"||a==="companion")&&i.push(e.is_dead?'<span class="cx2-pill dead"><span class="sw"></span>Fallen</span>':e.is_inactive?'<span class="cx2-pill"><span class="sw"></span>Departed</span>':'<span class="cx2-pill alive"><span class="sw"></span>Active</span>'),a==="pc"&&(e.party_role&&i.push(`<span class="cx2-pill cx2-pill--combat">${escHtml(e.party_role)}</span>`),e.noncombat_role&&i.push(`<span class="cx2-pill cx2-pill--noncombat">${escHtml(e.noncombat_role)}</span>`)),a==="companion"&&i.push('<span class="cx2-pill">Bonded</span>'),a==="location"&&n&&i.push('<span class="cx2-pill alive">Visited</span>'),a==="spell"&&(e.ref_concentration&&i.push('<span class="cx2-pill">Concentration</span>'),e.ref_ritual&&i.push('<span class="cx2-pill">Ritual</span>'));const o=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart();return(_CX2_PILLS[a]||[]).forEach(r=>{if(r.only&&r.only==="daggerheart"!==o)return;
353let c=s[r.key];if((c==null||c==="")&&r.auto)try{c=r.auto(e)}catch{}c!=null&&c!==""?i.push(`<span class="cx2-pill cx2-pill--set${t?" cx2-pill--fillable":""}"${t?` data-pkey="${escHtml(r.key)}" data-plabel="${escHtml(r.label)}" title="Edit"`:""}>${escHtml(String(c))}</span>`):t&&i.push(`<span class="cx2-pill cx2-pill--add cx2-pill--fillable" data-pkey="${escHtml(r.key)}" data-plabel="${escHtml(r.label)}">+ ${escHtml(r.label)}</span>`)}),i.join("")}let _cx2SealCutoff={cid:null,cutoff:1/0},_cx2SealFidSeq=0;function _codex2SealCutoff(){const e=currentCampaign&&currentCampaign.id;if(_cx2SealCutoff.cid===e)return _cx2SealCutoff.cutoff;let t=1/0;try{const n=currentCampaign&&currentCampaign.bardic_index&&currentCampaign.bardic_index.entity_sessions;if(n){const a=(allEntities||[]).filter(s=>s&&(s.type==="npc"||s.type==="faction")).map(s=>(n[String(s.id)]||[]).length).filter(s=>s>0).sort((s,i)=>i-s);if(a.length){const s=Math.max(0,Math.floor(a.length*.1)-1);t=Math.max(2,a[s])}}}catch{}return _cx2SealCutoff={cid:e,cutoff:t},t}function _codex2WarrantsSeal(e){const t=e.type;if(t==="pc"||t==="companion"||t==="deity")return!0;if(t==="npc"||t==="faction")try{const n=currentCampaign&&currentCampaign.bardic_index&&currentCampaign.bardic_index.entity_sessions;return(n&&n[String(e.id)]||[]).length>=_codex2SealCutoff()}catch{return!1}return!1}function _codex2WarrantsBardicTake(e){const t=e.type;if(t==="pc"||t==="companion"||t==="deity")return!0;if(t==="npc"||t==="faction"){try{const n=currentCampaign&&currentCampaign.bardic_index&&currentCampaign.bardic_index.entity_sessions;if(n&&n[String(e.id)]&&n[String(e.id)].length>=3)return!0}catch{}try{if(typeof allSessions<"u"&&Array.isArray(allSessions)){for(const n of allSessions)if((n.session_quotes||[]).some(a=>a&&a.speaker_id===e.id))return!0}}catch{}return!1}return!1}const _DTUNE_SLIDERS=[["depth","Depth",0,1,.005,3],["feather","Edge softness",.005,.2,.005,3],["deckle","Deckle amount",0,.05,.001,3],["scale","Deckle scale",3,40,1,0]];function _cx2PcIdentityStrip(e){const t=[],n=e.species||e.dnd_beyond_data&&e.dnd_beyond_data.species||"";n&&t.push(["Ancestry",n]),e.party_role&&t.push(["Role",e.party_role]);const a=e.is_dead?"Fallen":e.is_inactive?"Departed":"";return a&&t.push(["Status",a]),t.length?`<div class="cx2-ledger cx2-statledger">
354    <div class="cx2-ledger-eyebrow"><span class="led-title">Character</span></div>
355    ${t.map(([i,o])=>`<div class="cx2-ledger-row"><span class="cx2-ledger-key">${escHtml(i)}</span><span class="cx2-ledger-val">${escHtml(String(o))}</span></div>`).join("")}
356  </div>`:""}function _cx2PcStatBlock(e){if(!e||e.type!=="pc")return"";const t=typeof _codex2CanEdit=="function"&&_codex2CanEdit(e),n=!t&&!!e.assignable&&!!(currentUser&&currentUser.id),a=t?`<button type="button" class="cx2-edit-pen cx2-sb-edit" data-sb-edit="${e.id}" title="Edit stats">&#9998;</button>`:"";if(!e.dnd_beyond_data){const d=_cx2PcIdentityStrip(e);if(!t&&!n)return d;const u=t&&typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart(),p=t&&!u&&typeof _codex2IsGm=="function"&&_codex2IsGm()?'<div class="form-helper-text cx2-sb-empty-alt">Or sync from D&amp;D Beyond with the <a href="https://chromewebstore.google.com/detail/lbkabfefehjgfoeackpiomkikhbnbndo?utm_source=threadfall-codex" target="_blank" rel="noopener noreferrer">Threadfall Party Sync</a> Chrome extension.</div>':"",m=u?`<button type="button" class="btn-ghost btn-xs" data-sb-demiplane="${e.id}">Import from a Demiplane link</button>`:"";let f="";try{const g=(typeof renderPartyCard=="function"?renderPartyCard(e,{forCodex:!0,canImport:t,canClaim:n}):"")||"",h=document.createElement("div");h.innerHTML=g,f=h.querySelector(".cx2-pc-sheet-cluster")?.outerHTML||""}catch{f=""}return d+`<div class="cx2-ledger cx2-statledger">
357      <div class="cx2-ledger-eyebrow"><span class="led-title">Statistics</span>${a}</div>
358      <div class="cx2-sb-empty">
359        ${t?'<div class="form-helper-text">No stats recorded yet.</div>':""}
360        ${f}
361        ${t?`<button type="button" class="btn-ghost btn-xs" data-sb-edit="${e.id}"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="cx2-btn-ico"><path d="M11 2.5 13.5 5 5 13.5 2 14l.5-3Z"/><path d="M9.5 4 12 6.5"/></svg> Enter stats manually</button>`:""}
362        ${m}
363        ${p}
364      </div>
365    </div>`}let s="";try{s=(typeof renderPartyCard=="function"?renderPartyCard(e,{forCodex:!0,canImport:t,canClaim:n}):"")||""}catch{return""}if(!s)return"";const i=document.createElement("div");i.innerHTML=s;const o=i.querySelector(".party-pc-stats");if(!o)return"";o.querySelectorAll(".party-pc-actions, .party-pc-name-row, .pc-best-quote, .pc-quote-divider, .pc-role-badge, .pc-noncombat-badge, .pc-ai-badge--ddb").forEach(d=>d.remove()),o.querySelectorAll(".ddb-ability-grid").forEach(d=>{d.querySelector(".ddb-ab-score")||d.classList.add("cx2-sb-passives")}),o.querySelectorAll(".ddb-row").forEach(d=>{const u=d.querySelector(".ddb-row-label");u&&u.textContent.trim()==="Profs"&&d.remove()});const r=o.querySelector(".skill-pills-grid");if(r){const d=Array.from(r.querySelectorAll(".skill-pill")),u=f=>f.matches(".sp-best, .sp-tied, .sp-near")||!!f.querySelector(".sp-adv"),p=d.filter(f=>!u(f)),m=d.filter(u);
365if(p.length&&m.length){const f=document.createElement("details");f.className="cx2-sb-skills-more",f.innerHTML=`<summary>Show all ${d.length} skills</summary>`;const g=document.createElement("div");g.className="skill-pills-grid",p.forEach(h=>g.appendChild(h)),f.appendChild(g),r.insertAdjacentElement("afterend",f)}else if(!m.length){const f=document.createElement("details");f.className="cx2-sb-skills-more",f.innerHTML=`<summary>Show all ${d.length} skills</summary>`,r.insertAdjacentElement("afterend",f),f.appendChild(r)}}if(!o.innerHTML.trim())return"";const c=[[".ddb-ab-best, .ddb-ab-tied-best, .sp-best, .sp-tied",'<span class="pleg-swatch pleg-swatch--best"></span>Best in party'],[".ddb-ab-near-best, .sp-near",'<span class="pleg-swatch pleg-swatch--near"></span>Near best (&minus;1)'],[".party-unique-tag",'<span class="pleg-swatch pleg-swatch--unique"></span>Unique to this PC'],[".key-spell-fav",'<span class="pleg-swatch pleg-swatch--fav"></span>Most used ability'],[".sp-adv",'<span class="pleg-swatch pleg-swatch--adv">A</span>Has advantage']].filter(([d])=>o.querySelector(d)),l=c.length?`<div class="party-legend cx2-sb-legend">
366      ${c.map(([,d])=>`<span class="pleg-item">${d}</span>`).join("")}
367    </div>`:"";return`<div class="cx2-ledger cx2-statledger">
368    <div class="cx2-ledger-eyebrow"><span class="led-title">Statistics</span>${a}</div>
369    <div class="cx2-pc-statwrap">${o.innerHTML}</div>
370    ${l}
371  </div>`}const _CX2_ACT_ICO={skull:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2A6.8 6.8 0 0 0 5.2 10c0 2 .9 3.8 2.3 5 .4.3.6.7.6 1.2v1.5c0 .3.3.6.6.6H9v-1.6h1.6v1.6h2.8v-1.6H15v1.6h.3c.3 0 .6-.3.6-.6v-1.5c0-.5.2-.9.6-1.2 1.4-1.2 2.3-3 2.3-5A6.8 6.8 0 0 0 12 3.2Z"/><circle cx="9.4" cy="10.6" r="1.4" fill="currentColor" stroke="none"/><circle cx="14.6" cy="10.6" r="1.4" fill="currentColor" stroke="none"/></svg>',depart:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 4h3.5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H14"/><path d="M10 8l-4 4 4 4"/><path d="M6 12h9"/></svg>',refresh:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg>',tidy:'<svg class="cx2-actbtn-ico cx2-ico-tidy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path pathLength="1" d="M4 6h16"/><path pathLength="1" d="M4 12h11"/><path pathLength="1" d="M4 18h6"/></svg>',eye:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.5"/></svg>',companion:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-companion"/></svg>',sheet:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="2.5" width="14" height="19" rx="2"/><path d="M9 7h6M9 11h6M9 15h4"/></svg>',edit:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16v4z"/><path d="M14 6l4 4"/></svg>',merge:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3v5a4 4 0 0 0 4 4h8"/><path d="M6 21v-5"/><path d="M15 9l3 3-3 3"/></svg>',trash:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M9 7V4.5h6V7"/><rect x="6" y="7" width="12" height="13" rx="1.5"/>
371<line x1="10" y1="11" x2="10" y2="16"/><line x1="14" y1="11" x2="14" y2="16"/></svg>',screen:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 5.5 7 4 7 19.5 2.5 21Z"/><rect x="7" y="3.5" width="10" height="17" rx="0.6"/><path d="M17 4 21.5 5.5 21.5 21 17 19.5Z"/><path d="M9.4 11.6Q12 14 14.6 11.6"/><path d="M9.8 12.6 9.3 13.5M12 13.1 12 14M14.2 12.6 14.7 13.5"/></svg>',book:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="3.5" width="14" height="17" rx="1.6"/><path d="M8 3.5 8 20.5"/><path d="M11 11.3Q13.5 13.5 16 11.3"/><path d="M11.6 12.3 11.1 13.2M13.5 12.8 13.5 13.7M15.4 12.3 15.9 13.2"/></svg>',eyeRays:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12Q12 7.5 19 12 12 16.5 5 12Z"/><circle cx="12" cy="12" r="2.2"/><path d="M12 5 12 2.5M7 7.2 5.6 5.6M17 7.2 18.4 5.6M4.6 10.2 2.7 9.4M19.4 10.2 21.3 9.4"/></svg>',eyeClosed:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.8 12.5Q12 17.6 20.2 12.5"/><path d="M6.6 14 5.7 15.5M9.4 15.1 8.9 16.6M12 15.5 12 17.1M14.6 15.1 15.1 16.6M17.4 14 18.3 15.5"/></svg>',quill:'<svg class="cx2-actbtn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 3.5c-5.5.8-10.5 5-13 11l-2 5.5 5.5-2c6-2.5 10.2-7.5 11-13z"/><path d="M5.5 20 13 12.5"/></svg>'},_TF_PORT_GLYPH='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="4.5" r="2"/><line x1="12" y1="6.5" x2="12" y2="21"/><line x1="6.5" y1="10" x2="17.5" y2="10"/><path d="M4.5 14.5 a7.5 7.5 0 0 0 15 0"/></svg>',_MAP_ACT_ICO={codex:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3.5h10a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-13a2 2 0 0 1 2-2z"/><path d="M8 3.5v17"/><path d="M11 8h4M11 11.5h4M11 15h2.5"/></svg>',partyHere:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a5 5 0 0 1 5 5c0 3.5-5 10-5 10S7 10.5 7 7a5 5 0 0 1 5-5z"/><circle cx="12" cy="7" r="1.8" fill="currentColor" stroke="none"/><path d="M5 19l3-1.5M19 19l-3-1.5M12 21v-3.5"/></svg>',eye:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5S21.5 12 21.5 12S18 18.5 12 18.5S2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.5"/></svg>',eyeSlash:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5S21.5 12 21.5 12S18 18.5 12 18.5S2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.5"/><line x1="4" y1="4" x2="20" y2="20"/></svg>',here:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="7" stroke-dasharray="2 3"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/></svg>',move:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a4.5 4.5 0 0 1 4.5 4.5c0 3-4.5 8.5-4.5 8.5S7.5 9.5 7.5 6.5A4.5 4.5 0 0 1 12 2z"/>
371<circle cx="12" cy="6.5" r="1.6" fill="currentColor" stroke="none"/><path d="M7 17.5c1.5 1 3 1.5 5 1.5s3.5-.5 5-1.5"/><path d="M17 15.5l2 2-2 2"/></svg>',drill:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="14" rx="2"/><rect x="7.5" y="8.5" width="9" height="7" rx="1.5"/><path d="M12 17v4"/><path d="M9.5 19l2.5 2.5 2.5-2.5"/></svg>',trash:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M9 7V4.5h6V7"/><rect x="6" y="7" width="12" height="13" rx="1.5"/><line x1="10" y1="11" x2="10" y2="16"/><line x1="14" y1="11" x2="14" y2="16"/></svg>',shape:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19 Q9 5 19 5"/><rect x="3" y="17" width="4" height="4" rx="0.8" fill="currentColor" stroke="none"/><rect x="17" y="3" width="4" height="4" rx="0.8" fill="currentColor" stroke="none"/><circle cx="12.5" cy="10.5" r="2.2"/></svg>',magnet:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h3v8a3 3 0 0 0 6 0V4h3"/><line x1="6" y1="4" x2="9" y2="4"/><line x1="15" y1="4" x2="18" y2="4"/><line x1="6" y1="18" x2="6" y2="21"/><line x1="18" y1="18" x2="18" y2="21"/></svg>',rough:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="12" x2="21" y2="12"/><path d="M3 16 l3-4 3 4 3-4 3 4 3-4 3 4"/><path d="M3 8 l3 4 3-4 3 4 3-4 3 4 3-4"/></svg>',roughSec:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="12" x2="21" y2="12"/><path d="M8 16 l3-4 3 4 3-4"/><path d="M8 8 l3 4 3-4 3 4"/><line x1="7" y1="7" x2="7" y2="17"/><line x1="17" y1="7" x2="17" y2="17"/></svg>',roughClear:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="12" x2="21" y2="12"/><path d="M5 8l3 3-3 3M19 8l-3 3 3 3" opacity="0.35"/><path d="M14 6 L8 18"/><path d="M8 6 L14 18"/></svg>',toWater:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 14 c2.5-3 5-3 7.5 0s5 3 7.5 0 5-3 7.5 0"/><path d="M2 18 c2.5-3 5-3 7.5 0s5 3 7.5 0"/><path d="M10 8l2-2 2 2"/><line x1="12" y1="6" x2="12" y2="12"/></svg>',toLand:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="16" x2="21" y2="16"/><path d="M10 8l2-2 2 2"/><line x1="12" y1="6" x2="12" y2="12"/></svg>',dock:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="6" r="2"/><line x1="12" y1="8" x2="12" y2="20"/><line x1="7" y1="11" x2="17" y2="11"/><path d="M7 20 a5 5 0 0 0 10 0"/></svg>',dockClear:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="5.5" r="1.8"/><line x1="10" y1="7.3" x2="10" y2="18.5"/><line x1="5.5" y1="10" x2="14.5" y2="10"/><path d="M5.5 18.5 a4.5 4.5 0 0 0 9 0"/><line x1="16" y1="6" x2="22" y2="14"/><line x1="22" y1="6" x2="16" y2="14"/></svg>',more:'<svg class="at2-tb-ico at2-tb-ico--more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg>',levelOut:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 5l7 7-7 7"/><path d="M12 5l7 7-7 7"/></svg>',levelIn:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 5l-7 7 7 7"/><path d="M12 5l-7 7 7 7"/></svg>',levelAdd:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-4 9 4-9 4z"/><path d="M3 14l9 4 9-4"/><path d="M19 3v4M17 5h4"/></svg>',stairs:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 20.5h4.5V16H12v-4.5h4.5V7H21"/><path d="M3 20.5V17"/><path d="M21 7V3.5"/></svg>',chevUp:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 14.5 12 8l6.5 6.5"/></svg>',chevDown:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 9.5 12 16l6.5-6.5"/></svg>',chevL:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 5.5 8 12l6.5 6.5"/></svg>',chevR:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.5 5.5 16 12l-6.5 6.5"/></svg>',cycle:'<svg class="at2-cyc-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12a8 8 0 0 1 13.7-5.6"/><path d="M20 12a8 8 0 0 1-13.7 5.6"/>
371<path d="M18 2.8V7h-4.2"/><path d="M6 21.2V17h4.2"/></svg>',fitFrame:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 9V4.5H8"/><path d="M16 4.5h4.5V9"/><path d="M20.5 15v4.5H16"/><path d="M8 19.5H3.5V15"/></svg>',person:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="7" r="3.4"/><path d="M4.8 20.5c0-3.7 3.2-6.2 7.2-6.2s7.2 2.5 7.2 6.2"/></svg>',layers:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2 21 8l-9 4.8L3 8z"/><path d="M3.6 12.4 12 16.9l8.4-4.5"/><path d="M3.6 16.6 12 21l8.4-4.4"/></svg>',pinLocs:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.6a6 6 0 0 1 6 6c0 4.3-6 12.8-6 12.8S6 12.9 6 8.6a6 6 0 0 1 6-6z"/><circle cx="12" cy="8.6" r="2.1"/></svg>',addPlace:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.5a5 5 0 0 1 5 5c0 3.5-5 9.5-5 9.5S7 11 7 7.5a5 5 0 0 1 5-5z"/><path d="M12 5.5v4M10 7.5h4"/><path d="M5 20h14"/></svg>',route:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="18.5" r="2.2"/><circle cx="19" cy="5.5" r="2.2"/><path d="M7 17c2.5-1 2.5-4 .5-5.5S5 8 6.5 6.2"/><path d="M10.5 12.5c2.5 1.5 5 .5 6.5-1.5s.8-3.4 0-4.2" stroke-dasharray="2.5 2.5"/></svg>',ruler:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="8.5" width="19" height="7" rx="1.5"/><path d="M6.5 8.5v3M10 8.5v4.5M13.5 8.5v3M17 8.5v4.5"/></svg>',clock:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12.5" r="8"/><path d="M12 7.5v5l3.5 2"/></svg>',swords:'<svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4l11 11"/><path d="M13.5 17.5l4-4"/><path d="M16.5 16.5L19 19"/><path d="M19 4L8 15"/><path d="M10.5 17.5l-4-4"/><path d="M7.5 16.5L5 19"/></svg>'};async function _entityToggleDead(e){const t=allEntities.find(s=>s.id===e);if(!t)return null;const n=!!t.is_dead,a=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${e}`,{is_dead:!n});return allEntities=allEntities.map(s=>s.id===a.id?a:s),showToast(n?`${a.name} has been revived.`:`${a.name} marked as ${a.type==="pc"?"fallen":"dead"}.`),a}async function _entityToggleInactive(e){const t=allEntities.find(s=>s.id===e);if(!t)return null;const n=!!t.is_inactive,a=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${e}`,n?{is_inactive:!1}:{is_inactive:!0,is_dead:!1});return allEntities=allEntities.map(s=>s.id===a.id?a:s),n||_applyAutoFailedQuests(a),showToast(n?`\u{1F44B} ${a.name} has rejoined the party.`:`\u{1F44B} ${a.name} has left the party.`),a}async function _entityRebuildChronicle(e,t){const n=allEntities.find(i=>i.id===e);if(!n||!await confirmDialog({title:"Rebuild Chronicle",message:`This REPLACES "${n.name}"'s entire codex description with a chronicle rebuilt only from what the session recaps record about them, including replacing any notes or lore you've hand-written into it. The replaced text is kept for one-click Restore.`,confirmLabel:"Rebuild",danger:!0}))return null;const a=t?t.textContent:null,s=currentCampaign?.id;t&&(t.disabled=!0,t.textContent="\u{1F504} Grounding\u2026");try{let i=null;for(let c=0;c<40&&(i=await api("POST",`/api/campaigns/${s}/entities/${e}/rebuild-chronicle`),!(!i||!i.pending));c++)t&&(t.textContent=`\u{1F504} Grounding\u2026 ${i.pending} left`);const o=i&&i.entity;if(!o||!o.id)throw new Error("rebuild did not converge, try again");if(_campaignStale(s))return null;allEntities=allEntities.map(c=>c.id===o.id?o:c);const r=i.grounded_from||0;return showToast(`\u{1F504} Chronicle rebuilt from ${r} session${r===1?"":"s"}. Previous version saved (Restore available)`),o}catch(i){return showToast("Rebuild failed: "+(i&&i.message||"try again")),t&&(t.disabled=!1,t.textContent=a),null}}const _TF_TIDY_TYPES=new Set(["npc","location","faction","deity","item","lore","spell","companion"]),_TF_TIDY_VOICES=[{value:"plain",label:"Plain reference (skimmable, the default)"},{value:"read_aloud",label:"Read-aloud flavor (a line of atmosphere per section)"},{value:"terse",label:"Terse notes (fewest words that keep every fact)"}];async function _entityTidy(e,t){const n=allEntities.find(u=>u.id===e);if(!n)return null;const a=new Set(["spell","item"]);let s=a.has(n.type)?"terse":"plain",i={};try{const u=localStorage.getItem("tf_tidy_voice");u&&u.charAt(0)==="{"?i=JSON.parse(u)||{}:u&&!a.has(n.type)&&(i={[n.type]:u}),i[n.type]&&(s=i[n.type])}catch{i={}}const o=!!n.previous_description,r=await confirmDialog({title:"Tidy Entry",message:`This REPLACES "${n.name}"'s description with the same facts regrouped into short sections, including any notes you wrote into it by hand. Sentences that are about other entries rather than ${n.name} are left out. The current text is kept for one-click Restore.`+(o?" A previous version is already saved: tidying again replaces that saved copy with the current text, and the older one is gone.":""),confirmLabel:"Tidy",danger:!1,selectInput:{label:"Wording",options:_TF_TIDY_VOICES,defaultValue:s}});if(!r||!r.confirmed)return null;const c=r.select||s;try{i[n.type]=c,localStorage.setItem("tf_tidy_voice",JSON.stringify(i))}catch{}const l=t?t.innerHTML:null,d=currentCampaign?.id;t&&(t.disabled=!0,t.classList.add("is-busy"),t.querySelector(".actbtn-lbl")&&(t.querySelector(".actbtn-lbl").textContent="Tidying\u2026"));try{const u=await api("POST",`/api/campaigns/${d}/entities/${e}/tidy`,{voice:c}),p=u&&u.entity;if(!p||!p.id)throw new Error("no entry returned");
371if(_campaignStale(d))return null;allEntities=allEntities.map(f=>f.id===p.id?p:f);try{_codex2RenderCache&&_codex2RenderCache.byId&&delete _codex2RenderCache.byId[p.id]}catch{}const m=(u.off_topic||[]).length;return showToast(`Tidied into ${u.sections} section${u.sections===1?"":"s"}, ${u.chars_before.toLocaleString()} \u2192 ${u.chars_after.toLocaleString()} characters.`+(m?` ${m} sentence${m===1?"":"s"} about other entries left out.`:"")+" Restore previous is in the entry."),p}catch(u){return showToast("Tidy failed: "+(u&&u.message||"try again")),t&&(t.disabled=!1,t.classList.remove("is-busy"),l!=null&&(t.innerHTML=l)),null}}async function _cx2RestoreDescription(e,t){const n=currentCampaign?.id;if(n){t&&(t.disabled=!0);try{const a=await api("POST",`/api/campaigns/${n}/entities/${e}/restore-description`);if(_campaignStale(n))return;allEntities=allEntities.map(s=>s.id===a.id?a:s),showToast(`Restored previous description for "${a.name}". Restore again to switch back`),renderCompendium(),renderCodexEntry(e)}catch(a){showToast("Restore failed: "+(a&&a.message||"try again")),t&&(t.disabled=!1)}}}async function _cx2LoadGroundingNote(e,t){const n=e.querySelector(`.cx2-grounding-note[data-eid="${t}"]:not(.cx2-tidy-note)`);if(!n||!currentCampaign)return;const a=currentCampaign.id;try{const s=await api("GET",`/api/campaigns/${a}/entities/${t}/chronicle-status`);if(_campaignStale(a)||!n.isConnected)return;const i=[];if(s.grounded_at){const o=new Date(s.grounded_at).toLocaleDateString([],{month:"short",day:"numeric"});i.push(`Chronicle rebuilt from ${s.blurb_count} session${s.blurb_count===1?"":"s"} \xB7 ${o}`),s.stale_session_count>0&&i.push(`${s.stale_session_count} newer session${s.stale_session_count===1?"":"s"} not yet reflected`)}else i.push("Chronicle not yet rebuilt from session records");n.innerHTML=`<span class="cx2-grounding-txt">${escHtml(i.join(" \xB7 "))}</span>${s.has_undo?'<button type="button" class="btn-new btn-xs cx2-restore-desc">Restore previous</button>':""}`,n.classList.remove("hidden"),n.querySelector(".cx2-restore-desc")?.addEventListener("click",o=>_cx2RestoreDescription(t,o.currentTarget))}catch{}}function _cx2TeaseRowHtml(e){const t=(a,s)=>`<label class="cx2-tease-chk"><input type="checkbox" data-tease="${a}"${e[a]?" checked":""}><span>${s}</span></label>`,n=(a,s)=>`<label class="cx2-tease-chk cx2-tease-radio"><input type="radio" name="cx2-tease-name-${e.id}" data-tease-name="${a}"${a==="true"==!!e.tease_name?" checked":""}><span>${s}</span></label>`;return`<div class="cx2-tease-row" data-tease-for="${e.id}">
372            <span class="cx2-tease-hdr">Tease while staged</span>
373            <span class="cx2-tease-name">${n("true","True name")}${n("party","Party knows as")}<input type="text" class="cx2-tease-alias-in" data-tease-alias maxlength="80" placeholder="e.g. The Hooded Woman" value="${escHtml(e.tease_alias||"")}" autocomplete="off" aria-label="Party knows as"${e.tease_name?" disabled":""}></span>
374            ${t("tease_position","Position in the Faction Circle")}${t("tease_photo","Portrait")}
375            <span class="cx2-tease-help">Players see this much before you reveal the rest.</span>
376          </div>`}function _cx2WireTease(e,t){const n=e.querySelector(`.cx2-tease-row[data-tease-for="${t}"]`);if(!n)return;const a=async(i,o)=>{o&&(o.disabled=!0);try{const r=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${t}`,i),c=allEntities.findIndex(f=>f.id===t);c!==-1&&(allEntities[c]={...allEntities[c],...r&&typeof r=="object"?r:i});const l=c!==-1?allEntities[c]:null,d=e.querySelector(".cx2-staged-bar .gm-surface-seal");d&&l&&(d.innerHTML=(l.pending_reveal_private?"Only&nbsp;me":"Hidden")+(_teaseSummary(l)?" &middot; "+escHtml(_teaseSummary(l)):""));const u=n.querySelector("input[data-tease-alias]");u&&l&&(u.disabled=!!l.tease_name);const p=document.querySelector(`.cx2-rail-item[data-eid="${t}"] .tf-reveal-tag`);p&&l&&(p.outerHTML=_revealTagHtml(l));const m=l?_teaseSummary(l):"";showToast(m?"Players now see: "+_teaseParts(l).join(", ")+".":"Players see nothing of this entry until you reveal it.",3500)}catch(r){showToast("Save failed: "+(r&&r.message||"try again"))}finally{o&&(o.disabled=!1)}};n.querySelectorAll("input[data-tease]").forEach(i=>
376i.addEventListener("change",()=>a({[i.dataset.tease]:i.checked},i))),n.querySelectorAll("input[data-tease-name]").forEach(i=>i.addEventListener("change",()=>{if(!i.checked)return;const o={tease_name:i.dataset.teaseName==="true"},r=n.querySelector("input[data-tease-alias]");!o.tease_name&&r&&r.value.trim()&&(o.tease_alias=r.value.trim()),a(o,i),!o.tease_name&&r&&r.focus({preventScroll:!0})}));const s=n.querySelector("input[data-tease-alias]");if(s){let i=s.value;const o=()=>{const r=s.value.trim();r!==i&&(i=r,a({tease_alias:r||null},s))};s.addEventListener("change",o),s.addEventListener("keydown",r=>{r.key==="Enter"&&(r.preventDefault(),s.blur())})}}async function _entityReveal(e){const t=await api("POST",`/api/campaigns/${currentCampaign.id}/entities/${e}/reveal`,{}),n=allEntities.findIndex(s=>s.id===e);n!==-1&&(allEntities[n]={...allEntities[n],...t&&typeof t=="object"?t:{},pending_reveal:!1});const a=n!==-1&&allEntities[n]&&allEntities[n].name||t&&t.name||"Entity";return typeof window.tfToast=="function"&&typeof navigateToEntity=="function"?window.tfToast({type:"success",text:`Revealed \u201C${a}\u201D to players.`,action:{label:"View",onClick:()=>navigateToEntity(e)}}):showToast("Revealed to players."),t}let _cx2NotesEditing=null;function _cx2EditGmNotes(e,t){if(!e||e.dataset.editing==="1")return;e.dataset.editing="1",_cx2NotesEditing=t;const n=(allEntities||[]).find(r=>r.id===t),a=n&&n.gm_notes||"",s=document.createElement("textarea");s.className="gm-notes-ta cx2-prose",s.value=a,s.placeholder="Your private prep. Players never see this. Click away to save.",s.rows=Math.min(16,Math.max(4,a.split(`
377`).length+2)),e.style.display="none",e.insertAdjacentElement("afterend",s),s.focus();try{s.setSelectionRange(s.value.length,s.value.length)}catch{}let i=!1;const o=async r=>{if(i)return;i=!0,_cx2NotesEditing=null;const c=s.value.trim(),l=(a||"").trim();let d=l;if(!r&&c!==l)try{const u=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${t}`,{gm_notes:c||null});u&&u.id?allEntities=(allEntities||[]).map(p=>p.id===t?u:p):n&&(n.gm_notes=c||null),d=c}catch(u){showToast("Save failed: "+(u&&u.message||"try again")),d=l}s.remove(),e.dataset.editing="",e.style.display="",e.dataset.btsHost==="1"?typeof _btsAfterNote=="function"&&_btsAfterNote(e,t,d):_cx2OpenEid===t&&(e.innerHTML=`<span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>`+(d?_codex2RenderProse(d,t):'<span class="gm-notes-prompt">Your private prep: secrets, true motives, voice, stat reminders. Players never see this. Click to start typing&hellip;</span>'))};s.addEventListener("blur",()=>o(!1)),s.addEventListener("keydown",r=>{r.key==="Escape"?(r.preventDefault(),o(!0)):r.key==="Enter"&&(r.ctrlKey||r.metaKey)&&(r.preventDefault(),s.blur())})}let _cx2OpenEid=null;async function renderCodexEntry(e,t){t=t||{};const n=currentCampaign&&currentCampaign.id;if(!n)return;const a=_teaseView(allEntities.find(R=>R.id===e));if(!a||_cx2NotesEditing===e||_hiddenFromViewer(a))return;const s=_codex2CanEdit(a),i=_codex2IsGm()||!!(currentUser&&currentUser.is_admin),o=t.host||_codex2EnsureShell();if(o&&o._cx2Hero){try{o._cx2Hero.dispose()}catch{}o._cx2Hero=null}const r=o._cx2RenderToken=(o._cx2RenderToken||0)+1,c=()=>o._cx2RenderToken!==r,l=R=>String(R||"").replace(/[*_#`]+/g,"").trim(),d=l(a.bardic_flavor),u=l(a.bardic_take),p=a.bardic_status||"",m=_cx2BardicCycle(a,"flavor"),f=_cx2BardicCycle(a,"take"),g=_codex2WarrantsBardicTake(a),h=_codex2WarrantsSeal(a),v=!!d,b=!!u&&h,y=i&&g,_=i&&h,w=++_cx2SealFidSeq,$=`cx2ink-${w}`,E=`cx2dish-${w}`,k=t.host||_codex2EnsureShell();t.host||(document.querySelectorAll(".tab-panel").forEach(R=>R.classList.add("hidden")),document.querySelectorAll(".tab-btn").forEach(R=>R.classList.remove("active")),document.querySelector('[data-tab="compendium"]')?.classList.add("active"),k.classList.remove("hidden"),_tfStampCampaignHash(`#codex/${n}/${e}`),k._cx2RailCid!==n?(_codex2RenderRail(e),k._cx2RailCid=n):_codex2RailSetActive(e));let C=a.aliases;if(typeof C=="string")try{C=JSON.parse(C)}catch{C=[]}Array.isArray(C)||(C=[]);const S=a.name||"",q=S.indexOf(" "),T=(a.type==="pc"||a.type==="npc")&&!(a.reveal_masked&&!a.tease_name)&&q>0?`${escHtml(S.slice(0,q))}<span class="cx2-pipe">|</span>${escHtml(S.slice(q+1))}`:escHtml(S),P=(a.snippet||"").trim(),B=_codex2RenderProse(a.description||"",e),H=currentCampaign&&currentCampaign.name?escHtml(currentCampaign.name):"Codex",A='<svg class="led-mark" viewBox="0 0 556 290" aria-hidden="true"><use href="#tf-mark-compact"/></svg>',x='<svg viewBox="0 0 556 290" aria-hidden="true"><use href="#tf-mark-compact"/></svg>';let M=a.type||"entity";try{let R=a.dnd_beyond_data;
377if(typeof R=="string"&&(R=JSON.parse(R)),R&&R.system==="daggerheart"&&(R.class||R.subclass)){const U=[];R.class&&U.push(String(R.class).trim()),R.subclass&&U.push(String(R.subclass).trim()),M=U.join(" \xB7 ")}const Y=R&&R.classes;if(!(R&&R.system==="daggerheart")&&Array.isArray(Y)&&Y.length){const U=String(Y[0]).match(/^([A-Za-z][A-Za-z\s\/]*?)\s*\d*\s*(?:\(([^)]+)\))?\s*$/);M=U&&U[2]?U[1].trim()+" \xB7 "+U[2].trim():U?U[1].trim():String(Y[0])}}catch{}let D=!1,O=!1;if(a.type==="faction"||a.type==="item"||a.type==="location"){const R=_codex2EntityLedger(a),Y=R&&R.kind&&String(R.kind).trim()?String(R.kind).trim():null;let U=null;a.type==="item"?Y?U=Y:a.ref_item_kind&&(U=a.ref_item_kind,O=!0):a.type==="faction"?Y?U=Y:(U=a.ref_faction_kind||(a.ref_faction_role?String(a.ref_faction_role).replace(/\b\w/g,J=>J.toUpperCase()):null),U&&(O=!0)):U=Y,U&&(M=U,D=!0)}const N=s&&(a.type==="faction"||a.type==="item"||a.type==="location")&&(!O||a.type==="faction"),G=(a.type==="pc"||a.type==="companion")&&currentCampaign&&currentCampaign.party_name?currentCampaign.party_name:"";let F="";try{const R={},Y=/data-id="(\d+)"/g;let U;for(;U=Y.exec(B);){const Ie=+U[1];Ie&&Ie!==e&&(R[Ie]=(R[Ie]||0)+1)}const J=_codex2InDegree(),ce=(allEntities||[]).length,ge=ce<20?.025:.05,oe=[];Object.keys(R).forEach(Ie=>{const Pe=+Ie,Ge=allEntities.find(mt=>mt.id===Pe);if(!Ge||_codex2IsJunkFaction(Ge)||_hiddenFromViewer(Ge))return;const Ve=_teaseView(Ge);if(Ve.type==="companion"&&Ve.parent_entity_id===e)return;const Ze=Math.log((ce+1)/(1+(J[Pe]||0)));Ze<ge||oe.push([Ve,Math.log(1+R[Pe])*Ze])}),oe.sort((Ie,Pe)=>Pe[1]-Ie[1]);const xe={},Be=[];for(const Ie of oe){if(Be.length>=8)break;const Pe=Ie[0].type||"x";(xe[Pe]||0)>=3||(xe[Pe]=(xe[Pe]||0)+1,Be.push(Ie[0]))}F=Be.map(Ie=>_codex2RailItemHtml(Ie,null)).join("")}catch{}F||(F='<div class="cx2-ph-note cx2-ph-note--full">No connections recorded yet</div>');const W=await _codex2GetAppearances(n,e);if(c())return;const z=R=>typeof getSessionNum=="function"?getSessionNum(R)||0:R?R.id:0,V=W.map(R=>{const Y=(allSessions||[]).find(U=>U.id===R.session_id);return Y?Object.assign({},Y,{_note:R.note||""}):null}).filter(Boolean).sort((R,Y)=>z(R)-z(Y)),K=V[0]||null,se=V.length?V[V.length-1]:null,Q=_codex2BuildPills(a,s,V.length>0);let X="";const te=a.type==="pc"||a.type==="npc";te&&a.pronouns?X+=`<div class="cx2-ledger-row${s?" cx2-ledger-row--fillable":""}"${s?' data-pron-edit="1" title="Edit"':""}><span class="cx2-ledger-key">Pronouns</span><span class="cx2-ledger-val">${escHtml(a.pronouns)}</span></div>`:te&&s&&(X+='<div class="cx2-ledger-row cx2-ledger-row--fillable cx2-ledger-add" data-pron-edit="1"><span class="cx2-ledger-key">Pronouns</span><span class="cx2-ledger-addrow"><span class="cx2-ledger-add-cta">+ Add</span></span></div>');const le=_codex2KnownAs(S,C,a.type);le.length&&(X+=`<div class="cx2-ledger-row"><span class="cx2-ledger-key">Known as</span><span class="cx2-ledger-val">${escHtml(le.join(", "))}</span></div>`);const ue=_codex2BuildLedger(a,K,se,s,D?["kind"]:null),me=_cx2StandRowHtml(a,s);me&&(s||X||ue||_cx2StandKnown(a))&&(X+=me),X+=ue;const de=X.length>0,ye=_codex2QuoteCount(a),Se=V.length>0||ye>0?`<div class="cx2-tally">
378              <div class="cx2-tally-cell"><div class="cx2-tally-num">${V.length}</div><div class="cx2-tally-lab">Sessions</div></div>
379              <div class="cx2-tally-cell"><div class="cx2-tally-num">${ye}</div><div class="cx2-tally-lab">Quoted</div></div>
380            </div>`:"",he=_tfLedgerTokList(a),be=(()=>{if(a.type!=="pc"||!he.length)return null;const R=(allEntities||[]).filter(U=>U.type==="companion"&&U.parent_entity_id===a.id&&!_hiddenFromViewer(U)).map(U=>({ent:U,toks:_tfLedgerTokList(U)})).filter(U=>U.toks.length);if(!R.length)return null;const Y=R[Math.floor(Math.random()*R.length)];return{...Y,start:Math.floor(Math.random()*Y.toks.length)}})(),ne=(R,Y)=>{const U=Y?Y.px/R.px:1;return{pcW:U>1?Math.max(20,Math.round(96/U)):96,compW:Y?U>1?96:Math.max(20,Math.round(96*U)):0}},we=he.length?(()=>{const R=be?be.toks[be.start]:null,Y=ne(he[0],R);return`<div class="cx2-ledger-token" title="Battle-map token \u2014 click to download (VTT size)"><img src="${escHtml(he[0].sized)}" alt="${escHtml(S)} token" loading="lazy" style="--tok-w:${Y.pcW}px">`+(R?`<img class="cx2-ledger-token-comp" src="${escHtml(R.sized)}" alt="${escHtml(be.ent.name)}
380 token" title="${escHtml(be.ent.name)} \u2014 companion token, click to download" loading="lazy" style="--tok-w:${Y.compW}px">`:"")+"</div>"})():"",Me=s||de||we?`<div class="cx2-ledger">
381            ${we}
382            ${de?`<div class="cx2-ledger-eyebrow"><span class="led-title">${A}Ledger</span></div>${X}`:""}
383            ${Se}
384          </div>`:"",Ce=V.slice(-20),j=V.length-Ce.length,Z=a.type!=="pc"&&V.length?`<div class="cx2-panel">
385            <div class="cx2-panel-title">Appearances<span class="cx2-panel-count">${V.length}</span></div>
386            ${j>0?`<div class="cx2-ph-note">+${j} earlier session${j===1?"":"s"}</div>`:""}
387            ${Ce.map(R=>{const Y=typeof getSessionNum=="function"?getSessionNum(R):R.id,U=R._note||"";return`<div class="cx2-session cx2-session--link" data-session-id="${R.id}" title="Open in Chronicle"><span class="cx2-session-num"><strong>S${Y}</strong></span><span class="cx2-session-title">${escHtml(R.title||"Session "+Y)}${U?`<span class="blurb">${escHtml(_cx2CapFirst(U))}</span>`:""}</span></div>`}).join("")}
388          </div>`:"";let ie=[];try{typeof _hooksAllThreads<"u"&&Array.isArray(_hooksAllThreads)&&(ie=_hooksAllThreads.filter(R=>R&&Array.isArray(R.source_entity_ids)&&R.source_entity_ids.indexOf(e)!==-1&&R.user_status!=="dismissed"&&R.status!=="dormant").slice(0,3))}catch{}const pe=ie.length?`<div class="cx2-panel">
389            <div class="cx2-panel-title">Loose Threads</div>
390            ${ie.map(R=>{const Y=String(R.context||R.description||R.title||"").trim().split(/\s+/),U=Y.slice(0,8).join(" ")+(Y.length>8?"\u2026":""),J=_codex2IsGm();return`<div class="cx2-thread${J?" cx2-thread--link":""}"${J?` data-thread-id="${R.id}" title="Open in Fates"`:""}><span class="cx2-thread-glyph">&#9670;</span><span class="cx2-thread-text">${escHtml(_cx2CapFirst(R.title||"Untitled thread"))}
390${U?`<span class="cx2-thread-meta">${escHtml(U)}</span>`:""}</span></div>`}).join("")}
391          </div>`:"",re=a.type==="pc"?(allEntities||[]).filter(R=>R.type==="companion"&&R.parent_entity_id===a.id&&!_hiddenFromViewer(R)).sort((R,Y)=>(R.name||"").localeCompare(Y.name||"")):[],_e=a.type==="pc"&&(re.length||s)?`<div class="cx2-panel">
392            <div class="cx2-panel-title">Companions${re.length?`<span class="cx2-panel-count">${re.length}</span>`:""}${s?`<button type="button" class="cx2-comp-add" data-cx2-add-companion="${a.id}" title="Add a companion">+ Add</button>`:""}</div>
393            ${re.length?re.map(R=>`<div class="cx2-comp-row" data-eid="${R.id}" role="button" tabindex="0" title="Open ${escHtml(R.name)}">${R.image_crop_url||R.image_url?`<img class="cx2-comp-thumb" src="${escHtml(thumbUrl(R.image_crop_url||R.image_url))}" alt="" />`:'<span class="cx2-comp-thumb cx2-comp-thumb--glyph"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-companion"/></svg></span>'}<span class="cx2-comp-name">${escHtml(R.name)}</span></div>`).join(""):`<div class="cx2-ph-note">No companions yet. ${escHtml(_companionExplainer())}</div>`}
394          </div>`:"",fe=a.type==="faction"&&window.FactionMap?`<div class="cx2-panel cx2-fmap-panel"><div class="cx2-panel-title">Members</div>
395        <div class="cx2-fmap-host" data-fid="${a.id}"><p class="form-helper-text">Loading\u2026</p></div></div>`:"",$e=Me+_cx2PcStatBlock(a)+_e+fe+Z+pe+`<div class="entity-crosslinks-mount cx2-xlinks" data-entity-id="${e}"></div>`,Ee=["pc","npc","faction","deity","companion"].indexOf(a.type)!==-1?_codex2EntityQuotes(a,3):[],Ae=Ee.length?`<h2 class="cx2-section cx2-section--spaced">In Their Own Words</h2>
396          <div class="cx2-quotes">${Ee.map(R=>{const Y=`"${(R.quote||"").trim()}" \u2014 ${S}${R.sessionNum!=null?", Session "+R.sessionNum:""}${R.context?" ("+R.context+")":""}`,U=i&&R.id?`<span class="pc-quote-actions"><button class="btn-ghost pc-quote-edit-btn" data-quote-id="${R.id}" data-quote="${escHtml(R.quote)}" data-context="${escHtml(R.context||"")}" data-session-id="${R.session_id!=null?R.session_id:""}" title="Edit quote">\u270E</button><button class="btn-ghost pc-quote-reassign-btn" data-quote-id="${R.id}" title="Reassign quote">\u2197</button><button class="btn-ghost pc-quote-delete-btn" data-quote-id="${R.id}" title="Delete quote">\u2715</button></span>`:"";return`<blockquote class="cx2-quote pc-quote-row" data-quote-id="${R.id!=null?R.id:""}"><p class="cx2-quote-text">${escHtml(R.quote)}</p><div class="cx2-quote-attr">${escHtml(S)}${R.sessionNum!=null?" &middot; Session "+R.sessionNum:""}${R.context?" &middot; "+escHtml(R.context):""}</div><button type="button" class="tf-quote-share" data-copy="${escHtml(Y)}" title="Share this quote" aria-label="Share quote"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.3 13.4l7.4 4.2M15.7 6.4l-7.4 4.2"/></svg></button>${R.id!=null?_p2ReactionBar({id:R.id}):""}${U}</blockquote>`}).join("")}</div>`:"",ae=await _codex2GetSpec(n,e);if(c())return;let ee=ae||(a.image_url?{mode:"fullbleed",image_url:a.image_url}:null),ke=ee?ee.mode||"fullbleed":null;const Te=i&&_cx2BakeOwed(ee),Re=i&&!!(ee&&ee.bake_status==="failed")?`<span class="tf-badge tf-badge--amber cx2-bake-failed" title="${escHtml(_cx2BakeFailTip(ee.baked_treatment))}">Treatment didn\u2019t finish</span>`:"",et=!!(ee&&(ee.mode||"")==="dissolve"&&(ee.has_matte||ee.matte_url)&&!(ee.has_dist||ee.dist_urls)),tt=Te&&!(ee&&(ee.has_matte||ee.matte_url)&&(ee.baked_treatment!=="dissolve"||ee.has_dist||ee.dist_urls));et?(ee=Object.assign({},ee,{mode:"cutout"}),ke="cutout"):tt&&(ke="fullbleed");const nt=!!(a.image_url&&String(a.image_url).trim()),Ke=s?'<button type="button" class="cx2-hero-edit-btn" title="Edit image" aria-label="Edit image"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l4-4 3 3 4-5 3 4"/><circle cx="8.5" cy="9.5" r="1.1"/></svg></button>':"",at=s&&!nt?'<button type="button" class="cx2-add-image-cta" title="Add an image">+ Add image</button>':"",st=`<span class="cap-title">${escHtml(a.name)}</span>`,Oe=e%2===0?"left":"right";let He="cx2-portrait",qe="right";if(ke==="dissolve"){const R=ee.edge||"right";
396R==="left"?(He="cx2-portrait cx2-dis-left",qe="left"):R==="right"?(He="cx2-portrait cx2-dis-right",qe="right"):(He=`cx2-portrait cx2-dis-tb cx2-dis-tb--${Oe}`,qe=null)}else ke==="cutout"?(He=`cx2-portrait cx2-corner cx2-corner--${Oe}`,qe=Oe==="left"?"right":"left"):(He=`cx2-portrait cx2-corner cx2-corner--${Oe}`,qe=null);let je="";if(ee)if(ke==="fullbleed"){const R=Te?_cx2BakeOverlayHtml(ee.baked_treatment):"",Y=ee.label?`<span class="cap-label">${escHtml(ee.label)}</span>`:"";je=`<figure class="${He}${i?" cx2-portrait--editable":""}${Te?" cx2-portrait--baking":""}"><img alt="" src="${escHtml(ee.image_url)}">${R}${Re}${Ke}<figcaption class="cx2-cap">${st}${Y}<span class="credit">${H}</span></figcaption></figure>`}else{const R=Te?_cx2BakeOverlayHtml(ee.baked_treatment):"";je=`<figure class="${He}${i?" cx2-portrait--editable":""}${Te?" cx2-portrait--baking":""}"><canvas></canvas>${R}${Re}${Ke}</figure>`}else a.reveal_masked&&!a.tease_photo&&(je=`<figure class="cx2-portrait cx2-corner cx2-corner--${Oe} cx2-portrait--silhouette">${_tfSilhouetteHtml(a)}</figure>`,qe=null);let De="";if(i){if((a.type==="pc"||a.type==="npc")&&(De+=`<button class="actbtn cx2-toggle-dead" type="button" data-dead="${!!a.is_dead}" aria-label="${a.is_dead?"Revive":a.type==="pc"?"Mark Fallen":"Mark Dead"}" title="${a.is_dead?"Revive":a.type==="pc"?"Mark Fallen":"Mark Dead"}">${_CX2_ACT_ICO.skull}<span class="actbtn-lbl">${a.is_dead?"Revive":a.type==="pc"?"Mark Fallen":"Mark Dead"}</span></button>`),a.type==="npc"&&(allEntities||[]).some(R=>R.type==="pc")){const R=escHtml(_companionExplainer());De+=`<button class="actbtn cx2-set-companion" type="button" title="${R}" aria-label="Set as a PC's Companion">${_CX2_ACT_ICO.companion}<span class="actbtn-lbl">Set as a PC's Companion</span></button>`}a.type==="pc"&&(De+=`<button class="actbtn cx2-toggle-inactive" type="button" data-inactive="${!!a.is_inactive}" aria-label="${a.is_inactive?"Restore to Party":"Left Party"}" title="${a.is_inactive?"Restore to Party":"Left Party"}">${_CX2_ACT_ICO.depart}<span class="actbtn-lbl">${a.is_inactive?"Restore to Party":"Left Party"}</span></button>`,De+=`<button class="actbtn cx2-add-companion" type="button" title="Add a companion to this character" aria-label="Add Companion">${_CX2_ACT_ICO.companion}<span class="actbtn-lbl">Add Companion</span></button>`,De+=`<button class="actbtn cx2-rebuild" type="button" title="Rebuild this codex description from the per-session chronicle" aria-label="Rebuild Chronicle">${_CX2_ACT_ICO.refresh}<span class="actbtn-lbl">Rebuild Chronicle</span></button>`),a.type!=="pc"&&typeof _TF_TIDY_TYPES<"u"&&_TF_TIDY_TYPES.has(a.type)&&(De+=`<button class="actbtn cx2-tidy" type="button" title="Reorganize this entry into short sections. The current text is kept for Restore" aria-label="Tidy Entry">${_CX2_ACT_ICO.tidy}<span class="actbtn-lbl">Tidy Entry</span></button>`),De+=`<button class="actbtn cx2-ftr" type="button" title="Correct the campaign record about ${escHtml(a.name||"this entry")}" aria-label="Set the Record Straight">${_CX2_ACT_ICO.quill}<span class="actbtn-lbl">Set the Record Straight</span></button>`}const We=a.type==="pc"&&!!a.owned_by_me,it=a.type==="pc"&&(i||We),ze=a.type==="pc"&&!i&&!We&&!!a.assignable&&!!(currentUser&&currentUser.id)&&!_isReadOnlyRole(),ot=it?`<button class="actbtn cx2-import-sheet" type="button" title="${a.pc_import_at?"Update this character sheet from a photo/PDF (stats only, your notes and portrait are kept)":"Import a character sheet from a photo or PDF"}" aria-label="${a.pc_import_at?"Update Sheet":"Import Sheet"}">${_CX2_ACT_ICO.sheet}<span class="actbtn-lbl">${a.pc_import_at?"Update Sheet":"Import Sheet"}</span></button>`:ze?`<button class="actbtn cx2-claim-pc" type="button" title="Claim this character as yours so you can keep its sheet current, your GM can reassign it" aria-label="Claim This Character">${_CX2_ACT_ICO.sheet}<span class="actbtn-lbl">Claim This Character</span></button>`:"",rt=i&&a.pending_reveal?`<button class="actbtn cx2-reveal" type="button" title="Reveal to players" aria-label="Reveal to players">${_CX2_ACT_ICO.eyeRays}<span class="actbtn-lbl">Reveal</span></button>`:"",ct=i&&a.pending_reveal?`
397        <div class="gm-surface cx2-gm-block cx2-staged-bar" role="status">
398          <div class="gm-surface-head">
399            <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.eyeClosed}</span>
400            <span class="gm-surface-label">Stage for Reveal</span>
401            <button class="gm-reveal cx2-reveal" type="button" title="Reveal to players">${_CX2_ACT_ICO.eyeRays}<span>Reveal</span></button>
402            <span class="gm-surface-seal">${a.pending_reveal_private?"Only&nbsp;me":"Hidden"}${_teaseSummary(a)?" &middot; "+escHtml(_teaseSummary(a)):""}</span>
403          </div>
404          <p class="gm-surface-lead">${a.pending_reveal_private?"Staged privately: only you and admins can see this entry. It enters the Codex for players when you reveal it.":"Hidden from players: staged to drop into the Codex when you reveal it. Your co-GMs can see it while it&rsquo;s hidden."}</p>
405          ${_cx2TeaseRowHtml(a)}
406        </div>`:"",Ye=(a.owner_private_notes||"").trim(),lt=Ye&&(i||We)?`
407          <div class="gm-surface gm-surface--staged cx2-gm-block" role="group" aria-label="Backstory: restricted to the GM and the owning player">
408            <div class="gm-surface-head">
409              <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.book}</span>
410              <span class="gm-surface-label">${i?"Backstory":"Your Backstory"}</span>
411              <span class="gm-surface-seal">GM&nbsp;+&nbsp;PC</span>
412            </div>
413            <p class="gm-surface-lead">${i?"Only you and your player can see this: it surfaces in play when the story gets there.":"Only you and your GM can see this: it surfaces in play when the story gets there."}</p>
414            <div class="gm-surface-body cx2-prose gm-bs-prose">
415              <span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.book}</span>
416              ${_codex2RenderProse(Ye,e)}
417            </div>
418          </div>`:"",Qe=(a.gm_notes||"").trim(),dt=i?`
419          <div class="gm-surface cx2-gm-block" role="group" aria-label="Behind the Screen \xB7 GM only">
420            <div class="gm-surface-head">
421              <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>
422              <span class="gm-surface-label">Behind the Screen</span>
423              <span class="gm-surface-seal">GM&nbsp;only</span>
424            </div>
425            <p class="gm-surface-lead">The table never sees this. The public description above is built blind to this box, so only what comes up in play ever reaches it.</p>
426            ${a.type==="npc"?_cx2WantsHtml(a):""}
427            <div class="gm-surface-body cx2-prose gm-bs-prose gm-notes-display" data-eid="${e}" tabindex="0" role="textbox" aria-multiline="true" aria-label="Behind the Screen notes: click to edit" title="Click to edit, saves when you click away">
428              <span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>
429              ${Qe?_codex2RenderProse(Qe,e):'<span class="gm-notes-prompt">Your private prep: secrets, true motives, voice, stat reminders. Players never see this. Click to start typing&hellip;</span>'}
430            </div>
431          </div>`:"",Je=a.type?a.type==="pc"||a.type==="npc"?a.type.toUpperCase():a.type.charAt(0).toUpperCase()+a.type.slice(1):"Entity",ut=i&&!a.pending_reveal&&typeof _discordRevealReady=="function"&&_discordRevealReady()?`<button class="cx2-sheet-cta cx2-sheet-cta--discord cx2-discord-share" type="button" aria-label="Post ${escHtml(S)}'s codex card to Discord" title="Post ${escHtml(S)}'s codex card to your Discord channel \u2014 no reveal, just a drop for the table">${DISCORD_GLYPH}</button>`:"",pt=`
432    <div class="cx2-stage">
433      <div class="cx2-article">
434        <div class="cx2-crumb">
435          <button class="cx2-back2" type="button">&lsaquo; Codex</button>
436          <span class="sep">\xB7</span><span class="cur">${escHtml(S)}</span>
437          ${s||ze?`<span class="actions">${rt}${i?`<button class="actbtn cx2-merge" type="button" title="Merge this entry into another" aria-label="Merge">${_CX2_ACT_ICO.merge}<span class="actbtn-lbl">Merge</span></button>`:""}${s?`<button class="actbtn cx2-edit" type="button" title="Edit" aria-label="Edit">${_CX2_ACT_ICO.edit}<span class="actbtn-lbl">Edit</span></button>`:""}${ot}${De}${i?`<button class="actbtn cx2-del cx2-del--danger" type="button" title="Delete this entry" aria-label="Delete">${_CX2_ACT_ICO.trash}<span class="actbtn-lbl">Delete</span></button>`:""}</span>`:""}
438        </div>
439        ${s||ze?`<div class="cx2-sheet-cta-row">${ut}<button class="cx2-sheet-cta cx2-make-sheet" type="button" aria-label="Make a shareable ${Je} Sheet" title="Make a shareable ${Je} Sheet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.3 13.4l7.4 4.2M15.7 6.4l-7.4 4.2"/></svg></button></div>`:""}
440        ${ct}
441        ${i&&a.type==="pc"?`<div class="cx2-grounding-note hidden" data-eid="${e}"></div>`:""}
442        ${i&&a.type!=="pc"&&a.previous_description?`<div class="cx2-grounding-note cx2-tidy-note" data-eid="${e}"><span class="cx2-grounding-txt">Previous version saved</span><button type="button" class="btn-new btn-xs cx2-restore-desc">Restore previous</button></div>`:""}
443        <div class="cx2-title-block">
444          ${b?`<div class="cx2-stamp${i?" cx2-editable":""}" title="Bard's Take${i?", hover to edit":""}">
445            <svg class="blob" viewBox="0 0 120 120" aria-hidden="true">
446              <defs>
447                <filter id="${$}" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
448                  <!-- Stamped premium-gold wax seal (finalized 2026-05-30). ONE themed colour
449                       (var(--color-gold)); ALL depth = light/shadow modulating it. (A) poured ridged
450                       edge; (a) deepen via multiply of a darker shade of the same gold; (b) strong rim
451                       shadow; (d) inner contact vignette; (c) one sharp UL specular pinpoint = the "wet"
452                       tell. multiply darkens / screen lightens \u2014 never opaque-covers the gold. -->
453                  <feTurbulence type="fractalNoise" baseFrequency="0.03 0.034" numOctaves="3" seed="7" result="edgeNoise"/>
454                  <feDisplacementMap in="SourceGraphic" in2="edgeNoise" scale="14" xChannelSelector="R" yChannelSelector="G" result="wax"/>
455                  <feFlood flood-color="#4A3D1E" flood-opacity="0.30" result="deepColor"/>
456                  <feComposite in="deepColor" in2="wax" operator="in" result="deepen"/>
457                  <feMorphology in="wax" operator="erode" radius="6" result="coreErode"/>
458                  <feComposite in="wax" in2="coreErode" operator="out" result="rimBand"/>
459                  <feGaussianBlur in="rimBand" stdDeviation="2.2" result="rimSoft"/>
460                  <feOffset in="rimSoft" dx="2.2" dy="2.2" result="rimShShift"/>
461                  <feComposite in="rimShShift" in2="wax" operator="in" result="rimShMask"/>
462                  <feFlood flood-color="#150b00" flood-opacity="0.55" result="shColor"/>
463                  <feComposite in="shColor" in2="rimShMask" operator="in" result="rimSh"/>
464                  <feMorphology in="wax" operator="erode" radius="3" result="vErodeOuter"/>
465                  <feMorphology in="wax" operator="erode" radius="11" result="vErodeInner"/>
466                  <feComposite in="vErodeOuter" in2="vErodeInner" operator="out" result="vBand"/>
467                  <feGaussianBlur in="vBand" stdDeviation="4" result="vSoft"/>
468                  <feComposite in="vSoft" in2="wax" operator="in" result="vMask"/>
469                  <feFlood flood-color="#241803" flood-opacity="0.22" result="vColor"/>
470                  <feComposite in="vColor" in2="vMask" operator="in" result="vignette"/>
471                  <feMorphology in="wax" operator="erode" radius="2" result="glintCoreErode"/>
472                  <feComposite in="wax" in2="glintCoreErode" operator="out" result="glintRing"/>
473                  <feGaussianBlur in="glintRing" stdDeviation="0.5" result="glintSharp"/>
474                  <feOffset in="glintSharp" dx="-3.2" dy="-3.6" result="glintShift"/>
475                  <feComposite in="glintShift" in2="wax" operator="in" result="glintMask"/>
476                  <feFlood flood-color="#fffdf4" flood-opacity="0.95" result="glintColor"/>
477                  <feComposite in="glintColor" in2="glintMask" operator="in" result="glintFull"/>
478                  <feOffset in="coreErode" dx="6" dy="6" result="ulGate"/>
479                  <feComposite in="glintFull" in2="ulGate" operator="out" result="glint"/>
480                  <feMerge result="merged"><feMergeNode in="wax"/></feMerge>
481                  <feBlend in="deepen" in2="merged" mode="multiply" result="s1"/>
482                  <feBlend in="vignette" in2="s1" mode="multiply" result="s2"/>
483                  <feBlend in="rimSh" in2="s2" mode="multiply" result="s3"/>
484                  <feBlend in="glint" in2="s3" mode="screen" result="s4"/>
485                  <feComposite in="s4" in2="wax" operator="in"/>
486                </filter>
487                <filter id="${E}" x="-40%" y="-40%" width="180%" height="180%" color-interpolation-filters="sRGB">
488                  <!-- Perfect CIRCLE recess: continuous round groove + directional bias (darker UL wall,
489                       faint catch-light BR). Outputs only crescents \u2014 the wax gold shows through. -->
490                  <feMorphology in="SourceAlpha" operator="erode" radius="4" result="grooveErode"/>
491                  <feComposite in="SourceAlpha" in2="grooveErode" operator="out" result="grooveRing"/>
492                  <feGaussianBlur in="grooveRing" stdDeviation="3.2" result="grooveSoft"/>
493                  <feComposite in="grooveSoft" in2="SourceAlpha" operator="in" result="grooveMask"/>
494                  <feFlood flood-color="#1a1003" flood-opacity="0.34" result="grooveColor"/>
495                  <feComposite in="grooveColor" in2="grooveMask" operator="in" result="groove"/>
496                  <feOffset in="grooveSoft" dx="3" dy="3.2" result="ulShift"/>
497                  <feComposite in="ulShift" in2="SourceAlpha" operator="in" result="ulMask"/>
498                  <feFlood flood-color="#0f0900" flood-opacity="0.34" result="ulColor"/>
499                  <feComposite in="ulColor" in2="ulMask" operator="in" result="ulShade"/>
500                  <feOffset in="grooveSoft" dx="-3" dy="-3.2" result="brShift"/>
501                  <feComposite in="brShift" in2="SourceAlpha" operator="in" result="brMask"/>
502                  <feFlood flood-color="#fff3d4" flood-opacity="0.18" result="brColor"/>
503                  <feComposite in="brColor" in2="brMask" operator="in" result="brCatch"/>
504                  <feMerge><feMergeNode in="groove"/><feMergeNode in="ulShade"/><feMergeNode in="brCatch"/></feMerge>
505                </filter>
506              </defs>
507              <path class="wax" filter="url(#${$})" d="M60 8 C82 8 96 16 104 34 C112 50 114 64 108 82 C102 100 84 112 60 112 C36 112 18 100 12 82 C6 64 8 50 16 34 C24 16 38 8 60 8 Z"/>
508              <circle class="dish" cx="60" cy="60" r="43" filter="url(#${E})"/>
509            </svg>
510            <div class="sc">
511              <svg class="sc-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 4s-7 1-12 6c-3 3-4 9-4 9s6-1 9-4c5-5 7-11 7-11z"/><path d="M4 19l5-5"/></svg>
512              <span class="sc-read">BARD&rsquo;S TAKE</span>
513              <span class="sc-word cx2-take-word">${escHtml(_cx2CapFirst(u))}</span>
514            </div>
515            ${i?`<div class="cx2-ai-tray cx2-stamp-tray">${!f.locked&&f.count>1?'<button class="cx2-ai-act cycle" type="button" data-scyc="cycle" title="Cycle to another word"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4v5h-5"/></svg></button>':""}<button class="cx2-ai-act lock${f.locked?" is-locked":""}" type="button" data-scyc="${f.locked?"unlock":"lock"}" title="${f.locked?"Locked. Recaps won\u2019t change this. Click to unlock.":"Unlocked. A recap may refresh this. Click to lock."}">${f.locked?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 7-1.4"/></svg>'}</button><button class="cx2-ai-act edit" type="button" data-scyc="edit" title="Edit (locks this word)">&#9998;</button><button class="cx2-ai-act reject" type="button" data-scyc="delete" title="Remove this word from the cycle">&#10005;</button></div>`:""}
516          </div>`:_?`<button class="cx2-bardic-addchip" type="button" data-badd="take" title="Add the Bard's one-word take">+ Bard&rsquo;s Take</button>`:""}
517          <div class="cx2-eyebrow2">
518            <span class="cx2-flag${N?" cx2-flag--editable":""}"${N?' data-kind-edit="1" title="Edit Kind"':""}>${escHtml(M)}</span>
519            <span class="cx2-ey-rule"></span>
520            ${G?`<span class="cx2-ey-kicker">${escHtml(G)}</span>`:""}
521          </div>
522          ${(()=>{const R=J=>/^\d+$/.test(String(J))?+J:_tfRomanToNum(J)||9999,Y=(currentCampaign&&currentCampaign.company_deeds||[]).filter(J=>J&&J.honorific&&(J.honorees||[]).includes(a.id)).map((J,ce)=>({t:J.honorific,o:R(J.chapter),i:ce})).sort((J,ce)=>J.o-ce.o||J.i-ce.i),U=[];return Y.forEach(J=>{U.includes(J.t)||U.push(J.t)}),a.honorific&&!U.includes(a.honorific)&&U.push(a.honorific),U.length?`<div class="cx2-honorific" title="Granted on the company's Honors Roll">${U.map(_tfHonorRibbonSvg).join("")}</div>`:""})()}
523          <h1 class="cx2-title${a.reveal_masked&&!a.tease_name?" cx2-title--alias":""}">${T}</h1>
524          ${P?`<p class="cx2-epithet">&mdash; ${escHtml(_cx2CapFirst(P))}</p>`:""}
525          <div class="cx2-meta">
526            ${Q}
527          </div>
528        </div>
529        <div class="cx2-main">
530          ${v?`<div class="cx2-bardic${m.locked?" cx2-bardic--locked":""}">
531            <svg class="cx2-quill" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 4s-7 1-12 6c-3 3-4 9-4 9s6-1 9-4c5-5 7-11 7-11z"/><path d="M4 19l5-5"/></svg>
532            ${i?`<div class="cx2-bardic-review">${!m.locked&&m.count>1?'<button class="cx2-br-icon cycle" type="button" data-bcyc="cycle" title="Cycle to another line"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4v5h-5"/></svg></button>':""}<button class="cx2-br-icon lock${m.locked?" is-locked":""}" type="button" data-bcyc="${m.locked?"unlock":"lock"}" title="${m.locked?"Locked. Recaps won\u2019t change this. Click to unlock.":"Unlocked. A recap may refresh this. Click to lock."}">${m.locked?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 7-1.4"/></svg>'}</button><button class="cx2-br-icon edit" type="button" data-bcyc="edit" title="Edit (locks this line)">&#9998;</button><button class="cx2-br-icon reject" type="button" data-bcyc="delete" title="Remove this line from the cycle">&#10005;</button></div>`:""}
533            <p class="cx2-bardic-flavor">${_bardicFlavorHtml(d)}</p>
534            <div class="cx2-bardic-attr">&mdash;
534 ${escHtml(_bardicByline("Bardic Companion"))}${i&&m.count>1?`<span class="cx2-bardic-pos">${m.idx+1} / ${m.count}</span>`:""}</div>
535          </div>`:y?'<button class="cx2-bardic-add" type="button" data-badd="flavor">&#9998; Add the Bard&rsquo;s framing</button>':""}
536          <div class="cx2-record-hdr"><h2 class="cx2-section">The Record</h2>${at}</div>
537          <div class="cx2-record-body">
538          ${je}
539          <div class="cx2-prose">${B}</div>
540          </div>
541          ${lt}
542          ${dt}
543          ${Ae}
544          <h2 class="cx2-section cx2-section--spaced">Connections</h2>
545          <div class="cx2-connections">${F}</div>
546          ${a.type==="pc"?`<div class="cx2-moments-mount" data-entity-id="${e}"></div>`:""}
547        </div>
548        <aside class="cx2-side">${$e}</aside>
549        <div class="cx2-colophon"><span class="mark">${x} Threadfall Codex</span><span>${H}</span></div>
550      </div>
551    </div>`,ve=k.querySelector(".cx2-right"),Xe=e===_cx2OpenEid&&!t.toTop||t.preserveScroll?ve.scrollTop:null;_cx2OpenEid=e,ve.innerHTML='<button class="cx2-close" type="button" title="Close, back to glossary">&times;</button>'+pt,ve.scrollTop=Xe??0,ve.querySelector(".cx2-close").onclick=_codex2Back,(a.type==="npc"||a.type==="faction")&&_cx2FillStanding(e),k.querySelector(".cx2-back2").onclick=_codex2Back,k.querySelectorAll(".cx2-edit").forEach(R=>{R.onclick=()=>{try{openEntityModal(e)}catch{}}}),k.querySelectorAll(".cx2-merge").forEach(R=>{R.onclick=()=>{try{openMergeModal(e)}catch{}}}),k.querySelectorAll(".cx2-del").forEach(R=>{R.onclick=async()=>{const Y=(allEntities.find(U=>U.id===e)||{}).name||"this entry";if(await confirmDialog({title:"Delete Entity",message:`Delete "${Y}"? This cannot be undone.`,confirmLabel:"Delete"}))try{await api("DELETE",`/api/campaigns/${n}/entities/${e}`),allEntities=allEntities.filter(J=>J.id!==e);const U=document.getElementById("tab-codex2");U&&(U._cx2RailCid=null),_codex2Back()}catch(U){showToast("Delete failed: "+(U&&U.message?U.message:U),6e3)}}}),k.querySelector(".cx2-toggle-dead")?.addEventListener("click",async()=>{try{await _entityToggleDead(e)&&(renderCompendium(),_cx2RerenderEntry(e))}catch(R){showToast("Failed: "+(R&&R.message||"try again"))}}),k.querySelector(".cx2-toggle-inactive")?.addEventListener("click",async()=>{try{await _entityToggleInactive(e)&&(renderCompendium(),_cx2RerenderEntry(e))}catch(R){showToast("Failed: "+(R&&R.message||"try again"))}}),k.querySelector(".cx2-import-sheet")?.addEventListener("click",()=>{try{_openImportReview(e)}catch{}}),k.querySelector(".cx2-ftr")?.addEventListener("click",()=>{try{openForTheRecord({entityId:e})}catch{}}),k.querySelectorAll("[data-sb-edit]").forEach(R=>R.addEventListener("click",()=>{try{_openEditStatsModal(parseInt(R.dataset.sbEdit))}catch{}})),k.querySelectorAll("[data-cx2-sheet-pc]").forEach(R=>R.addEventListener("click",Y=>{Y.stopPropagation(),Y.preventDefault();const U=parseInt(R.dataset.cx2SheetPc);U&&typeof _pcSheetControlClick=="function"&&_pcSheetControlClick(U)})),k.querySelectorAll("[data-sb-demiplane]").forEach(R=>R.addEventListener("click",()=>{try{const Y=parseInt(R.dataset.sbDemiplane);_openImportReview(Y),typeof _importOpenSourcePanel=="function"&&_importOpenSourcePanel("demiplane")}catch{}})),k.querySelector(".cx2-claim-pc")?.addEventListener("click",async R=>{const Y=currentCampaign&&currentCampaign.id;if(Y){R.currentTarget.disabled=!0;try{await api("POST",`/api/campaigns/${Y}/entities/${e}/claim`),_afterPcClaimed(e),typeof showToast=="function"&&showToast("\u2713 Claimed: you can now keep this sheet current",4e3),_cx2RerenderEntry(e)}catch(U){R.currentTarget.disabled=!1,typeof showToast=="function"&&showToast("Claim failed: "+(U.message||U),4e3)}}}),k.querySelectorAll("[data-sb-claim]").forEach(R=>R.addEventListener("click",async Y=>{Y.stopPropagation(),Y.preventDefault();const U=parseInt(R.dataset.sbClaim),J=currentCampaign&&currentCampaign.id;if(!(!U||!J)){R.disabled=!0;try{await api("POST",`/api/campaigns/${J}/entities/${U}/claim`),_afterPcClaimed(U),typeof showToast=="function"&&showToast("\u2713 Claimed: you can now keep this sheet current",4e3),_cx2RerenderEntry(U)}catch(ce){R.disabled=!1,typeof showToast=="function"&&showToast("Claim failed: "+(ce.message||ce),4e3)}}})),k.querySelector(".cx2-add-companion")?.addEventListener("click",()=>{try{openEntityModal(null,e)}catch{}}),k.querySelector(".cx2-set-companion")?.addEventListener("click",()=>{try{openEntityModal(e,null,"companion")}catch{}}),k.querySelector(".cx2-make-sheet")?.addEventListener("click",()=>{typeof _openEntitySheetModal=="function"&&_openEntitySheetModal(e)}),k.querySelector(".cx2-discord-share")?.addEventListener("click",async R=>{const Y=R.currentTarget,U=currentCampaign&&currentCampaign.id;if(!(!U||Y.disabled)){Y.disabled=!0;try{await api("POST",`/api/campaigns/${U}/entities/${e}/discord-share`,{}),typeof showToast=="function"&&showToast(`Posted to Discord \u2014 ${a.name}'s codex card is in the channel.`,4e3)}catch(J){typeof showToast=="function"&&showToast("Discord post failed: "+(J&&J.message||"Unknown"),5e3)}Y.disabled=!1}}),k.querySelector(".cx2-rebuild")?.addEventListener("click",async R=>{await _entityRebuildChronicle(e,R.currentTarget)&&(renderCompendium(),_cx2RerenderEntry(e))}),k.querySelector(".cx2-tidy")?.addEventListener("click",async R=>{await _entityTidy(e,R.currentTarget)&&(renderCompendium(),_cx2RerenderEntry(e))}),k.querySelector(".cx2-tidy-note .cx2-restore-desc")?.addEventListener("click",R=>_cx2RestoreDescription(e,R.currentTarget)),_cx2LoadGroundingNote(k,e),k.querySelectorAll(".cx2-reveal").forEach(R=>R.addEventListener("click",async()=>{try{await _entityReveal(e),renderCompendium(),_cx2RerenderEntry(e)}catch(Y){showToast("Reveal failed: "+(Y&&Y.message||"try again"))}})),_cx2WireTease(k,e),k.querySelectorAll(".gm-notes-display").forEach(R=>{R.addEventListener("click",()=>_cx2EditGmNotes(R,e)),R.addEventListener("keydown",Y=>{(Y.key==="Enter"||Y.key===" ")&&(Y.preventDefault(),_cx2EditGmNotes(R,e))})});const Fe=ve.querySelector(".cx2-quotes");i&&Fe&&_rewireQuoteButtons(Fe,e,()=>_cx2RerenderEntry(e)),Fe&&typeof _qcApplyCardBadges=="function"&&_qcApplyCardBadges(),Fe&&_p2HydrateReactions(Fe,n);try{_renderEntityCrosslinks(e)}catch{}if(a.type==="pc")try{_cx2RenderHeroMoments(e)}catch{}if(k.querySelectorAll(".cx2-connections .cx2-rail-item[data-eid]").forEach(R=>{R.onclick=()=>{const Y=parseInt(R.dataset.eid);Y&&renderCodexEntry(Y)}}),k.querySelectorAll(".cx2-comp-row[data-eid]").forEach(R=>{const Y=()=>{const U=parseInt(R.dataset.eid);U&&renderCodexEntry(U)};R.onclick=Y,R.onkeydown=U=>{(U.key==="Enter"||U.key===" ")&&(U.preventDefault(),Y())}}),k.querySelectorAll("[data-cx2-add-companion]").forEach(R=>{R.onclick=Y=>{Y.stopPropagation(),openEntityModal(null,parseInt(R.dataset.cx2AddCompanion))}}),a.type==="faction"&&window.FactionMap&&_cx2FmapLoad(n,a.id),ve.querySelectorAll(".cx2-thread--link[data-thread-id]").forEach(R=>{R.onclick=()=>{const Y=parseInt(R.dataset.threadId);Y&&_codex2GoToThread(Y)}}
551),ve.querySelectorAll(".cx2-session--link[data-session-id]").forEach(R=>{R.onclick=()=>{const Y=parseInt(R.dataset.sessionId);Y&&_codex2GoToSession(Y)}}),s){if(_codex2AddPen(ve,".cx2-epithet",e,"snippet",!1,()=>a.snippet||""),_codex2AddPen(ve,".cx2-prose",e,"description",!0,()=>a.description||"",!0),i){const U=ve.querySelector(".cx2-bardic-flavor");ve.querySelectorAll(".cx2-bardic-review [data-bcyc]").forEach(ce=>{ce.onclick=ge=>{ge.stopPropagation();const oe=ce.dataset.bcyc;oe==="edit"&&U?_codex2InlineEdit(U,{entityId:e,multiline:!0,value:a.bardic_flavor||"",placeholder:"The Bard's framing of this entry\u2026",onSave:xe=>_codex2BardicCycleAction(e,"edit",xe)}):(oe==="cycle"||oe==="lock"||oe==="unlock"||oe==="delete")&&_codex2BardicCycleAction(e,oe)}}),ve.querySelectorAll(".cx2-stamp .blob, .cx2-stamp .sc-glyph").forEach(ce=>ce.style.pointerEvents="none");const J=ve.querySelector(".cx2-take-word");ve.querySelectorAll(".cx2-stamp-tray [data-scyc]").forEach(ce=>{ce.onclick=ge=>{ge.stopPropagation();const oe=ce.dataset.scyc;oe==="edit"&&J?_codex2InlineEdit(J,{entityId:e,multiline:!1,value:a.bardic_take||"",placeholder:"one word",onSave:xe=>_codex2BardicCycleAction(e,"edit",xe,"take")}):(oe==="cycle"||oe==="lock"||oe==="unlock"||oe==="delete")&&_codex2BardicCycleAction(e,oe,null,"take")}}),ve.querySelectorAll("[data-badd]").forEach(ce=>{ce.onclick=ge=>{ge.stopPropagation(),ce.dataset.badd==="take"?_codex2InlineEdit(ce,{entityId:e,multiline:!1,value:a.bardic_take||"",placeholder:"one word",onSave:oe=>_codex2BardicCycleAction(e,"edit",oe,"take")}):_codex2InlineEdit(ce,{entityId:e,multiline:!0,value:a.bardic_flavor||"",placeholder:"The Bard's framing\u2026",onSave:oe=>_codex2BardicCycleAction(e,"edit",oe)})}})}ve.querySelectorAll("[data-pron-edit]").forEach(U=>{U.addEventListener("click",J=>
551{J.stopPropagation(),_codex2InlineEdit(U,{entityId:e,field:"pronouns",value:a.pronouns||"",multiline:!1,placeholder:"e.g. she/her"})})}),ve.querySelectorAll("[data-kind-edit]").forEach(U=>{U.addEventListener("click",J=>{J.stopPropagation();const ce=_codex2EntityLedger(a),ge=a.type==="location"?"Kind (e.g. City, Tavern)":a.type==="item"?"Kind (e.g. Wondrous Item)":"Kind (e.g. Criminal Syndicate)";_codex2InlineEdit(U,{entityId:e,ledgerKey:"kind",value:ce.kind!=null?String(ce.kind):"",multiline:!1,placeholder:ge})})}),ve.querySelectorAll(".cx2-ledger-row--fillable").forEach(U=>{const J=U.getAttribute("data-lkey");if(!J)return;const ce=U.getAttribute("data-llabel")||J;U.addEventListener("click",ge=>{if(ge.target.closest(".entity-link"))return;ge.stopPropagation();const oe=_codex2EntityLedger(a),xe=oe[J]!=null?String(oe[J]):"";_codex2InlineEdit(U,{entityId:e,ledgerKey:J,value:xe,multiline:!1,placeholder:ce})})}),window._tfLedgerTokTimer&&(clearInterval(window._tfLedgerTokTimer),window._tfLedgerTokTimer=null);const R=ve.querySelector(".cx2-ledger-token");if(R){let U=0,J=be?be.start:0;const ce=R.querySelector("img:not(.cx2-ledger-token-comp)"),ge=R.querySelector(".cx2-ledger-token-comp"),oe=be?be.toks:[];(he.length>1||oe.length>1)&&ce&&(window._tfLedgerTokTimer=setInterval(()=>{if(!document.body.contains(ce)){clearInterval(window._tfLedgerTokTimer),window._tfLedgerTokTimer=null;return}if(U=(U+1)%he.length,oe.length>1){let Be;do Be=Math.floor(Math.random()*oe.length);while(Be===J);J=Be}const xe=ne(he[U],oe.length?oe[J]:null);he.length>1&&ce.classList.add("cx2-ledger-token-fading"),ge&&oe.length>1&&ge.classList.add("cx2-ledger-token-fading"),setTimeout(()=>{he.length>1&&(ce.onload=()=>ce.classList.remove("cx2-ledger-token-fading"),ce.src=he[U].sized),ce.style.setProperty("--tok-w",xe.pcW+"px"),ge&&oe.length>1&&(ge.onload=()=>ge.classList.remove("cx2-ledger-token-fading"),ge.src=oe[J].sized),ge&&oe.length&&ge.style.setProperty("--tok-w",xe.compW+"px")},280)},4e3)),R.addEventListener("click",xe=>{xe.stopPropagation(),ge&&xe.target===ge&&oe.length?_tfTokenDownload(be.ent,{token:oe[J].raw,size:oe[J].size}):_tfTokenDownload(a,{token:he[U].raw,size:he[U].size})})}ve.querySelectorAll(".cx2-led-hide").forEach(U=>U.onclick=J=>{J.stopPropagation(),_codex2HideLedgerField(e,U.dataset.lhide,!0)}),ve.querySelectorAll(".cx2-led-show").forEach(U=>U.onclick=J=>{J.stopPropagation(),_codex2HideLedgerField(e,U.dataset.lshow,!1)});const Y=ve.querySelector(".cx2-led-showhidden");Y&&(Y.onclick=U=>{U.stopPropagation();const J=ve.querySelector(".cx2-led-hidden-wrap");J&&J.classList.toggle("hidden"),Y.classList.toggle("on")}),ve.querySelectorAll(".cx2-pill--fillable").forEach(U=>{const J=U.getAttribute("data-pkey");if(!J)return;const ce=U.getAttribute("data-plabel")||J;U.addEventListener("click",ge=>{ge.stopPropagation();const oe=_codex2EntityLedger(a),xe=oe[J]!=null?String(oe[J]):"",Be=_CX2_PILL_OPTIONS[J];Be?_codex2InlinePick(U,{entityId:e,ledgerKey:J,options:Be,current:xe}
551):_codex2InlineEdit(U,{entityId:e,ledgerKey:J,value:xe,multiline:!1,placeholder:ce})})})}if(ee&&ke!=="fullbleed"){const R=k.querySelector(".cx2-portrait"),Y=R&&R.querySelector("canvas");if(Y&&window.CodexHero){const U=CodexHero.mount(Y);if(U){k._cx2Hero=U;try{await U.show(ee)}catch{}}}if(qe){const U=ke==="dissolve"&&ee.dist_urls?ee.dist_urls[ee.edge||"right"]:null;U?_codex2SetGeodesicShape(R,U,ee.depth,qe):_codex2SetMatteShape(R,ee.matte_url,qe)}}Te?_cx2BakeWatch(n,e):ee&&_cx2HeroCycleStart(k,n,e,a,ke),k.querySelectorAll(".cx2-hero-edit-btn, .cx2-add-image-cta").forEach(R=>R.onclick=Y=>{Y.stopPropagation(),openCx2ImageModal(e)}),!t.host&&typeof loadThreads=="function"&&k._cx2ThreadsCid!==n&&(typeof _hooksAllThreads>"u"||!_hooksAllThreads||!_hooksAllThreads.length)&&(k._cx2ThreadsCid=n,Promise.resolve().then(()=>loadThreads()).then(()=>{(location.hash||"")===`#codex/${n}/${e}`&&_hooksAllThreads&&_hooksAllThreads.length&&renderCodexEntry(e)}).catch(()=>{}))}window.openCodex2=renderCodexEntry;let _myCharSelId=null;function _myCharOwnedPcs(){const e=_splitPcsByLifecycle(),t=n=>(n||[]).filter(a=>a&&(a.owned_by_me||_codex2OwnsEntity(a)));return{active:t(e.active),fallen:t(e.fallen),departed:t(e.departed)}}function _myCharHasOwned(){const e=_myCharOwnedPcs();return e.active.length+e.fallen.length+e.departed.length>0}function _myCharEditing(){const e=document.getElementById("tab-mychar");if(!e||e.classList.contains("hidden"))return!1;const t=document.activeElement;return t&&e.contains(t)&&/^(TEXTAREA|INPUT|SELECT)$/.test(t.tagName)?!0:!!e.querySelector(".cx2-edit-box, .gm-notes-ta")}function _afterPcClaimed(e){const t=(allEntities||[]).find(n=>n.id===e);t&&(t.owned_by_me=!0,t.assignable=!1);try{_applyMyCharTabVisibility()}catch{}return _pcClaimTrack("claimed"),t}function _applyMyCharTabVisibility(){const e=document.querySelector('.tab-btn[data-tab="mychar"]');if(!e)return;const t=_myCharHasOwned();e.classList.toggle("hidden",!t);const n=document.getElementById("tab-mychar"),a=!!(n&&!n.classList.contains("hidden"));!t&&e.classList.contains("active")?document.querySelector('.tab-btn[data-tab="compendium"]')?.click():t&&a&&!_myCharEditing()&&renderMyCharacterTab()}function _myCharCardHtml(e,t){const n=typeof ENTITY_CFG<"u"&&ENTITY_CFG[e.type]||{color:"var(--accent-blue)"},a=e.portrait_offset_x!=null?e.portrait_offset_x:50,s=e.portrait_offset_y!=null?e.portrait_offset_y:0,i=e.image_crop_url||e.image_url,o=i?typeof thumbUrl=="function"?thumbUrl(i):i:null,r=o?`<img src="${escHtml(o)}" alt="" loading="lazy">`:entityGlyphRef(e.type,"mychar-card-glyph"),c=e.is_dead?" is-dead":e.is_inactive?" is-departed":"",l=e.is_dead?'<span class="mychar-card-status">\u2620 Fallen</span>':e.is_inactive?'<span class="mychar-card-status">Departed</span>':"",d=e.id===t?" is-selected":"";return`<button class="mychar-card tf-card tf-card--sm${c}${d}" type="button" data-mychar-pc="${e.id}" style="--entity-color:${n.color}"${d?' aria-current="true"':""} title="${escHtml(e.name||"Character")}"><span class="mychar-card-portrait" style="--portrait-x:${a}%;--portrait-y:${s}%">${r}</span><span class="mychar-card-info"><span class="mychar-card-name">${escHtml(e.name||"Unnamed")}</span>${l}</span></button>`}async function renderMyCharacterTab(e){const t=document.getElementById("tab-mychar");if(!t)return;const n=_myCharOwnedPcs(),a=[...n.active,...n.fallen,...n.departed];if(!a.length){t.innerHTML='<div class="mychar-stage"><div class="mychar-empty placeholder--padded"><p class="mychar-empty-title">Your seat at the table is still yours.</p><p class="form-helper-text">When your GM adds or assigns your character, claim it in the Codex and it lands here.</p></div></div>';return}const s=a.find(u=>u.id===_myCharSelId)||n.active[0]||a[0];_myCharSelId=s.id;const i=a.length>1,o=(u,p,m)=>p.length?`<span class="form-section-label${m||""}">${u}</span><div class="mychar-card-row">${p.map(f=>_myCharCardHtml(f,s.id)).join("")}</div>`:"",r=i?`<div class="mychar-switch">
552      ${o("Your Story",n.active,"")}
553      ${o("\u2620 Fallen",n.fallen,"")}
554      ${o("Departed",n.departed,"")}
555    </div>`:"";let c=t.querySelector('.mychar-stage[data-built="1"]');c||(t.innerHTML=`<div class="mychar-stage" data-built="1">
556        <div class="mychar-topbar">
557          ${_introBannerHtml("mychar")}
558          <div class="mychar-switch-mount"></div>
559        </div>
560        <div class="mychar-host cx2-mychar-host"><div class="cx2-right"></div></div>
561      </div>`,c=t.querySelector('.mychar-stage[data-built="1"]'));const l=c.querySelector(".mychar-switch-mount");l&&(l.innerHTML=r),c.querySelectorAll("[data-mychar-pc]").forEach(u=>{u.onclick=()=>{const p=parseInt(u.dataset.mycharPc||u.dataset.mycharpc||u.getAttribute("data-mychar-pc"));p&&p!==_myCharSelId&&(_myCharSelId=p,renderMyCharacterTab())}});const d=c.querySelector(".mychar-host");try{await renderCodexEntry(s.id,{host:d,toTop:!(e&&e.keepScroll)})}catch{}}window.renderMyCharacterTab=renderMyCharacterTab;let _cx2IM=null;const _TF_ENTITY_ART_TYPES=["npc","deity","location","item","faction","companion"];function _tfCanGenArt(e){return _TF_ENTITY_ART_TYPES.includes(e)?!0:e==="pc"&&!!(currentUser&&currentUser.is_admin)}let _cx2ImOnClose=null;function _cx2EnsureImgModal(){let e=document.getElementById("cx2-imgmod-overlay");return e||(e=document.createElement("div"),e.id="cx2-imgmod-overlay",e.className="modal-overlay hidden",e.setAttribute("role","dialog"),e.setAttribute("aria-modal","true"),e.innerHTML=`<div class="modal-box modal-box--xxl cool-paper-surface cx2-imgmod-box">
562      <div class="cx2-imgmod-hdr">
563        <span class="form-section-label">Edit image</span>
564        <h2 class="cx2-imgmod-title" id="cx2-imgmod-title"></h2>
565        <button type="button" class="cx2-imgmod-close" id="cx2-imgmod-close" aria-label="Close">&times;</button>
566      </div>
567      <div class="cx2-imgmod-layout">
568        <div class="cx2-imgmod-preview-col">
569          <div class="cx2-imgmod-canvas-wrap" id="cx2-imgmod-canvas-wrap"></div>
570          <div class="cx2-imgmod-status-badge" id="cx2-imgmod-status"></div>
571        </div>
572        <div class="cx2-imgmod-controls-col">
573          <div class="cx2-imgmod-section">
574            <span class="form-section-label">Images</span>
575            <div class="cx2-imgmod-gallery" id="cx2-imgmod-gallery"></div>
576            <div class="cx2-imgmod-upload-status hidden" id="cx2-imgmod-upstatus"></div>
577            <div class="cx2-imgmod-gen hidden" id="cx2-imgmod-gen"></div>
578            <div class="cx2-imgmod-gen hidden" id="cx2-imgmod-token"></div>
579            <input type="file" id="cx2-imgmod-file" accept="image/*" multiple class="hidden">
580          </div>
581          <div class="cx2-imgmod-section">
582            <span class="form-section-label">Treatment</span>
583            <div id="cx2-imgmod-treatment"></div>
584          </div>
585          <div class="cx2-imgmod-section">
586            <span class="form-section-label">Framing</span>
587            <div id="cx2-imgmod-focal"></div>
588          </div>
589          <div class="cx2-imgmod-section" id="cx2-imgmod-label-section">
590            <span class="form-section-label">Label</span>
591            <input type="text" id="cx2-imgmod-label" maxlength="200"
592                   placeholder="e.g. The night the bridge fell">
593            <p class="form-helper-text">Optional, for this image only. Shown as the caption wherever this image appears in the Chronicle.</p>
594          </div>
595        </div>
596      </div>
597      <div class="modal-foot">
598        <button type="button" class="btn-ghost hidden" id="cx2-imgmod-reset">Reset to Codex</button>
599        <button type="button" class="btn-ghost" id="cx2-imgmod-cancel">Cancel</button>
600        <button type="button" class="btn-new" id="cx2-imgmod-save">Save changes</button>
601      </div>
602    </div>`,document.body.appendChild(e),e.querySelector("#cx2-imgmod-close").onclick=_cx2ImgModClose,e.querySelector("#cx2-imgmod-cancel").onclick=_cx2ImgModClose,e.querySelector("#cx2-imgmod-reset").onclick=async()=>{const t=_cx2IM;if(!(!t||!t.pctx||!await confirmDialog({title:"Reset to Codex?",message:"This chapter will go back to showing this entry exactly as the Codex does. The Codex itself is never changed.",confirmLabel:"Reset",cancelLabel:"Keep the chapter look"}))){try{await api("DELETE",`/api/campaigns/${t.cid}/sessions/${t.pctx.sessionId}/inline/${t.entityId}/overrides`)}catch(a){showToast(a&&a.message||"Could not reset.",4e3);return}_cx2ImgModClose(!0),_chr2RerenderIfOpen(),showToast("Back to the Codex look.")}},e.addEventListener("mousedown",t=>{e._md=t.target===e}),e.addEventListener("click",t=>{t.target===e&&e._md&&_cx2ImgModClose()}),e.querySelector("#cx2-imgmod-save").onclick=_cx2ImgModSave,e.querySelector("#cx2-imgmod-file").onchange=_cx2ImgModUpload,document.addEventListener("keydown",t=>{_tfModalEsc(t,e)&&(t.preventDefault(),_cx2ImgModClose())}),e)}async function openCx2ImageModal(e,t,n){const a=currentCampaign&&currentCampaign.id,s=(allEntities||[]).find(_=>_.id===e);if(!a||!s)return;_cx2ImOnClose=typeof t=="function"?t:null;const i=n&&n.sessionId?{sessionId:+n.sessionId}:null;let o,r;i?(o=(await _chr2GetPlacementSpecs(a,i.sessionId))[e]||null,r=!!(_chr2PlacementSpecs&&_chr2PlacementSpecs.premium)):(o=await _codex2GetSpec(a,e),r=!!_codex2Premium);const c=o||(s.image_url?{mode:"fullbleed",image_url:s.image_url}:null);let l=null;if(!i)try{const _=await api("GET",`/api/campaigns/${a}/entities/${e}/gallery/render`);l={},(_&&_.items||[]).forEach(w=>{w&&w.source_url&&(l[w.source_url]=w)})}catch{l=null}const d={type:c&&(c.baked_treatment||c.mode)||"fullbleed",edge:c&&c.edge||"top",depth:c&&c.depth!=null?+c.depth:.13,feather:c&&c.feather!=null?+c.feather:.015,deckle:c&&c.deckle!=null?+c.deckle:.009,scale:c&&c.scale!=null?+c.scale:8};d.type==="spot"&&(d.type="cutout");let u=_entityImageGallery(s);i&&c&&c.source_image_url&&u.includes(c.source_image_url)&&(u=[c.source_image_url].concat(u.filter(_=>_!==c.source_image_url))),_cx2IM={entityId:e,cid:a,ent:s,pctx:i,gspecs:l,editUrl:u[0],spec:c,isPc:s.type==="pc",premium:r,hasMatte:!!(c&&(c.has_matte||c.matte_url)),hasDist:!!(c&&(c.has_dist||c.dist_urls)),matteUrl:c&&(c.asset_matte_url||c.matte_url)||null,distUrls:c&&(c.asset_dist_urls||c.dist_urls)||null,urls:u.slice(),origUrls:u.slice(),pending:d,origPending:{...d},hero:null,uploading:!1,mirror:!!(c&&c.mirror),origMirror:!!(c&&c.mirror),focal:(()=>{const _=_cx2FocalForPrimary(s,u[0]);return c&&c.mirror&&_&&_.x!=null&&(_.x=Math.round(100-_.x)),_})(),origFocal:null,crop:c&&c.crop&&c.crop.ar?{ar:c.crop.ar,zoom:Math.max(1,Math.min(4,+c.crop.zoom||1)),cx:c.crop.rect?(+c.crop.rect.x+ +c.crop.rect.w/2)*100:null,cy:c.crop.rect?(+c.crop.rect.y+ +c.crop.rect.h/2)*100:null}:{ar:null,zoom:1,cx:null,cy:null},origCrop:null,cropUrl:c&&c.crop_url||null,natW:0,natH:0,label:l&&l[u[0]]&&l[u[0]].label||c&&c.label||"",origLabel:null},_cx2IM.origFocal={..._cx2IM.focal},_cx2IM.origCrop={..._cx2IM.crop},_cx2IM.origLabel=_cx2IM.label;const p=_cx2EnsureImgModal(),m=p.querySelector(".cx2-imgmod-hdr .form-section-label");m&&(m.textContent=i?"Edit image \xB7 this chapter only":"Edit image"),p.querySelector("#cx2-imgmod-title").textContent=i?`${s.name||"Entity"}: this chapter\u2019s look`:s.name||"Entity";const f=p.querySelector("#cx2-imgmod-reset");f&&f.classList.toggle("hidden",!i);const g=p.querySelector("#cx2-imgmod-save");
602g&&(g.disabled=!1,g.textContent=i?"Save for this chapter":"Save changes");const h=p.querySelector("#cx2-imgmod-upstatus");h&&(h.textContent="",h.classList.add("hidden"));const v=p.querySelector("#cx2-imgmod-gen");v&&(v.innerHTML="",v.classList.add("hidden"));const b=p.querySelector("#cx2-imgmod-label-section");b&&b.classList.toggle("hidden",!!i);const y=p.querySelector("#cx2-imgmod-label");y&&(y.value=_cx2IM.label||"",y.oninput=()=>{_cx2IM&&(_cx2IM.label=y.value)}),_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview(),_cx2ImgModRenderFraming(),_cx2ImgModRenderToken(),p.classList.remove("hidden")}function _cx2FocalForPrimary(e,t){return e&&t&&e.focal_src===t&&e.focal_x!=null&&+e.focal_x>=0&&e.focal_y!=null&&+e.focal_y>=0?{x:+e.focal_x,y:+e.focal_y,known:!0}:{x:50,y:50,known:!1}}function _cx2ImgModEditUrl(){return!_cx2IM.pctx&&_cx2IM.editUrl&&_cx2IM.urls.includes(_cx2IM.editUrl)?_cx2IM.editUrl:_cx2IM.urls[0]}function _cx2ImgModSeedEditTarget(){const e=_cx2ImgModEditUrl(),t=!_cx2IM.pctx&&_cx2IM.gspecs&&e?_cx2IM.gspecs[e]:null;if(t){if(_cx2IM.mirror=!!t.mirror,_cx2IM.focal=t.focal?{x:Math.round(t.mirror?100-t.focal.x:t.focal.x),y:Math.round(t.focal.y),known:!0}:{x:50,y:50,known:!1},_cx2IM.crop=t.crop&&t.crop.ar?{ar:t.crop.ar,zoom:Math.max(1,Math.min(4,+t.crop.zoom||1)),cx:t.crop.rect?(+t.crop.rect.x+ +t.crop.rect.w/2)*100:null,cy:t.crop.rect?(+t.crop.rect.y+ +t.crop.rect.h/2)*100:null}:{ar:null,zoom:1,cx:null,cy:null},_cx2IM.cropUrl=t.crop_url||null,_cx2IM.hasMatte=!!t.has_matte,_cx2IM.hasDist=!!t.has_dist,_cx2IM.matteUrl=t.asset_matte_url||null,_cx2IM.distUrls=t.asset_dist_urls||null,_cx2IM.spec=Object.assign({},t,{shadow:t.tuned?!!t.tuned.shadow:!!t.shadow}),t.tuned&&t.tuned.treatment){const n=t.tuned;_cx2IM.pending={type:n.treatment==="spot"?"cutout":n.treatment,edge:n.edge||"top",depth:n.depth!=null?+n.depth:.13,feather:n.feather!=null?+n.feather:.015,deckle:n.deckle!=null?+n.deckle:.009,scale:n.scale!=null?+n.scale:8}}}else _cx2IM.focal=_cx2FocalForPrimary(_cx2IM.ent,e),_cx2IM.crop={ar:null,zoom:1,cx:null,cy:null},_cx2IM.cropUrl=null,_cx2IM.mirror=!1;if(_cx2IM.origFocal={..._cx2IM.focal},_cx2IM.origCrop={..._cx2IM.crop},_cx2IM.origMirror=_cx2IM.mirror,_cx2IM.origPending={..._cx2IM.pending},!_cx2IM.pctx){_cx2IM.label=t&&t.label||"",_cx2IM.origLabel=_cx2IM.label;const n=document.getElementById("cx2-imgmod-label");n&&(n.value=_cx2IM.label)}_cx2IM.natW=0,_cx2IM.natH=0,_cx2ImgModRenderFraming()}function _cx2ImgModPrimaryFocalReset(){_cx2IM.editUrl=_cx2IM.urls[0],_cx2ImgModSeedEditTarget(),_cx2ImgModRenderGallery()}async function _cx2ImgModSelectForEdit(e){if(!_cx2IM||_cx2IM.pctx||!e||e===_cx2ImgModEditUrl())return;const t=_cx2ImgModDirty();(t.treat||t.crop||t.mirror||t.focal||t.label)&&(!await confirmDialog({title:"Switch image?",message:"You have unsaved changes to the current image. Switch anyway and discard them?",confirmLabel:"Switch",cancelLabel:"Keep editing"})||!_cx2IM)||(_cx2IM.editUrl=e,_cx2ImgModSeedEditTarget(),_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview())}const _CX2_FRAME_ARS={"3:4":3/4,"1:1":1,"16:9":16/9},_CX2_FRAME_ZMIN=1,_CX2_FRAME_ZMAX=4,_CX2_CROP_TARGET_PX=1600,_CX2_CROP_UPSCALE_MIN=1.15;function _cx2FrameGeom(){const e=_cx2IM&&_cx2IM.crop;if(!e||!e.ar)return null;const t=_CX2_FRAME_ARS[e.ar],n=_cx2IM.natW,a=_cx2IM.natH;if(!t||!n||!a)return null;const s=Math.max(_CX2_FRAME_ZMIN,Math.min(_CX2_FRAME_ZMAX,+e.zoom||1)),i=Math.min(n,a*t),o=i/s/n,r=i/t/s/a;let c=(e.cx!=null?e.cx:_cx2IM.focal?_cx2IM.focal.x:50)/100,l=(e.cy!=null?e.cy:_cx2IM.focal?_cx2IM.focal.y:50)/100;return c=Math.max(o/2,Math.min(1-o/2,c)),l=Math.max(r/2,Math.min(1-r/2,l)),{x:c-o/2,y:l-r/2,w:o,h:r,cx:c,cy:l}}function _cx2FocalClampToFrame(){const e=_cx2FrameGeom();if(!e||!_cx2IM||!_cx2IM.focal)return;const t=Math.max((e.x+.01)*100,Math.min((e.x+e.w-.01)*100,_cx2IM.focal.x)),n=Math.max((e.y+.01)*100,Math.min((e.y+e.h-.01)*100,_cx2IM.focal.y));_cx2IM.focal={x:Math.round(t),y:Math.round(n),known:_cx2IM.focal.known||!1}}function _cx2FrameSync(){const e=document.getElementById("cx2-imgmod-focal");if(!e||!_cx2IM)return;const t=e.querySelector("#cx2-frame-box"),n=e.querySelector("#cx2-focal-marker"),a=e.querySelector("#cx2-frame-zoomrow"),s=e.querySelector("#cx2-frame-zval"),i=e.querySelector("#cx2-frame-warn"),o=e.querySelector("#cx2-frame-hint"),r=_cx2IM.focal||{x:50,y:50};n&&(n.style.left=r.x+"%",n.style.top=r.y+"%");
602const c=_cx2FrameGeom(),l=!!(_cx2IM.crop&&_cx2IM.crop.ar);if(a&&a.classList.toggle("hidden",!l),o&&(o.textContent=l?"Drag to position the frame. It\u2019s what appears on the page. Drag the ring onto the face (it stays inside the frame). Scroll or +/\u2212 to zoom.":"Drag the ring onto your character\u2019s face. The Party card and quote cards crop to this point."),!c){t&&t.classList.add("hidden"),i&&i.classList.add("hidden");return}if(t&&(t.classList.remove("hidden"),t.style.left=c.x*100+"%",t.style.top=c.y*100+"%",t.style.width=c.w*100+"%",t.style.height=c.h*100+"%"),s&&(s.textContent=Math.round((+_cx2IM.crop.zoom||1)*100)+"%"),i){const d=Math.max(c.w*_cx2IM.natW,c.h*_cx2IM.natH);i.classList.toggle("hidden",!(d>0&&_CX2_CROP_TARGET_PX/d>_CX2_CROP_UPSCALE_MIN))}}function _cx2FrameSetZoom(e){if(!_cx2IM||!_cx2IM.crop||!_cx2IM.crop.ar)return;_cx2IM.crop.zoom=Math.max(_CX2_FRAME_ZMIN,Math.min(_CX2_FRAME_ZMAX,e));const t=_cx2FrameGeom();t&&(_cx2IM.crop.cx=t.cx*100,_cx2IM.crop.cy=t.cy*100),_cx2FocalClampToFrame(),_cx2FrameSync()}function _cx2ImgModRenderFraming(){const e=document.getElementById("cx2-imgmod-focal");if(!e||!_cx2IM)return;const t=_cx2ImgModEditUrl();if(!t){e.innerHTML='<p class="placeholder--padded">No image to frame. Add one above.</p>';return}const n=_cx2IM.crop||{ar:null,zoom:1},a=_cx2IM.focal||{x:50,y:50},s=[["","Original"],["3:4","3:4"],["1:1","1:1"],["16:9","16:9"]],i=_cx2IM.mirror?_tfHflipUrl(t):t,o='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18"/><path d="M8 8L4 12l4 4"/><path d="M16 8l4 4-4 4"/></svg>';e.innerHTML=`<div class="cx2-frame-chips">${s.map(([h,v])=>`<button type="button" class="cx2-imgmod-typebtn cx2-frame-chip${(n.ar||"")===h?" on":""}" data-ar="${h}">${v}</button>`).join("")}
603       <button type="button" class="cx2-imgmod-typebtn cx2-frame-mirror${_cx2IM.mirror?" on":""}" id="cx2-frame-mirror" title="Flip the image horizontally, e.g. so the character faces the prose">${o} Mirror</button></div>
604     <div class="cx2-focal-wrap" id="cx2-focal-wrap" tabindex="0" role="application"
605          aria-label="Framing: drag to position; scroll, plus/minus, or arrow keys to adjust"
606          title="${n.ar?"Drag to position, scroll to zoom":"Click or drag to set the focus point"}">
607       <img class="cx2-focal-img" src="${escHtml(i)}" alt="" draggable="false">
608       <div class="cx2-frame-box hidden" id="cx2-frame-box"></div>
609       <div class="cx2-focal-marker" id="cx2-focal-marker" style="left:${a.x}%;top:${a.y}%"></div>
610     </div>
611     <div class="cx2-frame-zoomrow${n.ar?"":" hidden"}" id="cx2-frame-zoomrow">
612       <button type="button" class="cx2-imgmod-typebtn cx2-frame-zbtn" data-z="-1" aria-label="Zoom out">&minus;</button>
613       <span class="cx2-frame-zval" id="cx2-frame-zval"></span>
614       <button type="button" class="cx2-imgmod-typebtn cx2-frame-zbtn" data-z="1" aria-label="Zoom in">+</button>
615     </div>
616     <p class="form-helper-text form-helper-text--warn hidden" id="cx2-frame-warn">This zoom will upscale the image, quality may soften.</p>
617     <p class="cx2-focal-hint form-helper-text" id="cx2-frame-hint"></p>`;const r=e.querySelector("#cx2-focal-wrap"),c=e.querySelector(".cx2-focal-img"),l=()=>{c.naturalWidth&&_cx2IM&&(_cx2IM.natW=c.naturalWidth,_cx2IM.natH=c.naturalHeight,_cx2FrameSync())};c.complete?l():c.addEventListener("load",l);const d=h=>{const v=r.getBoundingClientRect();return!v.width||!v.height?null:{x:Math.max(0,Math.min(100,(h.clientX-v.left)/v.width*100)),y:Math.max(0,Math.min(100,(h.clientY-v.top)/v.height*100)),dispW:v.width,dispH:v.height}},u=h=>{const v=d(h);v&&(_cx2IM.focal={x:Math.round(v.x),y:Math.round(v.y),known:_cx2IM.focal&&_cx2IM.focal.known||!1},_cx2FocalClampToFrame(),_cx2FrameSync())},p=h=>{const v=d(h);if(!v||!_cx2IM.crop||!_cx2IM.crop.ar)return;_cx2IM.crop.cx=v.x,_cx2IM.crop.cy=v.y;const b=_cx2FrameGeom();b&&(_cx2IM.crop.cx=b.cx*100,_cx2IM.crop.cy=b.cy*100),_cx2FocalClampToFrame(),_cx2FrameSync()};let m=null;r.addEventListener("pointerdown",h=>{h.preventDefault();const v=d(h);if(!v)return;const b=!!(_cx2IM.crop&&_cx2IM.crop.ar);let y=!1;if(b&&_cx2IM.focal){const _=(v.x-_cx2IM.focal.x)/100*v.dispW,w=(v.y-_cx2IM.focal.y)/100*v.dispH;y=Math.hypot(_,w)<=18}m=!b||y?"focal":"frame";try{r.setPointerCapture(h.pointerId)}catch{}(m==="focal"?u:p)(h)}),r.addEventListener("pointermove",h=>{m==="focal"?u(h):m==="frame"&&p(h)});const f=h=>{m=null;try{r.releasePointerCapture(h.pointerId)}catch{}};r.addEventListener("pointerup",f),r.addEventListener("pointercancel",f),r.addEventListener("wheel",h=>{!_cx2IM||!_cx2IM.crop||!_cx2IM.crop.ar||(h.preventDefault(),_cx2FrameSetZoom((+_c
617x2IM.crop.zoom||1)*(h.deltaY<0?1.08:1/1.08)))},{passive:!1}),r.addEventListener("keydown",h=>{if(!_cx2IM)return;const v={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]}[h.key];if(v){if(h.preventDefault(),!!(_cx2IM.crop&&_cx2IM.crop.ar)&&!h.shiftKey){const y=_cx2FrameGeom();_cx2IM.crop.cx=(y?y.cx*100:50)+v[0],_cx2IM.crop.cy=(y?y.cy*100:50)+v[1];const _=_cx2FrameGeom();_&&(_cx2IM.crop.cx=_.cx*100,_cx2IM.crop.cy=_.cy*100),_cx2FocalClampToFrame()}else{const y=_cx2IM.focal||{x:50,y:50};_cx2IM.focal={x:Math.max(0,Math.min(100,y.x+v[0])),y:Math.max(0,Math.min(100,y.y+v[1])),known:y.known||!1},_cx2FocalClampToFrame()}_cx2FrameSync()}else h.key==="+"||h.key==="="?(h.preventDefault(),_cx2FrameSetZoom((+(_cx2IM.crop||{}).zoom||1)*1.15)):(h.key==="-"||h.key==="_")&&(h.preventDefault(),_cx2FrameSetZoom((+(_cx2IM.crop||{}).zoom||1)/1.15))}),e.querySelectorAll(".cx2-frame-chip").forEach(h=>h.onclick=()=>{const v=h.dataset.ar||null;if(!_cx2IM)return;const b=_cx2IM.crop||{ar:null,zoom:1,cx:null,cy:null},y=_cx2IM.focal||{x:50,y:50};_cx2IM.crop=v?{ar:v,zoom:b.ar&&+b.zoom||1,cx:b.ar?b.cx:y.x,cy:b.ar?b.cy:y.y}:{ar:null,zoom:1,cx:null,cy:null},_cx2ImgModRenderFraming()});const g=e.querySelector("#cx2-frame-mirror");g&&(g.onclick=()=>{_cx2IM&&(_cx2IM.mirror=!_cx2IM.mirror,_cx2IM.focal&&_cx2IM.focal.x!=null&&(_cx2IM.focal={..._cx2IM.focal,x:Math.round(100-_cx2IM.focal.x)}),_cx2IM.crop&&_cx2IM.crop.ar&&_cx2IM.crop.cx!=null&&(_cx2IM.crop.cx=100-_cx2IM.crop.cx),_cx2ImgModRenderFraming(),_cx2ImgModRenderPreview())}),e.querySelectorAll(".cx2-frame-zbtn").forEach(h=>h.onclick=()=>{const v=+(_cx2IM&&_cx2IM.crop||{}).zoom||1;_cx2FrameSetZoom(h.dataset.z==="1"?v*1.15:v/1.15)}),_cx2FrameSync()}function _cx2CropPayload(){const e=_cx2IM&&_cx2IM.crop;if(!e||!e.ar)return null;const t=_cx2FrameGeom();if(t)return{ar:e.ar,zoom:Math.max(_CX2_FRAME_ZMIN,Math.min(_CX2_FRAME_ZMAX,+e.zoom||1)),rect:{x:+t.x.toFixed(4),y:+t.y.toFixed(4),w:+t.w.toFixed(4),h:+t.h.toFixed(4)}};const n=_cx2IM.spec&&_cx2IM.spec.crop;return n&&n.rect&&n.ar===e.ar?{ar:e.ar,zoom:+e.zoom||1,rect:n.rect}:null}function _cx2ImgModPrimaryChanged(){const e=_cx2IM.spec;return!e||_cx2ImgModEditUrl()!==(e.source_image_url||_cx2IM.origUrls[0])}function _cx2ImgModTypeAllowed(e){return e==="fullbleed"?!0:_cx2IM.isPc||_cx2IM.premium}function _cx2ImgModTypeBaked(e){return e==="fullbleed"?!0:_cx2ImgModPrimaryChanged()||!!_cx2IM.mirror!=!!_cx2IM.origMirror?!1:e==="cutout"?!!_cx2IM.hasMatte:e==="dissolve"?!!(_cx2IM.hasMatte&&_cx2IM.hasDist):!1}function _cx2ImgModPreviewSpec(){const e=_cx2ImgModEditUrl(),t=_cx2IM.pending.type,n=!!_cx2IM.mirror,a=o=>{if(!o)return o;const r=o.indexOf("/a_hflip/")!==-1;return n&&!r?_tfHflipUrl(o):!n&&r?o.replace("/a_hflip/","/"):o},s=!_cx2ImgModPrimaryChanged()&&_cx2IM.cropUrl&&n===!!_cx2IM.origMirror?_cx2IM.cropUrl:n?_tfHflipUrl(e):e,i=a(!_cx2ImgModPrimaryChanged()&&_cx2IM.spec&&_cx2IM.spec.image_url?_cx2IM.spec.image_url:e);return!e||t==="fullbleed"||!_cx2ImgModTypeBaked(t)?{mode:"fullbleed",image_url:t==="fullbleed"?i:s}:t==="cutout"?{mode:"cutout",image_url:s,matte_url:_cx2IM.matteUrl,shadow:(_cx2IM.pending.shadow,!1)}:{mode:"dissolve",image_url:s,matte_url:_cx2IM.matteUrl,dist_urls:_cx2IM.distUrls,edge:_cx2IM.pending.edge,depth:_cx2IM.pending.depth,feather:_cx2IM.pending.feather,deckle:_cx2IM.pending.deckle,scale:_cx2IM.pending.scale}}function _cx2ImgModLayout(e){const t=_cx2IM.entityId%2===0?"left":"right";if(e==="dissolve"){const n=_cx2IM.pending.edge||"right";return n==="left"?{cls:"cx2-dis-left",wrapSide:"left"}:n==="right"?{cls:"cx2-dis-right",wrapSide:"right"}:{cls:`cx2-dis-tb cx2-dis-tb--${t}`,wrapSide:null}}return e==="cutout"?{cls:`cx2-corner cx2-corner--${t}`,wrapSide:t==="left"?"right":"left"}:{cls:`cx2-corner cx2-corner--${t}`,wrapSide:null}}const _CX2_WRAP_LOREM="The chronicle continues here, the prose flowing around the portrait exactly as it will on the page. Each treatment and edge changes how the text hugs the figure: a left dissolve lets text wrap its right contour, a cutout follows the silhouette, a full-bleed sits in the corner. Adjust the controls and watch the layout settle.";function _cx2ImgModRenderPreview(){const e=document.getElementById("cx2-imgmod-canvas-wrap"),t=document.getElementById("cx2-imgmod-status");if(!e)return;if(_cx2IM.hero){try{_cx2IM.hero.dispose()}catch{}_cx2IM.hero=null}e.innerHTML="";const n=_cx2ImgModEditUrl();if(!n){e.innerHTML='<div class="placeholder--padded">No image. Add one below</div>',t&&(t.textContent="");return}const a=_cx2ImgModPreviewSpec(),s=_cx2ImgModLayout(a.mode),i=_cx2IM.pending.type!=="fullbleed"&&!_cx2ImgModTypeAllowed(_cx2IM.pending.type)&&a.mode!=="fullbleed",o=a.mode==="fullbleed"?`<figure class="cx2-portrait ${s.cls}"><img alt="" src="${escHtml(a.image_url||n)}"></figure>`:`<figure class="cx2-portrait ${s.cls}"><canvas></canvas></figure>`,r=i?`<div class="cx2-imgmod-ghost-cta"><div class="cx2-imgmod-ghost-msg">Your PCs already look like this. Bring the rest of your cast to life. <b>${escHtml(_cx2IM.pending.type)}</b> treatments are on any plan from $10/mo.</div><button type="button" class="btn-new cx2-imgmod-ghost-btn" id="cx2-imgmod-ghost-upgrade">See plans</button></div>`:"";e.innerHTML=`<div class="cx2-imgmod-stage${i?" cx2-imgmod-stage--ghost":""}">${o}<p class="cx2-imgmod-wrap-prose">${escHtml(_CX2_WRAP_LOREM)}</p></div>${r}`;const c=e.querySelector(".cx2-portrait");if(a.mode!=="fullbleed"&&window.CodexHero){const d=c.querySelector("canvas"),u=CodexHero.mount(d);if(u){_cx2IM.hero=u;try{u.show(a)}catch{}}if(s.wrapSide){const p=a.mode==="dissolve"&&a.dist_urls?a.dist_urls[a.edge||"right"]:null;p?_codex2SetGeodesicShape(c,p,a.depth,s.wrapSide):a.matte_url&&_codex2SetMatteShape(c,a.matte_url,s.wrapSide)}}if(t){const d=_cx2ImgModTypeAllowed(_cx2IM.pending.type),u=_cx2ImgModTypeBaked(_cx2IM.pending.type);t.innerHTML=d?u||_cx2IM.pending.type==="fullbleed"?`${a.mode}${a.mode==="dissolve"?" \xB7 "+_cx2IM.pending.edge:""}`:'<span class="cx2-imgmod-untreated">not applied yet \xB7 Save to apply</span>':'<span class="cx2-imgmod-untreated">\u2726 premium preview</span>'}const l=e.querySelector("#cx2-imgmod-ghost-upgrade");l&&(l.onclick=()=>{typeof _tfShowPricing=="function"?_tfShowPricing():location.hash="#pricing"})}function _cx2ImgModRenderGallery(){const e=document.getElementById("cx2-imgmod-gallery");if(!e)return;const t=_cx2IM.urls,n=_cx2IM.pctx,a=n?"Showing in this chapter":"Primary",s=n?"Show in this chapter":"Set as primary",i=_cx2ImgModEditUrl();e.innerHTML=t.map((d,u)=>{const p=u===0,m=!n&&d===i,f=p?`<span class="cx2-imgmod-primary-badge" title="${a}">&#9733;</span>`:`<button type="button" class="cx2-imgmod-tile-action cx2-imgmod-tile-star" data-star="${u}" title="${s}">&#9733;</button>`,g=!n&&t.length>1?`<button type="button" class="cx2-imgmod-tile-action cx2-imgmod-tile-remove" data-rm="${u}" title="Remove">&times;</button>`:"";return`<div class="cx2-imgmod-tile${p?" cx2-imgmod-tile--primary":""}${m?" cx2-imgmod-tile--editing":""}"${n?"":` data-sel="${u}" title="Edit this image&rsquo;s settings"`}><img src="${escHtml(thumbUrl(d)||d)}" alt="">${f}${g}</div>`}).join("")+(n?"":'<button type="button" class="cx2-imgmod-add-tile" id="cx2-imgmod-add"><span class="cx2-imgmod-add-plus">+</span><span>Add</span></button>'+(_codex2IsGm()||currentUser&&currentUser.is_admin?'<button type="button" class="cx2-imgmod-add-tile cx2-imgmod-lib-tile" id="cx2-imgmod-lib"><span class="cx2-imgmod-add-plus">&#128247;</span><span>Library</span></button>':"")+(_tfCanGenArt(_cx2IM.ent&&_cx2IM.ent.type)?'<button type="button" class="cx2-imgmod-add-tile cx2-imgmod-gen-tile" id="cx2-imgmod-gen-btn"><span class="cx2-imgmod-add-plus"><svg class="tf-glyph" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.5 9.6 6.4 14.5 8 9.6 9.6 8 14.5 6.4 9.6 1.5 8 6.4 6.4Z"/></svg></span><span>Generate</span></button>':"")+(_tfCanGenToken(_cx2IM.ent)?'<button type="button" class="cx2-imgmod-add-tile cx2-imgmod-gen-tile" id="cx2-imgmod-token-btn"><span class="cx2-imgmod-add-plus"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><circle cx="12" cy="12" r="4"></circle></svg></span><span>Token</span></button>':"")),e.querySelectorAll("[data-star]").forEach(d=>d.onclick=u=>{u.stopPropagation(),_cx2ImgModSetPrimary(+d.dataset.star)}),e.querySelectorAll("[data-rm]").forEach(d=>d.onclick=u=>{u.stopPropagation(),_cx2ImgModRemove(+d.dataset.rm)}),e.querySelectorAll("[data-sel]").forEach(d=>d.onclick=()=>{const u=_cx2IM.urls[+d.dataset.sel];u&&(_cx2IM.tokenViewUrl=null,_cx2ImgModSelectForEdit(u),_cx2ImgModRenderToken())});const o=e.querySelector("#cx2-imgmod-add");o&&(o.onclick=()=>document.getElementById("cx2-imgmod-file").click());const r=e.querySelector("#cx2-imgmod-lib");r&&(r.onclick=()=>_campLibOpen({autoAssign:!1,title:"Add from your library",subtitle:"Pick a loose campaign image to add to this entry. Save to keep it.",onPick:d=>{!d||!d.url||_cx2IM.urls.includes(d.url)||(_cx2IM.urls.push(d.url),_cx2ImgModRenderGallery(),_cx2ImgModRenderPreview())}}));const c=e.querySelector("#cx2-imgmod-token-btn");c&&(c.onclick=()=>
617{if(_cx2IM){if(_cx2IM.tokenOpen=!_cx2IM.tokenOpen,_cx2IM.tokenOpen){const d=document.getElementById("cx2-imgmod-gen");d&&(d.classList.add("hidden"),d.innerHTML="")}_cx2ImgModRenderToken()}});const l=e.querySelector("#cx2-imgmod-gen-btn");l&&(l.onclick=()=>{const d=document.getElementById("cx2-imgmod-gen");if(d){if(!d.classList.contains("hidden")){d.classList.add("hidden"),d.innerHTML="";return}_cx2IM&&_cx2IM.tokenOpen&&(_cx2IM.tokenOpen=!1,_cx2ImgModRenderToken()),d.classList.remove("hidden"),_tfRenderAiImagePanel(d,{suggestUrl:`/api/campaigns/${_cx2IM.cid}/entities/${_cx2IM.entityId}/suggest-art`,generateUrl:`/api/campaigns/${_cx2IM.cid}/entities/${_cx2IM.entityId}/generate-art`,sceneLabel:"Describe the subject",suggestLabel:"Suggest from this entry",scenePlaceholder:"Describe what to depict, or use Suggest.",styleSeed:_tfCampaignStyleSeed(),busyMsg:"Generating art: this can take ~15\u201330s\u2026",onDone:async u=>{if(!u||!u.id)return;allEntities=allEntities.map(f=>f.id===u.id?u:f),_tfHandleAutoInline(u);const p=_entityImageGallery(u);_cx2IM.ent=u;const m=p.filter(f=>_cx2IM.origUrls.indexOf(f)===-1);_cx2IM.urls=[..._cx2IM.urls,...m],_cx2IM.origUrls=[..._cx2IM.origUrls,...m],d.classList.add("hidden"),d.innerHTML="",_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview(),_cx2ImgModPrimaryFocalReset(),showToast("Art generated.")}})}})}function _cx2ImgModResetOriginal(){!_cx2IM||_cx2IM.pctx||(_cx2IM.pending.type="fullbleed",_cx2IM.crop={ar:null,zoom:1,cx:null,cy:null},_cx2IM.mirror&&(_cx2IM.focal&&_cx2IM.focal.x!=null&&(_cx2IM.focal={..._cx2IM.focal,x:Math.round(100-_cx2IM.focal.x)}),_cx2IM.mirror=!1),_cx2ImgModRenderTreatment(),_cx2ImgModRenderFraming(),_cx2ImgModRenderPreview())}function _cx2ImgModSetPrimary(e){e<=0||e>=_cx2IM.urls.length||(_cx2IM.urls=_galleryReorderPrimary(_cx2IM.urls,e),_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview(),_cx2ImgModPrimaryFocalReset())}function _cx2ImgModRemove(e){if(e<0||e>=_cx2IM.urls.length||_cx2IM.urls.length<=1)return;const t=_cx2IM.urls[e];_cx2IM.urls.splice(e,1),_cx2ImgModRenderGallery(),(e===0||t===_cx2IM.editUrl)&&(_cx2ImgModPrimaryFocalReset(),_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview())}function _cx2ImgModRenderTreatment(){const e=document.getElementById("cx2-imgmod-treatment");if(!e)return;const t=_cx2IM.pending,a=[["fullbleed","Full bleed"],["cutout","Cutout"],["dissolve","Dissolve"]].map(([f,g])=>{const h=_cx2ImgModTypeAllowed(f),v=h?"":' <span class="cx2-imgmod-prem">\u2726</span>';return`<button type="button" class="${"cx2-imgmod-typebtn"+(f===t.type?" on":"")+(h?"":" cx2-imgmod-typebtn--prem")}" data-type="${f}">${g}${v}</button>`}).join(""),i=!_cx2IM.isPc&&!_cx2IM.premium?'<div class="cx2-imgmod-upsell">Your PCs already have these treatments. Bring the rest of your cast to life. Cutout &amp; dissolve are on any plan from $10/mo. <button type="button" class="cx2-imgmod-upsell-link" id="cx2-imgmod-upgrade">See plans \u2192</button></div>':"",r=t.type==="dissolve"?`<div class="cx2-dtune-edge" id="cx2-imgmod-edge">${["top","bottom","left","right"].map(f=>`<button type="button" class="cx2-dtune-eb${f===t.edge?" on":""}" data-edge="${f}">${f}</button>`).join("")}</div>`+_DTUNE_SLIDERS.map(([f,g,h,v,b,y])=>`<label class="cx2-dtune-sl"><span>${g} <b id="cx2-imgmod-dtv-${f}">${(+t[f]).toFixed(y)}</b></span><input type="range" data-dk="${f}" data-dp="${y}" min="${h}" max="${v}" step="${b}" value="${t[f]}"></label>`).join(""):"",c=t.type!=="fullbleed"&&_cx2ImgModTypeAllowed(t.type)&&!_cx2ImgModTypeBaked(t.type)?`<div class="cx2-imgmod-no-treat-note">The <b>${escHtml(t.type)}</b> treatment isn\u2019t applied to this image yet. It previews full-bleed. Pick your settings and <b>Save</b> to apply it; it renders on its own and appears here when ready.</div>`:"",l='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>',d=t.type==="fullbleed"&&!(_cx2IM.crop&&_cx2IM.crop.ar)&&!_cx2IM.mirror,u=_cx2IM.pctx?"":`<div class="cx2-imgmod-reset-row"><button type="button" class="btn-ghost btn-xs cx2-imgmod-reset" id="cx2-imgmod-reset"${d?" disabled":""} title="Clear the treatment, crop, and mirror \u2014 back to the untouched image. Save to apply.">${l} Reset to original</button></div>`;e.innerHTML=`<div class="cx2-imgmod-typeseg">${a}</div>${u}${i}${r}${c}`;const p=e.querySelector("#cx2-imgmod-reset");p&&(p.onclick=_cx2ImgModResetOriginal),e.querySelectorAll(".cx2-imgmod-typebtn").forEach(f=>f.onclick=()=>{_cx2IM.pending.type=f.dataset.type,_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview()});const m=e.querySelector("#cx2-imgmod-upgrade");m&&(m.onclick=()=>{typeof _tfShowPricing=="function"?_tfShowPricing():location.hash="#pricing"}),e.querySelectorAll("#cx2-imgmod-edge .cx2-dtune-eb").forEach(f=>f.onclick=()=>{_cx2IM.pending.edge=f.dataset.edge,e.querySelectorAll("#cx2-imgmod-edge .cx2-dtune-eb").forEach(g=>g.classList.toggle("on",g===f)),_cx2ImgModRenderPreview()}),e.querySelectorAll("input[data-dk]").forEach(f=>f.oninput=()=>{const g=f.dataset.dk;_cx2IM.pending[g]=+f.value;const h=document.getElementById("cx2-imgmod-dtv-"+g);h&&(h.textContent=(+f.value).toFixed(+f.dataset.dp)),_cx2IM.hero&&_cx2IM.hero.setParams({[g]:+f.value})})}const _TF_TOKEN_TYPES=["pc","npc","companion","faction"],_TF_TOKEN_SIZES=["Tiny","Small","Medium","Large","Huge","Gargantuan"],_TF_TOKEN_PX={tiny:140,small:280,medium:280,large:560,huge:840,gargantuan:1120};function _tfCanGenToken(e){return!!(e&&_TF_TOKEN_TYPES.includes(e.type))}function _tfLedgerSize(e){const t=String((typeof _codex2EntityLedger=="function"&&_codex2EntityLedger(e)||{}).size||"").trim();return _TF_TOKEN_SIZES.find(n=>n.toLowerCase()===t.toLowerCase())||"Medium"}const _TF_TOKEN_PLANS=["Humanoid","Quadruped","Flying","Other"];function _tfTokenImgSettings(e,t){const n=typeof _codex2EntityLedger=="function"&&_codex2EntityLedger(e)||{},a=n._token_settings&&n._token_settings[t]||{},s=_TF_TOKEN_SIZES.find(o=>o.toLowerCase()===String(a.size||n.size||"").trim().toLowerCase())||"Medium",i=_TF_TOKEN_PLANS.find(o=>o.toLowerCase()===String(a.plan||n.body_plan||"").trim().toLowerCase())||"Humanoid";return{size:s,plan:i}}function _tfTokenPersistChoice(e,t){if(!_cx2IM)return;const n=_cx2IM.entityId,a=_cx2ImgModEditUrl()||_cx2IM.ent&&_cx2IM.ent.image_url,s=_codex2EntityLedger(_cx2IM.ent||{}),i={...s._token_settings||{}};i[a]={...i[a]||{},[e]:t};const o={_token_settings:i,[e==="plan"?"body_plan":"size"]:t};_cx2IM.ent={..._cx2IM.ent,ledger:{...s,...o}},allEntities=allEntities.map(r=>r.id===n?{...r,ledger:{...r.ledger||{},...o}}:r);try{_codex2SaveField(n,{ledger:o})}catch{}_cx2ImgModRenderToken()}const _TF_TOPDOWN_TOKEN_SCALE=1.35;function _tfTokenScale(e,t){const n=parseFloat((typeof _codex2EntityLedger=="function"&&_codex2EntityLedger(e)||{}).token_scale||"");if(isFinite(n)&&n>0)return Math.max(.5,Math.min(2,n));const a=String(t||(typeof _codex2EntityLedger=="function"&&_codex2EntityLedger(e)||{}).body_plan||"").toLowerCase();return a==="quadruped"||a==="flying"?_TF_TOPDOWN_TOKEN_SCALE:1}function _tfTokenMap(e){const t=e&&e.token_urls&&typeof e.token_urls=="object"?{...e.token_urls}:{};
617return e&&e.token_url&&e.image_url&&!t[e.image_url]&&(t[e.image_url]=e.token_url),t}function _tfLedgerTokList(e){if(!e||!_TF_TOKEN_TYPES.includes(e.type))return[];const t=_tfTokenMap(e),n=typeof _entityImageGallery=="function"?_entityImageGallery(e)||[]:[],a=[];return n.forEach(s=>{t[s]&&!a.includes(s)&&a.push(s)}),Object.keys(t).forEach(s=>{t[s]&&!a.includes(s)&&a.push(s)}),a.map(s=>{const i=_tfTokenImgSettings(e,s),o=_TF_TOKEN_PX[i.size.toLowerCase()]||280,r=_tfTokenScale(e,i.plan);return{raw:t[s],sized:t[s].replace("/upload/",`/upload/c_scale,w_${Math.round(o*1.5*r)}/`),size:i.size,px:o}})}let _tfImgQuota={};function _tfImgQuotaLine(e){const t=e&&_tfImgQuota[e];if(!t||!t.cap||t.cap>=9999)return"";const n=Math.max(0,t.cap-t.used);return`<p class="form-helper-text${n<=2?" form-helper-text--warn":""}">${t.used} of ${t.cap} images this month`+(n<=2?n===0?" \u2014 none left until it resets.":` \u2014 ${n} left.`:"")+"</p>"}function _tfDeadringerBatchLine(e,t){if(!(_isGm()||currentUser&&currentUser.is_admin))return"";
617const n=t&&_tfImgQuota[t],a=!!(n&&typeof n.cap=="number"&&n.cap<9999&&n.cap-n.used<=0),s=!!(e&&e.type==="faction");return!s&&!a?"":`<p class="form-helper-text cx2-token-dr">${s?"Building a squad? Deadringer bakes a whole set in one pass, on its own credits.":n&&n.cap===0?"Rather pay per token than subscribe? Deadringer bakes tokens on its own credits, 3 free to start.":"Need a whole party or warband before the pool resets? Deadringer bakes them in one batch, on its own credits."} <a class="btn-ghost btn-xs" href="${escHtml(_tfDeadringerHref(t))}" target="_blank" rel="noopener" title="Opens this campaign in Deadringer, signed in with your Threadfall account">Batch in Deadringer</a></p>`}function _cx2ImgModRenderToken(){const e=document.getElementById("cx2-imgmod-token");if(!e||!_cx2IM)return;const t=_cx2IM.ent,n=!_cx2IM.pctx&&_tfCanGenToken(t)&&!!_cx2IM.tokenOpen;if(e.classList.toggle("hidden",!n),!n){e.innerHTML="";return}const a='<span class="form-section-label">Token Art</span>';if(_cx2IM.gallery===void 0){_cx2IM.gallery=null;const k=_cx2IM.entityId;api("GET",`/api/campaigns/${_cx2IM.cid}/entities/${k}/tokens`).then(C=>{_cx2IM&&_cx2IM.entityId===k&&(_cx2IM.gallery=C&&C.tokens||[],_cx2ImgModRenderToken())}).catch(()=>{_cx2IM&&_cx2IM.entityId===k&&(_cx2IM.gallery=[])})}if(_cx2IM.tokenBusy){e.innerHTML=a+'<p class="form-helper-text">Rendering your token \u2014 this takes about a minute\u2026</p>';return}if(!t.image_url){e.innerHTML=a+'<p class="form-helper-text">Add a portrait first. The token is drawn top-down from this entry\u2019s images.</p>';return}const s=_cx2ImgModEditUrl()||t.image_url,i=_tfTokenMap(t);_cx2IM.tokenViewUrl&&!i[_cx2IM.tokenViewUrl]&&(_cx2IM.tokenViewUrl=null);const o=_cx2IM.tokenViewUrl||s,r=i[o]||null,c=!!r&&!(_cx2IM.urls||[]).includes(o),l=Object.keys(i),d=l.length>1||l.length===1&&!(_cx2IM.urls||[]).includes(l[0])?'<div class="cx2-token-strip">'+l.map(k=>{const C=!(_cx2IM.urls||[]).includes(k);return`<div class="cx2-token-strip-tile${k===o?" cx2-token-strip-tile--sel":""}${C?" cx2-token-strip-tile--orphan":""}" data-toksel="${escHtml(k)}" title="${C?"Source image no longer in this entry\u2019s gallery":"View this token"}"><img src="${escHtml(i[k])}" alt="Token"><button type="button" class="cx2-token-strip-rm" data-tokrm="${escHtml(k)}" title="Delete this token">&times;</button></div>`}).join("")+'</div><p class="form-helper-text">All of this entry\u2019s tokens \u2014 click one to view or download it; \xD7 deletes just that token.</p>':"",u=_cx2TokenGalleryHtml(t),p=_tfTokenImgSettings(t,o),m=p.size,f=p.plan,h=`<div class="cx2-token-sizerow"><span class="form-helper-text">Body</span>${'<select id="cx2-token-plan" class="cx2-token-size-sel" title="Body plan">'+_TF_TOKEN_PLANS.map(k=>`<option value="${k}"${k===f?" selected":""}>${k}</option>`).join("")+"</select>"}</div>`,v='<select id="cx2-token-size" class="cx2-token-size-sel" title="Grid footprint">'+_TF_TOKEN_SIZES.map(k=>`<option value="${k}"${k===m?" selected":""}>${k}</option>`).join("")+"</select>",b=((_codex2EntityLedger(t)||{})._token_weapons||{})[s]||"";_cx2IM.tokenWeaponByUrl||(_cx2IM.tokenWeaponByUrl={});const y=_cx2IM.tokenWeaponByUrl[s]!==void 0?_cx2IM.tokenWeaponByUrl[s]:b,_=`<div class="cx2-token-sizerow"><span class="form-helper-text">Tweaks</span><input type="text" id="cx2-token-weapon" class="cx2-token-weapon" maxlength="80" placeholder="e.g. replace sword with axe, add wings, no helmet" value="${escHtml(y)}"></div>`;if(r){const k=_TF_TOKEN_PX[m.toLowerCase()]||280;e.innerHTML=a+`<div class="cx2-token-row">
618        <div class="cx2-token-preview"><img src="${escHtml(r)}" alt="Token"></div>
619        <div class="cx2-token-actions">
620          ${c?"":`<div class="cx2-token-sizerow"><span class="form-helper-text">Size</span>${v}</div>
621          ${h}
622          ${_}`}
623          <button type="button" class="btn-new btn-xs" id="cx2-token-dl">Download VTT PNG</button>
624          <button type="button" class="btn-new btn-xs" id="cx2-token-dl-full">Full res</button>
625          ${c?"":'<button type="button" class="btn-new btn-xs" id="cx2-token-regen">Regenerate</button>'}
626          <button type="button" class="btn-ghost btn-xs btn-ghost--danger" id="cx2-token-rm">Remove</button>
627        </div>
628      </div>
629      ${c?'<p class="form-helper-text">This token\u2019s source image is no longer in the gallery \u2014 it can still be downloaded or deleted.</p>':""}
630      <p class="form-helper-text">Transparent PNG for your VTT \u2014 the body sits at grid scale (${m.toLowerCase()==="tiny"?"\xBD square":m.toLowerCase()==="large"?"2\xD72 squares":m.toLowerCase()==="huge"?"3\xD73 squares":m.toLowerCase()==="gargantuan"?"4\xD74 squares":"1 square"}, 280px per square); wings and gear flow past the square.</p>
631      <p class="form-helper-text hidden" id="cx2-token-overhang"></p>
632      ${c?"":'<p class="form-helper-text">Tweaks are saved with this image and reapplied every time you regenerate.</p>'}
633      ${_tfImgQuotaLine(_cx2IM&&_cx2IM.cid)}
634      ${_tfDeadringerBatchLine(t,_cx2IM&&_cx2IM.cid)}
635      ${d}${u}`;const C=e.querySelector("#cx2-token-dl");C&&(C.onclick=()=>_tfTokenDownload(_cx2IM&&_cx2IM.ent,{token:r,size:m,plan:f}));const S=e.querySelector("#cx2-token-dl-full");S&&(S.onclick=()=>_tfTokenDownload(_cx2IM&&_cx2IM.ent,{full:!0,token:r,size:m,plan:f}));const q=e.querySelector("#cx2-token-regen");q&&(q.onclick=_cx2TokenGenerate);const I=e.querySelector(".cx2-token-preview img");I&&(I.onload=()=>
635{const B=(I.naturalWidth||1024)*_tfTokenScale(_cx2IM&&_cx2IM.ent,f),H=e.querySelector("#cx2-token-overhang");H&&B>1044&&(H.textContent=`This token overhangs its square \u2014 in your VTT, set the token scale to \xD7${(B/1024).toFixed(1)} so the body stays at grid size.`,H.classList.remove("hidden"))});const T=e.querySelector("#cx2-token-rm");T&&(T.onclick=_cx2TokenRemove);const P=e.querySelector("#cx2-token-size");P&&(P.onchange=()=>{_tfTokenPersistChoice("size",P.value)})}else{e.innerHTML=a+`<div class="cx2-token-row">
636        <div class="cx2-token-sizerow"><span class="form-helper-text">Size</span>${v}</div>
637        ${h}
638        ${_}
639        <button type="button" class="btn-new btn-sm" id="cx2-token-gen">Make Token</button>
640      </div>
641      <p class="form-helper-text">A top-down battle-map token drawn from this entry\u2019s portrait. The body fills the creature\u2019s grid footprint; gear and wings may reach slightly past. Tweaks are saved with this image and reapplied every time you regenerate.</p>
642      ${_tfImgQuotaLine(_cx2IM&&_cx2IM.cid)}
643      ${_tfDeadringerBatchLine(t,_cx2IM&&_cx2IM.cid)}
644      ${d}${u}`;const k=e.querySelector("#cx2-token-gen");k&&(k.onclick=_cx2TokenGenerate)}e.querySelectorAll("[data-toksel]").forEach(k=>k.onclick=C=>{if(C.target&&C.target.closest&&C.target.closest("[data-tokrm]"))return;const S=k.dataset.toksel;_cx2IM&&(_cx2IM.tokenViewUrl=S,(_cx2IM.urls||[]).includes(S)&&_cx2ImgModSelectForEdit(S),_cx2ImgModRenderToken())}),e.querySelectorAll("[data-tokrm]").forEach(k=>k.onclick=C=>{C.stopPropagation(),_cx2TokenRemove(k.dataset.tokrm)}),e.querySelectorAll("[data-galuse]").forEach(k=>k.onclick=C=>{C.stopPropagation(),_cx2TokenGalleryUse(+k.dataset.galuse)}),e.querySelectorAll("[data-galrm]").forEach(k=>k.onclick=C=>{C.stopPropagation(),_cx2TokenGalleryRemove(+k.dataset.galrm)}),e.querySelectorAll("[data-galdl]").forEach(k=>k.onclick=C=>{C.stopPropagation();const S=(_cx2IM.gallery||[]).find(q=>q.id===+k.dataset.galdl);S&&_tfTokenDownload(_cx2IM.ent,{token:S.token_url,size:_tfTokenImgSettings(_cx2IM.ent,S.source_image_url||_cx2IM.ent.image_url).size})});const w=e.querySelector("#cx2-token-size");w&&!w.onchange&&(w.onchange=()=>_tfTokenPersistChoice("size",w.value));const $=e.querySelector("#cx2-token-plan");$&&($.onchange=()=>_tfTokenPersistChoice("plan",$.value));const E=e.querySelector("#cx2-token-weapon");E&&(E.oninput=()=>{_cx2IM&&(_cx2IM.tokenWeaponByUrl=_cx2IM.tokenWeaponByUrl||{},_cx2IM.tokenWeaponByUrl[s]=E.value)})}function _cx2TokenGalleryHtml(e){const t=_cx2IM&&_cx2IM.gallery||[],n=new Set(Object.values(_tfTokenMap(e))),a=t.filter(i=>!n.has(i.token_url));if(!a.length)return"";const s=[];return a.forEach(i=>{const o=i.set_label||"";let r=s.find(c=>c.key===o);r||(r={key:o,rows:[]},s.push(r)),r.rows.push(i)}),s.map(i=>{const o=i.key?`${escHtml(i.key)} \xB7 ${i.rows.length} token${i.rows.length===1?"":"s"}`:"More tokens",r=i.rows.some(c=>c.source==="deadringer")?' <span class="cx2-token-prov">Generated in Deadringer</span>':"";return`<div class="cx2-token-gallery"><p class="form-helper-text cx2-token-gallery-cap">${o}${r}</p><div class="cx2-token-strip">`+i.rows.map(c=>`<div class="cx2-token-strip-tile" title="${escHtml(c.label||"")}"><img src="${escHtml(c.token_url)}" alt="Token"><div class="cx2-token-gallery-acts"><button type="button" class="btn-new btn-xs" data-galuse="${c.id}" title="Make this the active token">Use</button><button type="button" class="btn-ghost btn-xs" data-galdl="${c.id}" title="Download VTT PNG">&#8615;</button></div><button type="button" class="cx2-token-strip-rm" data-galrm="${c.id}" title="Remove from this entry">&times;</button></div>`).join("")+"</div></div>"}).join("")}async function _cx2TokenGalleryUse(e){if(!_cx2IM)return;const t=_cx2IM.entityId,n=_cx2IM.cid;try{const a=await api("POST",`/api/campaigns/${n}/entities/${t}/tokens/${e}/activate`);a&&a.id&&(allEntities=allEntities.map(s=>s.id===a.id?{...s,token_url:a.token_url,token_urls:a.token_urls}:s),_cx2IM&&_cx2IM.entityId===t&&(_cx2IM.ent={..._cx2IM.ent,token_url:a.token_url,token_urls:a.token_urls},_cx2IM.tokenViewUrl=null,_cx2ImgModRenderToken())),showToast("Token set as active.")}catch(a){showToast(a&&a.message||"Could not switch the token.",4e3)}}async function _cx2TokenGalleryRemove(e){if(!_cx2IM)return;const t=_cx2IM.entityId,n=_cx2IM.cid;if(await confirmDialog({title:"Remove this token?",message:"It leaves this entry\u2019s gallery. The active token is not affected.",confirmLabel:"Remove",cancelLabel:"Keep it"}))try{await api("DELETE",`/api/campaigns/${n}/entities/${t}/tokens/${e}`),_cx2IM&&_cx2IM.entityId===t&&(_cx2IM.gallery=(_cx2IM.gallery||[]).filter(s=>s.id!==e),_cx2ImgModRenderToken()),showToast("Token removed.")}catch(s){showToast(s&&s.message||"Could not remove the token.",4e3)}}async function _cx2TokenGenerate(){if(!_cx2IM||_cx2IM.tokenBusy)return;const e=_cx2IM.entityId,t=_cx2IM.cid,n=_cx2ImgModEditUrl()||_cx2IM.ent&&_cx2IM.ent.image_url,a=_tfTokenImgSettings(_cx2IM.ent,n),s=document.getElementById("cx2-token-size"),i=s&&s.value||a.size,o=document.getElementById("cx2-token-plan"),r=o&&o.value||a.plan,c=document.getElementById("cx2-token-weapon"),l=(c?c.value:"").trim();_cx2IM.tokenBusy=!0,_cx2ImgModRenderToken();let d=null,u=null;try{const p={size:i,source_url:n,weapon:l,plan:r};d=await api("POST",`/api/campaigns/${t}/entities/${e}/generate-token`,p)}catch(p){u=p}if(d&&typeof d.cap=="number"&&(_tfImgQuota[t]={used:+d.used||0,cap:+d.cap}),u&&u.status===402){const p=_tfImgQuota[t];_tfImgQuota[t]={used:p&&p.cap||1,cap:p&&p.cap||1}}u&&u.status===403&&/paid plan/i.test(u.message||"")&&(_tfImgQuota[t]={used:0,cap:0}),d&&d.id&&(allEntities=allEntities.map(p=>p.id===d.id?{...p,token_url:d.token_url,token_urls:d.token_urls,ledger:d.ledger}:p)),_cx2IM&&_cx2IM.entityId===e&&(d&&d.id&&(_cx2IM.ent={..._cx2IM.ent,token_url:d.token_url,token_urls:d.token_urls,ledger:d.ledger}),_cx2IM.tokenBusy=!1,_cx2IM.tokenViewUrl=null,_cx2IM.gallery=void 0,_cx2ImgModRenderToken()),u?showToast(u&&u.message||"Token generation failed, try again.",5e3):showToast("Token rendered.")}async function _tfTokenDownload(e,t){const n=t&&t.token||e&&e.token_url;if(!e||!n)return;const a=!!(t&&t.full),s=t&&t.size||_tfLedgerSize(e),i=_TF_TOKEN_PX[s.toLowerCase()]||280,o=await new Promise(u=>{const p=new Image;p.onload=()=>
644u(p.naturalWidth||1024),p.onerror=()=>u(1024),p.src=n}),r=Math.round(o*i/1024*_tfTokenScale(e,t&&t.plan)),c=a?n:n.replace("/upload/",`/upload/c_scale,w_${r}/`),l=o/1024,d=`${e.type==="companion"?"Companion":String(e.type||"").toUpperCase()}_${String(e.name||"Token").replace(/[^A-Za-z0-9]+/g,"")}_${s}${l>1.02?"_x"+l.toFixed(1):""}${a?"_full":""}.png`;try{const u=await(await fetch(c)).blob(),p=document.createElement("a");p.href=URL.createObjectURL(u),p.download=d,document.body.appendChild(p),p.click(),p.remove(),URL.revokeObjectURL(p.href)}catch{showToast("Download failed, try again.",4e3)}}async function _cx2TokenRemove(e){if(!_cx2IM)return;const t=_cx2IM.entityId,n=_cx2IM.cid,a=typeof e=="string"&&e||_cx2IM.tokenViewUrl||_cx2ImgModEditUrl()||_cx2IM.ent&&_cx2IM.ent.image_url;if(!await confirmDialog({title:"Remove this token?",message:"This image\u2019s token is removed from the entry. You can make a new one any time.",confirmLabel:"Remove",cancelLabel:"Keep it"}))return;try{await api("DELETE",`/api/campaigns/${n}/entities/${t}/token?source_url=${encodeURIComponent(a||"")}`)}catch(o){showToast(o&&o.message||"Could not remove the token.",4e3);return}const i=o=>{const r={...o.token_urls||{}};return delete r[a],{...o,token_urls:r,token_url:o.image_url===a?null:o.token_url}};allEntities=allEntities.map(o=>o.id===t?i(o):o),_cx2IM&&_cx2IM.entityId===t&&(_cx2IM.ent=i(_cx2IM.ent),_cx2IM.tokenViewUrl===a&&(_cx2IM.tokenViewUrl=null),_cx2ImgModRenderToken()),showToast("Token removed.")}async function _cx2ImgModUpload(e){const t=Array.from(e.target&&e.target.files||[]);if(e.target.value="",!t.length)return;const n=document.getElementById("cx2-imgmod-upstatus");_cx2IM.uploading=!0,n&&n.classList.remove("hidden");const a=document.getElementById("cx2-imgmod-save");a&&(a.disabled=!0);try{for(let s=0;s<t.length;s++){n&&(n.textContent=t.length>1?`Uploading ${s+1} of ${t.length}\u2026`:"Uploading\u2026");const i=await _tfUploadDirect(t[s],{paramsPath:`/api/campaigns/${_cx2IM.cid}/upload-params`,kind:"entity",endpoint:`/api/campaigns/${_cx2IM.cid}/entities/${_cx2IM.entityId}/upload-image`});allEntities=allEntities.map(c=>c.id===i.id?i:c),_tfHandleAutoInline(i);const r=_entityImageGallery(i).find(c=>_cx2IM.origUrls.indexOf(c)===-1);r&&(_cx2IM.urls=[r,..._cx2IM.urls.filter(c=>c!==r)],_cx2IM.origUrls.indexOf(r)===-1&&(_cx2IM.origUrls=[..._cx2IM.origUrls,r]))}n&&n.classList.add("hidden"),_cx2ImgModRenderGallery(),_cx2ImgModRenderTreatment(),_cx2ImgModRenderPreview(),_cx2ImgModPrimaryFocalReset()}catch(s){n&&(n.textContent="Upload failed: "+(s&&s.message?s.message:s))}finally{_cx2IM.uploading=!1,a&&(a.disabled=!1)}}function _cx2ImgModDirty(){const e=JSON.stringify(_cx2IM.urls)!==JSON.stringify(_cx2IM.origUrls),t=["type","edge","depth","feather","deckle","scale"].some(u=>_cx2IM.pending[u]!==_cx2IM.origPending[u]),n=_cx2ImgModTypeAllowed(_cx2IM.pending.type),a=_cx2IM.focal,s=_cx2IM.origFocal,i=!!(a&&s&&(Math.round(a.x)!==Math.round(s.x)||Math.round(a.y)!==Math.round(s.y))),o=_cx2IM.crop||{},r=_cx2IM.origCrop||{},c=(o.ar||null)!==(r.ar||null)||!!o.ar&&Math.abs((+o.zoom||1)-(+r.zoom||1))>.005||!!o.ar&&(Math.round(o.cx!=null?o.cx:-1)!==Math.round(r.cx!=null?r.cx:-1)||Math.round(o.cy!=null?o.cy:-1)!==Math.round(r.cy!=null?r.cy:-1)),l=!!_cx2IM.mirror!=!!_cx2IM.origMirror,d=!_cx2IM.pctx&&String(_cx2IM.label||"").trim()!==String(_cx2IM.origLabel||"").trim();return{images:e,treat:t&&n,crop:c&&n,mirror:l&&n,focal:i,label:d}}async function _cx2ImgModSave(){if(_cx2IM.uploading)return;const e=_cx2ImgModDirty(),t=_cx2IM.entityId,n=_cx2IM.cid,a=_cx2IM.urls.slice(),s=_cx2ImgModEditUrl(),i=s===a[0],o={..._cx2IM.pending},r=_cx2IM.spec&&_cx2IM.spec.shadow,c=!!_cx2IM.mirror,l=_cx2IM.focal?{x:Math.round(c?100-_cx2IM.focal.x:_cx2IM.focal.x),y:Math.round(_cx2IM.focal.y)}:null,d=_cx2CropPayload();if(_cx2IM.crop&&_cx2IM.crop.ar&&!d){alert("The image is still loading, try Save again in a moment.");return}const u=e.treat||e.crop||e.mirror,p=o.type==="dissolve"||o.type==="cutout",m=e.treat&&p&&typeof _cx2ImgModTypeBaked=="function"&&!_cx2ImgModTypeBaked(o.type)||(e.crop||e.mirror)&&p,f=_cx2IM.pctx,g=document.getElementById("cx2-imgmod-save");g&&(g.disabled=!0);try{let h=!1;if(f){if(e.images&&await api("POST",`/api/campaigns/${n}/sessions/${f.sessionId}/inline/${t}/image`,{image_url:a[0]}),u){const v={treatment:o.type||"dissolve",edge:o.edge,depth:o.depth,feather:o.feather,deckle:o.deckle,scale:o.scale,shadow:!!r,mirror:c};d&&(v.crop=d),await api("POST",`/api/campaigns/${n}/sessions/${f.sessionId}/inline/${t}/treatment`,v)}}else{const v=a[0]!==_cx2IM.origUrls[0],b=(u||e.images&&v&&i)&&_cx2ImgModTypeAllowed(o.type),y={image_urls:a};i||(y.edit_url=s),b&&(y.treatment={treatment:o.type||"dissolve",edge:o.edge,depth:o.depth,feather:o.feather,deckle:o.deckle,scale:o.scale,shadow:!!r,mirror:c},d&&(y.treatment.crop=d)),e.focal&&l&&(y.focal=l),e.label&&(y.label=String(_cx2IM.label||"").trim());const _=await api("POST",`/api/campaigns/${n}/entities/${t}/image-editor/save`,y),{bake_queued:w,bake_processing:$,auto_inline:E,...k}=_;h=w===!0,allEntities=(allEntities||[]).map(C=>C.id===k.id?k:C);try{renderCompendium()}catch{}}_cx2ImgModClose(!0),!f&&(e.images||e.crop||e.mirror||e.treat||e.label)&&(_codex2RenderCache.cid=null),f||_codex2RerenderIfOpen(),_chr2RerenderIfOpen(),(m||h)&&(f||i)&&_cx2BakeWatch(n,t,f?{sessionId:f.sessionId}:void 0),typeof showToast=="function"&&showToast(m||h?!f&&!i?"\u2726 Treatment requested; it will be ready wherever this image appears":e.crop&&!e.treat?"\u2726 Framing requested; it appears here as soon as it\u2019s ready":"\u2726 Treatment requested; it appears here as soon as it\u2019s ready":e.crop?"Framing saved":e.treat?"Treatment saved":e.images?"Image updated":e.focal?"Focus point saved":"Label saved")}catch(h){g&&(g.disabled=!1),alert("Save failed: "+(h&&h.message?h.message:h))}}async function _cx2ImgModClose(e){const t=document.getElementById("cx2-imgmod-overlay");if(!t||!_cx2IM||_cx2IM.uploading)return;if(e!==!0){const a=_cx2ImgModDirty();if((a.images||a.treat||a.crop||a.mirror||a.focal||a.label)&&!await confirmDialog({title:"Discard changes?",message:"Discard your changes to this image?",confirmLabel:"Discard",cancelLabel:"Keep editing"}))return}if(_cx2IM.hero)try{_cx2IM.hero.dispose()}catch{}t.classList.add("hidden"),_cx2IM=null;const n=_cx2ImOnClose;if(_cx2ImOnClose=null,typeof n=="function")try{n()}catch{}}const _CX2_CYCLE_MS=8e3,_CX2_CYCLE_FADE_MS=450;let _cx2CycleSeq=0;async function _cx2HeroCycleStart(e,t,n,a,s){const i=++_cx2CycleSeq,o=_entityImageGallery(a);if(o.length<2||window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches)return;const r=e.querySelector(".cx2-portrait");if(!r)return;let c;try{const g=await api("GET",`/api/campaigns/${t}/entities/${n}/gallery/render`);c=g&&g.items||[]}catch{return}if(i!==_cx2CycleSeq||!r.isConnected)return;const l=s!=="fullbleed";let d=c.filter(g=>g&&g.image_url);if(l&&(d=d.filter(g=>g.mode==="cutout"||g.mode==="dissolve")),d.length<2)return;const u=r.querySelector(l?"canvas":"img");if(!u)return;const p=l?e._cx2Hero||window.CodexHero&&CodexHero.mount(u):null;if(l&&!p)return;l&&(e._cx2Hero=p),r.classList.add("cx2-portrait--cycling");let m=Math.max(0,d.findIndex(g=>g.source_url===o[0]));const f=setInterval(async()=>
644{if(i!==_cx2CycleSeq||!r.isConnected){clearInterval(f);return}if(document.visibilityState!=="visible"||_cx2IM)return;const g=d[(m+1)%d.length];if(await new Promise(h=>{const v=new Image;v.onload=v.onerror=()=>h(),v.src=g.image_url}),i!==_cx2CycleSeq||!r.isConnected){clearInterval(f);return}if(r.classList.add("cx2-hero-fade-out"),await new Promise(h=>setTimeout(h,_CX2_CYCLE_FADE_MS)),i!==_cx2CycleSeq||!r.isConnected){clearInterval(f),r.classList.remove("cx2-hero-fade-out");return}try{if(l)await p.show(g);else{u.src=g.image_url;const h=r.querySelector(".cx2-cap");if(h){let v=h.querySelector(".cap-label");g.label?(v||(v=document.createElement("span"),v.className="cap-label",h.insertBefore(v,h.querySelector(".credit")||null)),v.textContent=g.label):v&&v.remove()}}}catch{}m=(m+1)%d.length,r.classList.remove("cx2-hero-fade-out")},_CX2_CYCLE_MS)}async function _codex2HandleHash(){const e=(location.hash||"").match(/^#codex\/(\d+)\/(\d+)$/);if(!e)return!1;const t=+e[1],n=+e[2];if(typeof getStoredToken!="function"||!getStoredToken())return!1;const a=++_tfNavSeq;_tfPendingLanding="codex";try{return(!currentCampaign||currentCampaign.id!==t)&&await bootWithCampaignId(t),_tfNavStale(a)||await renderCodexEntry(n),!0}catch(s){return console.warn("[codex2] deep-link failed",s),!1}finally{_tfPendingLanding=null}}window.addEventListener("hashchange",_codex2HandleHash);const _CHR2_MARK_SVG='<svg viewBox="0 0 556 290" aria-hidden="true"><polygon points="0,289 33,269 77,232 89,230 127,187 143,190 189,125 195,126 203,116 235,66 239,64 248,71 275,20 290,10 293,0 316,75 316,290 0,290" fill="currentColor"/><polygon points="334,68 352,84 375,119 387,121 414,154 438,170 504,245 556,288 556,290 334,290" fill="currentColor" opacity="0.5"/></svg>',_CHR2_QUILL_SVG='<svg class="quill" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 4s-7 1-12 6c-3 3-4 9-4 9s6-1 9-4c5-5 7-11 7-11z"/><path d="M4 19l5-5"/></svg>';function _chr2Panel(){let e=document.getElementById("tab-chronicle2");if(!e){e=document.createElement("div"),e.id="tab-chronicle2",e.className="tab-panel hidden",e.setAttribute("role","tabpanel");const t=document.getElementById("tab-compendium");t.parentNode.insertBefore(e,t.nextSibling)}return e}function _chr2Show(e,t){document.querySelectorAll(".tab-panel").forEach(n=>n.classList.add("hidden")),document.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),document.querySelector('[data-tab="sessions"]')?.classList.add("active"),e.classList.remove("hidden"),_tfStampCampaignHash(t)}function _chr2Activate(e,t){e&&(e.addEventListener("click",t),e.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),t())}),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0"),e.getAttribute("role")||e.setAttribute("role","button"))}function _chr2SessionNum(e){if(e.is_session_zero)return 0;const t=(allSessions||[]).filter(a=>!a.is_session_zero),n=t.findIndex(a=>a.id===e.id);return n>=0?t.length-n:e.number||0}function _chr2Roman(e){if(e=Math.floor(e),!(e>=1))return String(e);const t=[[1e3,"M"],[900,"CM"],[500,"D"],[400,"CD"],[100,"C"],[90,"XC"],[50,"L"],[40,"XL"],[10,"X"],[9,"IX"],[5,"V"],[4,"IV"],[1,"I"]];let n="";for(const[a,s]of t)for(;e>=a;)n+=s,e-=a;return n}function _chr2Zeros(){return(allSessions||[]).filter(e=>e.is_session_zero).sort((e,t)=>e.id-t.id).map(e=>({s:e,num:0}))}function _chr2ChapLabel(e){return e===0?"Prologue":"Chapter "+_chr2Roman(e)}function _chr2DateShort(e){if(!e.date)return"";try{return new Date(e.date+"T12:00:00").toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}catch{return""}}function _chr2DateLong(e){if(!e.date)return"";try{return new Date(e.date+"T12:00:00").toLocaleDateString(void 0,{month:"long",day:"numeric",year:"numeric"})}catch{return""}}function _chr2OneLine(e){if(e.session_snippet&&e.session_snippet.trim())return e.session_snippet.trim();if(e.session_blurb&&typeof _pickInsightfulSentence=="function"){const a=_pickInsightfulSentence(e.session_blurb);if(a)return a}const t=(e.session_blurb||e.summary||"").replace(/^#.*$/gm,"").replace(/\*\*/g,"").trim(),n=t.match(/[^.!?]+[.!?]/);return n?n[0].trim():t.slice(0,160)}function _chr2BriefText(e){if(!e)return"";const t=/^(?:tl;?dr|in brief|summary)\s*[:.\u2014-]?\s*/i,n=o=>String(o||"").replace(/\[\[/g,"").replace(/\]\]/g,"").replace(/\*\*(.+?)\*\*/g,"$1").replace(/\s*\[\d{1,3}\]/g,"").trim().replace(t,"").trim(),a=o=>{if(o=o.trim(),/^#/.test(o)){const r=o.replace(/^#{1,6}\s*/,"").trim();return t.test(r)?r.replace(t,"").trim():""}return o.replace(t,"").trim()},s=n(e.session_blurb);if(s)return s;const i=String(e.summary||"");if(i.trim()){const o=i.split(`
645`),r=o.findIndex(c=>/^##\s+/.test(c));if(r>
6450){const c=n(o.slice(0,r).map(a).filter(Boolean).join(" "));if(c&&c.length<=900)return c}}return""}function _chr2Beats(e){return e?e.split(`
646`).map(t=>{const n=t.match(/^##\s+(.+)/);return n?n[1].trim():null}).filter(Boolean):[]}function _chr2InThisSession(e){if(!e||!e.summary||!Array.isArray(allEntities))return[];const t=e.summary,n=[];for(const a of allEntities){if(_hiddenFromViewer(a))continue;const s=_teaseView(a),i=(s.name||"").trim();if(!(i.length<3))try{const o=new RegExp("\\b"+i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b","gi"),r=(t.match(o)||[]).length;r>0&&n.push({e:s,c:r})}catch{}}return n.sort((a,s)=>s.c-a.c),n.slice(0,14).map(a=>a.e)}function _chr2PickLayout(e){return e<=3?"broadsheet":"standard"}function _chr2MonthSpan(e){const t=e.filter(i=>i.s.date);if(t.length<2)return null;const n=new Date(t[0].s.date+"T12:00:00"),a=new Date(t[t.length-1].s.date+"T12:00:00"),s=Math.round((a-n)/(1e3*60*60*24*30.44));return s>=1?s:null}function _chr2BuildSections(e,t){const n=Array.isArray(t)?t:[];if(!n.length)return e.length?[{act:1,kind:"act",name:"",breakBefore:1,synthetic:!0}]:[];const a=e.length||1,s=n.map(o=>{const r=o.break_before!=null?o.break_before:o.start!=null?o.start:1;return{act:o.act,kind:o.kind||"act",name:o.name||"",breakBefore:r}});s.sort((o,r)=>o.breakBefore-r.breakBefore||o.act-r.act),s.length&&(s[0].breakBefore=1);const i=new Set([1]);for(let o=1;o<s.length;o++){let r=Math.max(2,Math.min(a,Math.round(s[o].breakBefore)||2));for(;i.has(r)&&r<a;)r++;for(;i.has(r)&&r>2;)r--;s[o].breakBefore=r,i.add(r)}return s}function _chr2SecForN(e,t){let n=e[0]||null;for(const a of e)a.breakBefore<=t&&(!n||a.breakBefore>=n.breakBefore)&&(n=a);return n}function _chr2SecLabel(e,t){if(t.kind==="prologue")return"Prologue";if(t.kind==="epilogue")return"Epilogue";let n=0;for(const a of e)if(a.kind==="act"&&(n++,a.act===t.act))return"Act "+n;return"Act "+(n||1)}function _chr2ValidGaps(e,t,n){const a=new Set(e.filter(i=>i.act!==n).map(i=>i.breakBefore)),s=[];for(let i=2;i<=t;i++)a.has(i)||s.push(i);return s}function _chr2NewActId(e){let t=0;for(const n of e)n.act>t&&(t=n.act);return t+1}function _chr2DefaultLead(){return`A complete account of ${currentCampaign&&currentCampaign.name||"Campaign"}'s sessions, recorded chapter by chapter, as it happened at the table.`}function _chr2TitleOrName(){return currentCampaign&&(currentCampaign.chronicle_title||"").trim()||currentCampaign&&currentCampaign.name||"The Chronicle"}function _chr2CanGenerate(){return!!(_tfBillingTier&&_tfBillingTier!=="free")}function _chr2OpenFieldEditor(e,t){const n=t==="title",a=e.querySelector(n?"#chr2-title-el":"#chr2-lead-el");if(!a||a.dataset.editing==="1")return;a.dataset.editing="1";const s=a.closest(".chr2-field-row");s&&s.classList.add("editing");const i=n?"chronicle_title":"chronicle_lead",o=currentCampaign&&currentCampaign.id,r=a.innerHTML,c=[],l=document.createElement(n?"input":"textarea");l.className=n?"chr2-inline-input chr2-title-input":"chr2-inline-textarea",l.value=(currentCampaign[i]||"").trim(),n||(l.rows=3),l.setAttribute("aria-label",n?"Edit chronicle title":"Edit story lead");const d=_chr2CanGenerate(),u=document.createElement("div");u.className="chr2-editor-actions";const p=d?'<button type="button" class="chr2-ed-gen btn-ghost btn-xs" title="Generate with AI" aria-label="Generate with AI">\u2726</button>':'<button type="button" class="chr2-ed-gen chr2-ed-gen--locked btn-ghost btn-xs" title="AI generation: Hobby plan and up" aria-label="AI generation requires a paid plan">\u2726</button>';u.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button>'+p+'<button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const m=document.createElement("div");m.className="chr2-field-editor",m.appendChild(l),m.appendChild(u),a.innerHTML="",a.appendChild(m),l.focus(),n&&l.select();const f=(h,v)=>{a.dataset.editing="",s&&s.classList.remove("editing"),v!=null?a.textContent=v:a.innerHTML=h};u.querySelector(".chr2-ed-cancel").addEventListener("click",()=>f(r)),u.querySelector(".chr2-ed-save").addEventListener("click",async()=>{let h=l.value.trim();n||(h=h.replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim()),await _chr2SaveCampaignField(i,h)&&f(null,h||(n?_chr2TitleOrName():_chr2DefaultLead()))});const g=u.querySelector(".chr2-ed-gen");d?g.addEventListener("click",async()=>
646{if(g.disabled)return;g.disabled=!0;const h=g.textContent;g.textContent="\u2026";try{const v=n?"generate-chronicle-title":"generate-chronicle-lead",b=n&&c.length?{avoid:c}:{},y=await api("POST",`/api/campaigns/${o}/${v}`,b),_=((n?y.title:y.lead)||"").replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim();_?(n&&c.push(_),l.value=_,l.focus()):showToast("Could not generate, try again.",4e3)}catch(v){showToast(`Generation failed: ${v.message||v}`,5e3)}finally{g.textContent=h,setTimeout(()=>{g.disabled=!1},3e3)}}):g.addEventListener("click",()=>{showToast("AI generation is on the Hobby plan and up.",4e3),typeof _showPricingScreen=="function"&&_showPricingScreen()}),l.addEventListener("keydown",h=>{h.key==="Escape"?(h.preventDefault(),f(r)):h.key==="Enter"&&(n||h.ctrlKey||h.metaKey)&&(h.preventDefault(),u.querySelector(".chr2-ed-save").click())})}async function _chr2SaveCampaignField(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return!1;try{const a=await api("PUT",`/api/campaigns/${n}`,{[e]:t||null});return currentCampaign={...currentCampaign,...a},!0}catch(a){return showToast(`Save failed: ${a.message||a}`,5e3),!1}}function _chr2OpenCaptionEditor(e,t){const n=e.querySelector("#cr-herocap");if(!n||n.dataset.editing==="1")return;const a=currentCampaign&&currentCampaign.id,s=(allSessions||[]).find(d=>d.id===t);if(!a||!s)return;n.dataset.editing="1",n.classList.add("editing");const i=n.innerHTML,o=document.createElement("textarea");o.className="chr2-inline-textarea scene-cap-input",o.rows=2,o.value=(s.header_caption||"").trim(),o.setAttribute("aria-label","Edit chapter caption"),o.placeholder="A short caption under the banner\u2026";const r=document.createElement("div");r.className="chr2-editor-actions",r.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const c=document.createElement("div");c.className="chr2-field-editor scene-cap-editor",c.appendChild(o),c.appendChild(r),n.innerHTML="",n.appendChild(c),o.focus();const l=d=>{n.dataset.editing="",n.classList.remove("editing"),n.innerHTML=d};r.querySelector(".chr2-ed-cancel").addEventListener("click",()=>l(i)),o.addEventListener("keydown",d=>{d.key==="Escape"&&(d.preventDefault(),l(i))}),r.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const d=o.value.replace(/\s+/g," ").trim();try{const u=await api("PUT",`/api/campaigns/${a}/sessions/${t}`,{header_caption:d});allSessions=(allSessions||[]).map(p=>p.id===t?u:p),n.dataset.editing="",n.classList.remove("editing"),renderChronicleRecap(t)}catch(u){showToast("Save failed: "+(u&&u.message||u),4e3)}})}function _chr2TitleCandidates(e){try{const t=e&&e.recap_step_cache&&e.recap_step_cache.session_title_candidates,n=t&&t.output;if(!n)return[];const a=(e&&e.title||"").trim().toLowerCase(),s=(Array.isArray(n.candidates)?n.candidates:[]).filter(r=>r&&typeof r.title=="string"&&r.title.trim()&&r.title.trim().toLowerCase()!==a),i=(n.backup||"").trim().toLowerCase();return s.slice().sort((r,c)=>(c.title.trim().toLowerCase()===i)-(r.title.trim().toLowerCase()===i)).slice(0,2)}catch{return[]}}const _TF_TITLE_ALTS_HINT_KEY="tf_title_alts_hint_seen_v1";function _tfTitleAltsHintDue(e){if(!e||!_chr2TitleCandidates(e).length)return!1;try{return localStorage.getItem(_TF_TITLE_ALTS_HINT_KEY)!=="1"}catch{return!1}}function _tfTitleAltsHintSeen(){try{localStorage.setItem(_TF_TITLE_ALTS_HINT_KEY,"1")}catch{}document.querySelectorAll(".chapter-title-edit--hinted").forEach(e=>{e.classList.remove("chapter-title-edit--hinted"),e.querySelector(".chapter-title-edit-hint")?.remove(),e.setAttribute("title","Rename chapter"),e.setAttribute("aria-label","Rename chapter")})}function _chr2OpenChapterTitleEditor(e,t){_tfTitleAltsHintSeen();const n=e.querySelector("#cr-title");if(!n||n.dataset.editing==="1")return;const a=currentCampaign&&currentCampaign.id,s=(allSessions||[]).find(m=>m.id===t);if(!a||!s)return;n.dataset.editing="1";const i=n.innerHTML,o=document.createElement("input");o.type="text",o.className="chr2-inline-input chapter-title-input",o.value=s.title||"",o.maxLength=200,o.setAttribute("aria-label","Chapter title");const r=document.createElement("span");
646r.className="chr2-editor-actions",r.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const c=document.createElement("span");c.className="chr2-title-box-row",c.appendChild(o),c.appendChild(r);const l=document.createElement("span");l.className="chr2-title-box",l.appendChild(c);const d=_chr2TitleCandidates(s);if(d.length){const m=document.createElement("div");m.className="chr2-title-suggest",m.innerHTML='<span class="form-section-label">Other titles the bard wrote</span><span class="chr2-title-suggest-row">'+d.map(f=>`<button type="button" class="btn-new btn-xs chr2-title-chip" data-title-pick="${escHtml(f.title)}">${escHtml(f.title)}</button>`).join("")+"</span>",m.addEventListener("click",f=>{const g=f.target.closest("[data-title-pick]");g&&(_tfTitleAltsHintSeen(),o.value=g.getAttribute("data-title-pick")||"",o.focus(),o.select())}),l.appendChild(m)}const u=document.createElement("span");u.className="chr2-field-editor chapter-title-editor",u.appendChild(l),n.innerHTML="",n.appendChild(u),o.focus(),o.select();const p=m=>{n.dataset.editing="",n.innerHTML=m};r.querySelector(".chr2-ed-cancel").addEventListener("click",()=>p(i)),o.addEventListener("keydown",m=>{m.key==="Escape"?(m.preventDefault(),p(i)):m.key==="Enter"&&(m.preventDefault(),r.querySelector(".chr2-ed-save").click())}),r.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const m=o.value.trim();if(!m){o.focus();return}if(m===(s.title||"")){p(i);return}try{const f=await api("PUT",`/api/campaigns/${a}/sessions/${t}`,{title:m});allSessions=(allSessions||[]).map(g=>g.id===t?f:g),n.dataset.editing="",renderChronicleRecap(t)}catch(f){showToast("Save failed: "+(f&&f.message||f),4e3)}})}function _chr2OpenDatelineEditor(e,t){const n=e.querySelector("#cr-dateline");if(!n||n.dataset.editing==="1")return;const a=currentCampaign&&currentCampaign.id,s=(allSessions||[]).find(d=>d.id===t);if(!a||!s)return;n.dataset.editing="1";const i=n.innerHTML,o=(s.date||"").slice(0,10).split("-"),r=o.length===3,c=document.createElement("span");c.className="chr2-field-editor chapter-date-editor",c.innerHTML=_tfBuildDatePartsHTML(r?{m:String(parseInt(o[1])),d:String(parseInt(o[2])),y:o[0].slice(2)}:{})+'<span class="chr2-editor-actions"><button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button></span>',n.innerHTML="",n.appendChild(c),_tfWireDateParts(c),c.querySelector(".date-part").focus();const l=d=>{n.dataset.editing="",n.innerHTML=d};c.querySelector(".chr2-ed-cancel").addEventListener("click",()=>l(i)),c.addEventListener("keydown",d=>{d.key==="Escape"?(d.preventDefault(),l(i)):d.key==="Enter"&&(d.preventDefault(),c.querySelector(".chr2-ed-save").click())}),c.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const d=_tfReadDateParts(c);if(d.error){showToast(d.error,4e3);return}const u=d.blank?{clear_date:!0}:{date:d.iso};try{const p=await api("PUT",`/api/campaigns/${a}/sessions/${t}`,u);allSessions=(allSessions||[]).map(m=>m.id===t?p:m),_tfResortSessions(),n.dataset.editing="",renderChronicleRecap(t)}catch(p){showToast("Save failed: "+(p&&p.message||p),4e3)}})}function _chr2OpenChapterBodyEditor(e,t){const n=e.querySelector("#cr-body");if(!n||n.dataset.editing==="1")return;const a=currentCampaign&&currentCampaign.id,s=(allSessions||[]).find(u=>u.id===t);if(!a||!s)return;n.dataset.editing="1",n.classList.add("editing");const i=n.innerHTML,o=document.createElement("textarea");o.className="chr2-inline-textarea chapter-body-input",o.value=s.summary||"",o.rows=20,o.setAttribute("aria-label","Chapter prose");const r=document.createElement("div");r.className="chr2-editor-actions",r.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const c=document.createElement("p");c.className="chapter-body-hint",c.textContent="Start a line with ## to add a header. Any text above the first ## isn\u2019t shown anywhere \u2014 the TL;DR players read is the Recap TL;DR bar above.";const l=document.createElement("div");
646l.className="chr2-field-editor chapter-body-editor",l.appendChild(o),l.appendChild(c),l.appendChild(r),n.innerHTML="",n.appendChild(l),o.focus();const d=u=>{n.dataset.editing="",n.classList.remove("editing"),s.summary?n.innerHTML=u:renderChronicleRecap(t)};r.querySelector(".chr2-ed-cancel").addEventListener("click",()=>d(i)),o.addEventListener("keydown",u=>{u.key==="Escape"&&(u.preventDefault(),d(i))}),r.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const u=o.value;if(u===(s.summary||"")){d(i);return}try{const p=await api("PUT",`/api/campaigns/${a}/sessions/${t}`,{summary:u});allSessions=(allSessions||[]).map(m=>m.id===t?p:m),n.dataset.editing="",n.classList.remove("editing"),renderChronicleRecap(t)}catch(p){showToast("Save failed: "+(p&&p.message||p),4e3)}})}function _chr2OpenTocNameEditor(e,t,n){const a=t.querySelector(".toc-title");if(!a||t.dataset.editing==="1")return;const s=currentCampaign&&currentCampaign.id,i=(allSessions||[]).find(u=>u.id===n);if(!s||!i)return;t.dataset.editing="1";const o=document.createElement("input");o.type="text",o.className="chr2-inline-input toc-name-input",o.value=i.title||"",o.maxLength=200,o.setAttribute("aria-label","Session name"),["click","mousedown"].forEach(u=>o.addEventListener(u,p=>p.stopPropagation()));let r=!1;const c=async()=>{if(r)return;r=!0;const u=o.value.trim();if(t.dataset.editing="",u&&u!==(i.title||""))try{const p=await api("PUT",`/api/campaigns/${s}/sessions/${n}`,{title:u});allSessions=(allSessions||[]).map(m=>m.id===n?p:m)}catch(p){showToast("Save failed: "+(p&&p.message||p),4e3)}_chr2RenderActsToc(e)};o.addEventListener("keydown",u=>{u.stopPropagation(),u.key==="Enter"?(u.preventDefault(),c()):u.key==="Escape"&&(u.preventDefault(),r=!0,t.dataset.editing="",_chr2RenderActsToc(e))}),o.addEventListener("blur",c);const l=_chr2TitleCandidates(i);let d=o;if(l.length){const u=document.createElement("span");u.className="chr2-toc-editor",u.appendChild(o);const p=document.createElement("span");p.className="chr2-title-suggest-row chr2-toc-suggest-row",l.forEach(m=>{const f=document.createElement("span");f.className="btn-new btn-xs chr2-title-chip",f.setAttribute("role","button"),f.setAttribute("tabindex","0"),f.textContent=m.title,f.addEventListener("mousedown",g=>{g.preventDefault(),g.stopPropagation()}),f.addEventListener("click",g=>{g.preventDefault(),g.stopPropagation(),_tfTitleAltsHintSeen(),o.value=m.title,o.focus(),o.select()}),p.appendChild(f)}),["click","mousedown"].forEach(m=>u.addEventListener(m,f=>f.stopPropagation())),u.appendChild(p),d=u}a.replaceWith(d),o.focus(),o.select()}function _chr2OpenTocSnippetEditor(e,t,n){const a=t.querySelector(".toc-teaser");if(!a||t.dataset.editing==="1")return;const s=currentCampaign&&currentCampaign.id,i=(allSessions||[]).find(d=>d.id===n);if(!s||!i)return;t.dataset.editing="1";const o=i.session_snippet&&i.session_snippet.trim()?i.session_snippet.trim():_chr2OneLine(i)||"",r=document.createElement("input");r.type="text",r.className="toc-snippet-input",r.value=o,r.maxLength=240,r.setAttribute("aria-label","Chapter teaser"),r.title="Empty restores the auto-generated teaser",["click","mousedown"].forEach(d=>r.addEventListener(d,u=>u.stopPropagation()));let c=!1;const l=async()=>{if(c)return;c=!0;const d=r.value.trim();if(t.dataset.editing="",d!==o)try{const u=await api("PUT",`/api/campaigns/${s}/sessions/${n}`,{session_snippet:d});allSessions=(allSessions||[]).map(p=>p.id===n?u:p)}catch(u){showToast("Save failed: "+(u&&u.message||u),4e3)}_chr2RenderActsToc(e)};r.addEventListener("keydown",d=>{d.stopPropagation(),d.key==="Enter"?(d.preventDefault(),l()):d.key==="Escape"&&(d.preventDefault(),c=!0,t.dataset.editing="",_chr2RenderActsToc(e))}),r.addEventListener("blur",l),a.innerHTML="",a.appendChild(r),r.focus(),r.select()}function _chr2OpenBardicEditor(e,t){const n=e.querySelector("#cr-bardic");if(!n||n.dataset.editing==="1")return;const a=currentCampaign&&currentCampaign.id,s=(allSessions||[]).find(m=>m.id===t);if(!a||!s)return;n.dataset.editing="1",n.classList.add("editing");const i=n.innerHTML,o=document.createElement("textarea");
646o.className="chr2-inline-textarea chapter-bardic-input",o.rows=3,o.value=(s.chronicle_bardic||"").trim(),o.setAttribute("aria-label","Edit bardic standfirst"),o.placeholder="A short, witty standfirst in the bardic voice\u2026";const r=_chr2CanGenerate(),c=document.createElement("div");c.className="chr2-editor-actions";const l=r?'<button type="button" class="chr2-ed-gen btn-ghost btn-xs" title="Generate with AI" aria-label="Generate with AI">\u2726</button>':'<button type="button" class="chr2-ed-gen chr2-ed-gen--locked btn-ghost btn-xs" title="AI generation: Hobby plan and up" aria-label="AI generation requires a paid plan">\u2726</button>';c.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button>'+l+'<button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const d=document.createElement("div");d.className="chr2-field-editor chapter-bardic-editor",d.appendChild(o),d.appendChild(c),n.innerHTML="",n.appendChild(d),o.focus();const u=m=>{n.dataset.editing="",n.classList.remove("editing"),n.innerHTML=m};c.querySelector(".chr2-ed-cancel").addEventListener("click",()=>u(i)),c.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const m=o.value.replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim();await _chr2SaveSessionBardic(t,m)&&(n.dataset.editing="",n.classList.remove("editing"),renderChronicleRecap(t))});const p=c.querySelector(".chr2-ed-gen");r?p.addEventListener("click",async()=>{if(p.disabled)return;p.disabled=!0;const m=p.textContent;p.textContent="\u2026";try{const g=((await api("POST",`/api/campaigns/${a}/sessions/${t}/generate-bardic`,{})).bardic||"").replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim();g?(o.value=g,o.focus()):showToast("Could not generate, try again.",4e3)}catch(f){showToast(`Generation failed: ${f.message||f}`,5e3)}finally{p.textContent=m,setTimeout(()=>{p.disabled=!1},3e3)}}):p.addEventListener("click",()=>{showToast("AI generation is on the Hobby plan and up.",4e3),typeof _showPricingScreen=="function"&&_showPricingScreen()}),o.addEventListener("keydown",m=>{m.key==="Escape"?(m.preventDefault(),u(i)):m.key==="Enter"&&(m.ctrlKey||m.metaKey)&&(m.preventDefault(),c.querySelector(".chr2-ed-save").click())})}async function _chr2SaveSessionBardic(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return!1;try{const a=await api("PUT",`/api/campaigns/${n}/sessions/${e}`,{chronicle_bardic:t}),s=(allSessions||[]).find(i=>i.id===e);return s&&(s.chronicle_bardic=a&&a.chronicle_bardic!==void 0?a.chronicle_bardic:t),!0}catch(a){return showToast(`Save failed: ${a.message||a}`,5e3),!1}}function _chr2OpenBriefEditor(e,t){const n=e.querySelector("#cr-brief"),a=n&&n.querySelector(".chapter-brief-body");if(!n||!a||n.dataset.editing==="1")return;const s=currentCampaign&&currentCampaign.id,i=(allSessions||[]).find(f=>f.id===t);if(!s||!i)return;n.dataset.editing="1",n.classList.add("editing");const o=a.innerHTML,r=(i.session_blurb||"").trim(),c=document.createElement("textarea");c.className="chr2-inline-textarea chapter-brief-input",c.rows=4,c.value=r||_chr2BriefText(i),c.setAttribute("aria-label","Edit the recap TL;DR"),c.placeholder="What players will read\u2026";const l=document.createElement("p");l.className="form-helper-text chapter-brief-hint",l.textContent="Posts as-is in the Discord recap and the 24-hour reminder.";const d=document.createElement("div");d.className="chr2-editor-actions",d.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const u=document.createElement("div");u.className="chr2-field-editor chapter-brief-editor",u.appendChild(c),u.appendChild(l),u.appendChild(d),a.innerHTML="",a.appendChild(u),c.focus();const p=()=>{n.dataset.editing="",n.classList.remove("editing"),a.innerHTML=o},m=async()=>{const f=c.value.replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim();if(!f){showToast("Write the TL;DR, or Cancel to keep it.",4e3),c.focus();return}if(f===r){p();return}if(!await _chr2SaveSessionBlurb(t,f))return;n.dataset.editing="",n.classList.remove("editing"),renderChronicleRecap(t);const h=_chr2Panel()?.querySelector("#cr-brief [data-brief-toggle]");h&&h.getAttribute("aria-expanded")!=="true"&&h.click()};d.querySelector(".chr2-ed-cancel").addEventListener("click",p),d.querySelector(".chr2-ed-save").addEventListener("click",m),c.addEventListener("keydown",f=>{f.stopPropagation(),f.key==="Escape"?(f.preventDefault(),p()):f.key==="Enter"&&(f.ctrlKey||f.metaKey)&&(f.preventDefault(),m())})}async function _chr2SaveSessionBlurb(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return!1;try{const a=await api("PUT",`/api/campaigns/${n}/sessions/${e}
646`,{session_blurb:t}),s=(allSessions||[]).find(i=>i.id===e);return s&&(s.session_blurb=a&&a.session_blurb!==void 0?a.session_blurb:t),!0}catch(a){return showToast(`Save failed: ${a.message||a}`,5e3),!1}}const _CHR2_GRIP_SVG='<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true"><circle cx="5" cy="3" r="1.4"/><circle cx="11" cy="3" r="1.4"/><circle cx="5" cy="8" r="1.4"/><circle cx="11" cy="8" r="1.4"/><circle cx="5" cy="13" r="1.4"/><circle cx="11" cy="13" r="1.4"/></svg>',_CHR2_PENCIL_SVG='<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>';function _chr2ClearNewRecapBadge(e,t){const n=t&&t.querySelector(".toc-new-recap[data-notif-id]");if(!n)return;const a=n.dataset.notifId;n.remove(),t.classList&&t.classList.remove("has-new-recap");const s=typeof _tfFindUnreadNotificationForSession=="function"?_tfFindUnreadNotificationForSession(+e):null;s&&(s.read_at=new Date().toISOString()),api("POST",`/api/notifications/${a}/read`,{}).then(()=>{typeof _tfRefreshNotifications=="function"&&_tfRefreshNotifications()}).catch(()=>{})}function _chr2NewRecapBadge(e){const t=typeof _tfFindUnreadNotificationForSession=="function"?_tfFindUnreadNotificationForSession(e):null;return t?`<span class="session-new-recap-badge toc-new-recap" title="New recap: open to read" data-notif-id="${t.id}">\u25CF New</span>`:""}let _chr2FmData=null;function _chr2FmOpenKeys(e){try{return new Set(JSON.parse(localStorage.getItem("tf_fm_open_"+e)||"[]"))}catch{return new Set}}function _chr2FmSaveOpen(e,t){try{localStorage.setItem("tf_fm_open_"+e,JSON.stringify([...t]))}catch{}}async function _chr2LoadFrontMatter(){if(!document.getElementById("chr2-front-matter")||!currentCampaign)return;const t=currentCampaign.id;let n=null;try{n=await api("GET",`/api/campaigns/${t}/table-rules/front-matter`)}catch{return}if(_campaignStale(t))return;const a=document.getElementById("chr2-front-matter");if(!a)return;const s=(n.house_rules||"").trim(),i=(n.lines_veils||"").trim(),o=(n.rulings||[]).filter(d=>d.status==="kept"),r=(n.rulings||[]).filter(d=>d.status==="proposed");if(!s&&!i&&!o.length&&!r.length){a.remove();return}_chr2FmData={cid:t,blocks:{}};const c=_chr2FmOpenKeys(t),l=[s||o.length||r.length?_chr2FmBlockHtml("hr","House Rules & Rulings",{house_rules:s,lines_veils:"",rulings:o},r,c.has("hr")):"",i?_chr2FmBlockHtml("lv","Lines & Veils",{house_rules:"",lines_veils:i,rulings:[]},[],c.has("lv")):""].join("");a.innerHTML=`<div class="chr2-fm"><div class="chr2-fm-head"><span class="chr2-fm-title">How We Play This Table</span></div>${l}</div>`,_chr2WireFrontMatter(a)}function _chr2FmActs(e){return`<span class="chr2-fm-acts">
647    <button type="button" class="chr2-fm-act chr2-fm-line-edit" ${e} title="Edit" aria-label="Edit">${_CHR2_PENCIL_SVG}</button>
648    <button type="button" class="chr2-fm-act chr2-fm-line-del" ${e} title="Delete" aria-label="Delete">&times;</button>
649  </span>`}function _chr2FmRowHtml(e,t,n){const a=_isGm(),s=e.kind==="ruling"?'<svg class="ln-rule-glyph ln-rule-glyph--ruling" aria-label="Ruling made at the table"><use href="#tf-glyph-quote"/></svg>':_lnRuleGlyph(e.kind),i=(e.subs||[]).length?`<span class="ln-rule-subs">${e.subs.map(c=>`<span class="ln-rule-sub" data-old="${escHtml(c)}">${_lnHi(c,t)}${a?_chr2FmActs(`data-doc="${n}" data-old="${escHtml(c)}"`):""}</span>`).join("")}</span>`:"",o=e.quote?`<span class="chr2-fm-receipt">"${_lnHi(e.quote,t)}"</span>`:"",r=e.kind==="ruling"?`data-rid="${e.rid}" data-old="${escHtml(e.text)}"`:`data-doc="${n}" data-old="${escHtml(e.text)}"`;return`<div class="chr2-fm-row${e.kind==="ruling"?" chr2-fm-row--ruling":""}">${s}<span class="chr2-fm-text"><span class="chr2-fm-main">${_lnHi(e.text,t)}</span>${i}${o}</span>${a?_chr2FmActs(r):""}</div>`}function _chr2FmRowsHtml(e,t,n){const a=(t||"").toLowerCase().split(/\s+/).filter(Boolean),s=a.length?e.filter(i=>{const o=(i.text+" "+(i.subs||[]).join(" ")+" "+(i.quote||"")).toLowerCase();return a.every(r=>o.includes(r))}):e;return s.length?s.map(i=>_chr2FmRowHtml(i,a,n)).join(""):'<p class="chr2-fm-none">Nothing on file matches.</p>'}function _chr2FmBlockHtml(e,t,n,a,s){const i=_tfTableRulesBullets(n),o=a||[];if(!i.length&&!o.length)return"";_chr2FmData&&(_chr2FmData.blocks[e]={bullets:i});const r=o.length?`<div class="chr2-fm-props">
650    ${o.map(p=>`<div class="chr2-fm-prop" data-rid="${p.id}">
651      <span class="chr2-fm-text">${escHtml(p.ruling)}${p.quote?`<span class="chr2-fm-receipt">"${escHtml(p.quote)}"</span>`:""}</span>
652      <span class="chr2-fm-prop-acts">
653        <button type="button" class="btn-new btn-xs chr2-fm-keep" data-rid="${p.id}" title="Write this into the rules">Keep</button>
654        <button type="button" class="btn-ghost btn-xs btn-ghost--danger chr2-fm-discard" data-rid="${p.id}" title="Not a ruling">Discard</button>
655      </span>
656    </div>`).join("")}
657  </div>`:"",c=o.length?`<span class="chr2-fm-confirm">${o.length} to confirm</span>`:"",l=`<input type="text" class="chr2-fm-search" data-fm-search="${e}" placeholder="${e==="lv"?"Find a line or veil\u2026":"Find a rule\u2026"}" autocomplete="off">`,d=e==="lv"?"lines_veils":"house_rules",u=_isGm()?e==="lv"?`<div class="chr2-fm-addrow"><input type="text" class="chr2-fm-add-input" data-fm-add="${e}" placeholder="Add a boundary\u2026" maxlength="400" autocomplete="off"><button type="button" class="btn-new btn-xs chr2-fm-add" data-doc="${d}" data-add="${e}" data-section="Lines">+ Line</button><button type="button" class="btn-new btn-xs chr2-fm-add" data-doc="${d}" data-add="${e}" data-section="Veils">+ Veil</button></div>`:`<div class="chr2-fm-addrow"><input type="text" class="chr2-fm-add-input" data-fm-add="${e}" placeholder="Add a house rule\u2026" maxlength="400" autocomplete="off"><button type="button" class="btn-new btn-xs chr2-fm-add" data-doc="${d}" data-add="${e}">+ Add</button></div>`:"";return`<div class="chr2-fm-sec${s?" open":""}" data-fm-block="${e}" data-fm-doc="${d}">
658    <button type="button" class="chr2-fm-sec-row" aria-expanded="${s?"true":"false"}">
659      <span class="chr2-fm-sec-title">${escHtml(t)}</span>
660      <span class="chr2-fm-count">(${i.length})</span>
661      ${c}
662      <span class="chr2-fm-chev" aria-hidden="true">\u203A</span>
663    </button>
664    <div class="chr2-fm-body${s?"":" hidden"}">${l}${r}<div class="chr2-fm-rows" data-fm-rows="${e}">${_chr2FmRowsHtml(i,"",d)}</div>${u}</div>
665  </div>`}function _chr2WireFrontMatter(e){const t=_chr2FmData&&_chr2FmData.cid;e.querySelectorAll(".chr2-fm-sec-row").forEach(i=>i.addEventListener("click",()=>{const o=i.closest(".chr2-fm-sec"),r=o&&o.querySelector(".chr2-fm-body");if(!r)return;const c=!r.classList.toggle("hidden");if(i.setAttribute("aria-expanded",c?"true":"false"),o.classList.toggle("open",c),!c){const l=o.querySelector(".chr2-fm-search");l&&l.value&&(l.value="",_chr2FmApplySearch(o))}if(t!=null){const l=new Set([...e.querySelectorAll(".chr2-fm-sec.open")].map(d=>d.dataset.fmBlock));_chr2FmSaveOpen(t,l)}})),e.querySelectorAll(".chr2-fm-search").forEach(i=>{i.addEventListener("input",()=>_chr2FmApplySearch(i.closest(".chr2-fm-sec"))),i.addEventListener("keydown",o=>{o.key==="Escape"&&i.value&&(o.stopPropagation(),i.value="",_chr2FmApplySearch(i.closest(".chr2-fm-sec")))})});const n=()=>{typeof _tableRulesLoadedCid<"u"&&(_tableRulesLoadedCid=null),_chr2LoadFrontMatter()},a=async(i,o,r,c)=>{r.disabled=!0;const l=currentCampaign?.id;try{if(await api("POST",`/api/campaigns/${l}/table-rules/rulings/${i}`,c?{action:o,text:c}:{action:o}),_campaignStale(l))return;n()}catch(d){r.disabled=!1,showToast("Failed: "+(d.message||"try again"))}};if(e.querySelectorAll(".chr2-fm-keep").forEach(i=>i.addEventListener("click",o=>{o.stopPropagation(),a(i.dataset.rid,"keep",i)})),e.querySelectorAll(".chr2-fm-discard").forEach(i=>i.addEventListener("click",o=>{o.stopPropagation(),a(i.dataset.rid,"discard",i)})),e._fmLinesWired)return;e._fmLinesWired=!0;const s=async(i,o)=>{const r=currentCampaign?.id;try{if(await api("POST",`/api/campaigns/${r}/table-rules/line`,i),_campaignStale(r))return;n()}catch(c){o&&o(),showToast("Failed: "+(c.message||"try again"))}};e.addEventListener("click",async i=>{const o=i.target.closest(".chr2-fm-line-del"),r=i.target.closest(".chr2-fm-line-edit"),c=i.target.closest(".chr2-fm-add");if(!o&&!r&&!c)return;if(i.stopPropagation(),c){const y=c.closest(".chr2-fm-addrow")?.querySelector(".chr2-fm-add-input"),_=(y?.value||"").trim();if(!_){y?.focus();return}c.disabled=!0,s({doc:c.dataset.doc,new:_,section:c.dataset.section||void 0},()=>{c.disabled=!1});return}const l=o||r,d=l.dataset.old||"";if(o){const y=l.dataset.rid;if(!await confirmDialog("Remove this from the record?"))return;l.disabled=!0,y?a(y,"discard",l):s({doc:l.dataset.doc,old:d,new:null},()=>{l.disabled=!1});return}const u=l.closest(".ln-rule-sub")||l.closest(".chr2-fm-row")?.querySelector(".chr2-fm-main");if(!u||u.querySelector(".chr2-fm-line-input"))return;const p=!!l.closest(".ln-rule-sub"),m=l.dataset.rid,f=l.dataset.doc;u.innerHTML='<input type="text" class="chr2-fm-line-input" maxlength="400">';const g=u.querySelector(".chr2-fm-line-input");
665g.value=d,g.focus(),g.select();const h=()=>_chr2FmApplySearch(l.closest(".chr2-fm-sec")||e.querySelector(`.chr2-fm-sec[data-fm-doc="${f}"]`));let v=!1;const b=()=>{if(v)return;v=!0;const y=g.value.trim();if(!y||y===d){h();return}m?a(m,"edit",g,y):s({doc:f,old:d,new:y,sub:p},h)};g.addEventListener("keydown",y=>{y.key==="Enter"?(y.preventDefault(),b()):y.key==="Escape"&&(y.stopPropagation(),v=!0,h())}),g.addEventListener("blur",b)})}function _chr2FmApplySearch(e){if(!e||!_chr2FmData)return;const t=e.dataset.fmBlock,n=e.querySelector(".chr2-fm-rows"),a=e.querySelector(".chr2-fm-search"),s=_chr2FmData.blocks[t];!n||!s||(n.innerHTML=_chr2FmRowsHtml(s.bullets,a?a.value.trim():"",e.dataset.fmDoc||"house_rules"))}function _chr2RenderActsToc(e){const t=e.querySelector("#chron-toc-acts");if(!t)return;const n=_isGm(),a=n?`<span class="sec-grip sec-grip--spacer" aria-hidden="true">${_CHR2_GRIP_SVG}</span><span class="sec-edit sec-edit--spacer" aria-hidden="true">${_CHR2_PENCIL_SVG}</span>`:"",s=(allSessions||[]).filter(y=>!y.is_session_zero).map(y=>({s:y,num:_chr2SessionNum(y)})).sort((y,_)=>y.num-_.num),i=_chr2Zeros(),o=s.length;if(o===0&&!i.length){t.innerHTML='<p class="placeholder--padded">No sessions recorded yet.</p>';return}const r=o?s[o-1].s.id:null,c=_chr2BuildSections(s,currentCampaign&&currentCampaign.chronicle_acts),l=c[0]||null,d={};for(const y of c)y!==l&&(d[y.breakBefore]=y);const u=y=>escHtml(y.name)||(n?'<span class="sec-name-ph">Name this act\u2026</span>':""),p=y=>y.kind==="prologue"?" is-prologue":y.kind==="epilogue"?" is-epilogue":"",m=(y,_)=>{const w=($,E)=>`<button class="sec-kind${y.kind===$?" on":""}" data-sec-kind="${y.act}:${$}" title="Mark as ${E}">${E}</button>`;return`<span class="sec-kinds">${w("prologue","Prologue")}${w("act","Act")}${w("epilogue","Epilogue")}${_?`<button class="sec-remove" data-sec-remove="${y.act}" title="Remove this act break" aria-label="Remove act break">&times;</button>`:""}</span>`},f=({s:y,num:_})=>{const w=_chr2Roman(_),$=_chr2DateShort(y),E=y.id===r,k=escHtml(_chr2OneLine(y)||""),C=n?`<span class="toc-edit toc-snippet-edit" role="button" tabindex="0" data-snippet-edit="${y.id}" title="Edit teaser" aria-label="Edit teaser">${_CHR2_PENCIL_SVG}</span>`:"";return`<button class="toc-row${E?" is-current":""}" data-open="${y.id}" data-n="${_}">
666      <span class="toc-num">${w}</span>
667      <span class="toc-main">
668        <span class="toc-title">${escHtml(y.title||"Untitled session")}</span>
669        ${n?`<span class="toc-edit" role="button" tabindex="0" data-toc-edit="${y.id}" title="Rename session" aria-label="Rename session">${_CHR2_PENCIL_SVG}</span>`:""}
670        ${_chr2NewRecapBadge(y.id)}
671        ${E?'<span class="toc-flag">Latest</span>':""}
672        <span class="toc-lead"></span>
673        <span class="toc-session">${escHtml($||"No date")}</span>
674      </span>
675      ${k?`<span class="toc-teaser">${k}${C}</span>`:""}
676    </button>`};let g="";const h=!!(l&&l.kind==="prologue");i.length&&!h&&(g+=`<div class="act-boundary-bar act-top is-prologue">${a}<span class="sec-name">Prologue</span></div>`,g+='<div id="chr2-front-matter"></div>',g+=i.map(f).join("")),n&&(!l||l.synthetic)&&(g+='<div class="acts-hint">Hover between chapters and click <b>\xF7 New act here</b> to mark where an act begins.</div>'),l&&(n?g+=`<div class="act-boundary-bar act-top${p(l)}" data-sec="${l.act}">
677        <span class="sec-grip sec-grip--spacer" aria-hidden="true">${_CHR2_GRIP_SVG}</span>
678        <button class="sec-edit" data-sec-edit="${l.act}" title="Rename act" aria-label="Rename act">${_CHR2_PENCIL_SVG}</button>
679        <span class="sec-name">${u(l)}</span>
680        <span class="ro">${escHtml(_chr2SecLabel(c,l))}</span>
681        ${m(l,!1)}
682      </div>`:g+=`<div class="act-boundary-bar act-top${p(l)}"><span class="sec-name">${u(l)}</span><span class="ro">${escHtml(_chr2SecLabel(c,l))}</span></div>`),i.length&&h&&(g+='<div id="chr2-front-matter"></div>',g+=i.map(f).join(""));for(let y=0;y<o;y++){const _=s[y];if(y>0){const w=d[_.num];w?n?g+=`<div class="act-boundary-bar${p(w)}" data-sec="${w.act}" data-break-before="${_.num}">
683            <span class="sec-grip" draggable="true" data-sec="${w.act}" title="Drag to move where this act begins" aria-label="Drag act break">${_CHR2_GRIP_SVG}</span>
684            <button class="sec-edit" data-sec-edit="${w.act}" title="Rename act" aria-label="Rename act">${_CHR2_PENCIL_SVG}</button>
685            <span class="sec-name">${u(w)}</span>
686            <span class="ro">${escHtml(_chr2SecLabel(c,w))}</span>
687            <button class="sec-move" data-sec-move="${w.act}" title="Move this act break" aria-label="Move act break">\u2195</button>
688            ${m(w,!0)}
689          </div>`:g+=`<div class="act-boundary-bar${p(w)}"><span class="sec-name">${u(w)}</span><span class="ro">${escHtml(_chr2SecLabel(c,w))}</span></div>`:n&&(g+=`<div class="act-insert" data-gap="${_.num}"><button class="act-insert-btn" data-gap="${_.num}" title="Start a new act at this chapter">\xF7 New act here</button></div>`)}g+=f(_)}if(t.innerHTML=g,_chr2LoadFrontMatter(),t.querySelectorAll(".toc-row[data-open]").forEach(y=>{y.addEventListener("mousedown",()=>{y._tfEditAtPress=y.dataset.editing==="1"},!0),y.addEventListener("click",_=>{const w=y._tfEditAtPress;y._tfEditAtPress=!1,!(w||y.dataset.editing==="1"||_.target.closest&&_.target.closest(".toc-edit, input"))&&(_chr2ClearNewRecapBadge(+y.dataset.open,y),renderChronicleRecap(+y.dataset.open))})}),t.querySelectorAll("[data-toc-edit]").forEach(y=>{const _=w=>{w.stopPropagation(),w.preventDefault();const $=y.closest(".toc-row");$&&_chr2OpenTocNameEditor(e,$,+y.dataset.tocEdit)};y.addEventListener("click",_),y.addEventListener("keydown",w=>{(w.key==="Enter"||w.key===" ")&&_(w)})}),t.querySelectorAll("[data-snippet-edit]").forEach(y=>{const _=w=>{w.stopPropagation(),w.preventDefault();const $=y.closest(".toc-row");$&&_chr2OpenTocSnippetEditor(e,$,+y.dataset.snippetEdit)};y.addEventListener("click",_),y.addEventListener("keydown",w=>{(w.key==="Enter"||w.key===" ")&&_(w)})}),!n)return;t.querySelectorAll("[data-sec-kind]").forEach(y=>{y.addEventListener("click",_=>{_.stopPropagation();const[w,$]=y.dataset.secKind.split(":"),E=c.find(k=>k.act===+w);E&&E.kind!==$&&(E.kind=$,_chr2SaveActs(e,c))})}),t.querySelectorAll("[data-sec-remove]").forEach(y=>{y.addEventListener("click",_=>{_.stopPropagation();const w=+y.dataset.secRemove;_chr2SaveActs(e,c.filter($=>$.act!==w)),showToast("Act break removed",2500)})}),t.querySelectorAll("[data-sec-edit]").forEach(y=>{y.addEventListener("click",_=>{_.stopPropagation(),_chr2StartRename(e,+y.dataset.secEdit,y.closest(".act-boundary-bar"),c)})}),t.querySelectorAll(".act-insert-btn[data-gap]").forEach(y=>{y.addEventListener("click",_=>{_.stopPropagation(),_chr2InsertActAt(e,+y.dataset.gap,c)})}),t.querySelectorAll("[data-sec-move]").forEach(y=>{y.addEventListener("click",_=>{_.stopPropagation(),_chr2OpenMoveMenu(e,+y.dataset.secMove,y,c,o)})});let v=null;const b=()=>t.querySelectorAll(".drop-target").forEach(y=>y.classList.remove("drop-target"));t.querySelectorAll(".act-boundary-bar .sec-grip").forEach(y=>{y.addEventListener("dragstart",_=>{v=+y.dataset.sec,_.dataTransfer.effectAllowed="move";try{_.dataTransfer.setData("text/plain",y.dataset.sec)}catch{}t.classList.add("dragging-boundary");const w=y.closest(".act-boundary-bar");w&&w.classList.add("dragging")}),y.addEventListener("dragend",()=>{v=null,b(),t.classList.remove("dragging-boundary"),t.querySelectorAll(".dragging").forEach(_=>_.classList.remove("dragging"))})}),t.querySelectorAll(".act-insert[data-gap]").forEach(y=>{y.addEventListener("dragover",_=>{v!=null&&(_.preventDefault(),b(),y.classList.add("drop-target"))}),y.addEventListener("dragleave",()=>y.classList.remove("drop-target")),y.addEventListener("drop",_=>{if(v==null)return;_.preventDefault(),b();const w=c.find(E=>E.act===v),$=+y.dataset.gap;v=null,w&&w.breakBefore!==$&&(w.breakBefore=$,_chr2SaveActs(e,c))})})}function _chr2InsertActAt(e,t,n){const a=n.slice();a.length||a.push({act:1,kind:"act",name:"",breakBefore:1});const s=_chr2NewActId(a);a.push({act:s,kind:"act",name:"",breakBefore:t}),_chr2SaveActs(e,a,{focusRename:s})}function _chr2OpenMoveMenu(e,t,n,a,s){e.querySelectorAll(".chr2-move-menu").forEach(l=>l.remove());const i=a.find(l=>l.act===t);if(!i)return;const o=_chr2ValidGaps(a,s,t),r=document.createElement("div");
689r.className="chr2-move-menu",r.setAttribute("role","menu"),r.innerHTML=o.length?o.map(l=>`<button class="chr2-move-item${l===i.breakBefore?" on":""}" data-gap="${l}" role="menuitem">Begin at Chapter ${_chr2Roman(l)}${l===i.breakBefore?" \xB7 current":""}</button>`).join(""):'<div class="chr2-move-empty">No other seam to move this break to.</div>',n.parentElement.style.position="relative",n.parentElement.appendChild(r),r.querySelectorAll("[data-gap]").forEach(l=>{l.addEventListener("click",d=>{d.stopPropagation();const u=+l.dataset.gap;r.remove(),u!==i.breakBefore&&(i.breakBefore=u,_chr2SaveActs(e,a))})});const c=l=>{!r.contains(l.target)&&l.target!==n&&(r.remove(),document.removeEventListener("click",c,!0))};setTimeout(()=>document.addEventListener("click",c,!0),0)}async function _chr2SaveActs(e,t,n){const a=currentCampaign&&currentCampaign.id;if(!a)return;const s=t.slice().sort((r,c)=>r.breakBefore-c.breakBefore||r.act-c.act);s.length&&(s[0]={...s[0],breakBefore:1});const i=s.map((r,c)=>({act:r.act,name:r.name||"",kind:r.kind||"act",break_before:c===0?null:r.breakBefore})),o=(allSessions||[]).filter(r=>!r.is_session_zero).map(r=>({s:r,num:_chr2SessionNum(r)})).sort((r,c)=>r.num-c.num);if(currentCampaign={...currentCampaign,chronicle_acts:i},_chr2RenderActsToc(e),n&&n.focusRename!=null){const r=e.querySelector(`[data-sec-edit="${n.focusRename}"]`);r&&_chr2StartRename(e,n.focusRename,r.closest(".act-boundary-bar"),_chr2BuildSections(o,i))}try{await api("POST",`/api/campaigns/${a}/chronicle/acts`,{acts:i})}catch(r){showToast(`Saving acts failed: ${r.message||r}`,5e3)}}function _chr2StartRename(e,t,n,a){const s=a.find(l=>l.act===t);if(!s||!n||n.querySelector(".sec-name-input"))return;const i=n.querySelector(".sec-name");if(!i)return;const o=document.createElement("input");o.className="sec-name-input",o.value=s.name||"",o.placeholder="Name this act\u2026",o.setAttribute("aria-label","Rename act"),i.replaceWith(o),o.focus(),o.select();let r=!1;const c=()=>{if(r)return;r=!0;const l=o.value.trim();l!==(s.name||"")?(s.name=l,_chr2SaveActs(e,a)):_chr2RenderActsToc(e)};o.addEventListener("keydown",l=>{l.key==="Enter"?(l.preventDefault(),c()):l.key==="Escape"&&(r=!0,_chr2RenderActsToc(e))}),o.addEventListener("blur",c)}let _qotnModalState=null,_qotnModalWired=!1;const _QOTN_NPC_KEY="npc",_QOTN_POLL_ANSWER_MAX=55,_QOTN_SVG_TROPHY='<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 4h8v5a4 4 0 0 1-8 0z"/><path d="M8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4"/><path d="M12 13v4M9 20h6"/></svg>',_QOTN_SVG_PREV='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>',_QOTN_SVG_NEXT='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>',_QOTN_SVG_REMOVE='<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';function _qotnPaintTrigger(e,t,n){if(e.classList.remove("hidden"),e.disabled=!1,t.already_sent){const a=!!t.poll_status&&t.poll_status!=="open";
689e.innerHTML=`${_QOTN_SVG_TROPHY}<span>${a?"Vote closed":"Poll sent"}</span>`,e.title=a?"This vote has closed":"Results post automatically when the vote closes",e.disabled=!!n}else e.innerHTML=`${_QOTN_SVG_TROPHY}<span>Quote of the Night</span>`,e.title=t.discord_configured?"Pick the lines and send your table a vote":"Pick the lines and copy a vote to paste in Discord, no bot needed"}async function _qotnDecorateCard(e){if(!e||!_isGm())return;const t=e.querySelector(".ft-qotn-cta");if(!t)return;const n=currentCampaign?.id,a=+t.dataset.qotnSession;if(!n||!a)return;const s=(allSessions||[]).find(r=>r.id===a);if(s&&s.qotn)return;let i;try{i=await api("GET",`/api/campaigns/${n}/quote-night/candidates?session_id=${a}`)}catch{return}if(_campaignStale(n))return;const o=document.querySelector(`#tab-chronicle2 .ft-qotn-cta[data-qotn-session="${a}"]`);if(o){if(!i||!i.available){o.classList.add("hidden");return}_qotnPaintTrigger(o,i,!0)}}async function _qotnDecorateDrawerBtn(e,t){const n=currentCampaign?.id;if(!e||!n||!t)return;let a;try{a=await api("GET",`/api/campaigns/${n}/quote-night/candidates?session_id=${t}`)}catch{return}_campaignStale(n)||!e.isConnected||!a||_qotnPaintTrigger(e,a,!1)}function _qotnSpeakerOptions(e){const t=(Array.isArray(allEntities)?allEntities:[]).filter(s=>s&&s.name&&["pc","npc","companion","deity"].includes(s.type)).slice().sort((s,i)=>s.name.localeCompare(i.name));let a=`<option value="party"${e==null?" selected":""}>The Party</option>`;for(const s of t){const i=String(s.id)===String(e)?" selected":"";a+=`<option value="${s.id}"${i}>${escHtml(s.name)}</option>`}return a}function _qotnEntity(e){return e==null?null:(allEntities||[]).find(t=>String(t.id)===String(e))||null}function _qotnKeyFor(e){const t=_qotnEntity(e.speaker_entity_id);if(t&&t.type==="pc")return"pc:"+t.id;if(t&&t.type==="companion"&&t.parent_entity_id!=null){const n=_qotnEntity(t.parent_entity_id);if(n&&n.type==="pc")return"pc:"+n.id}return _QOTN_NPC_KEY}function _qotnAnswerLen(e){return((e.speaker_name||"").trim()||"The party").length+2+(e.quote||"").trim().length}function _qotnRank(e,t){const n=a=>_qotnAnswerLen(a)<=_QOTN_POLL_ANSWER_MAX?1:0;return!!t.is_pinned-!!e.is_pinned||!!t.is_manual-!!e.is_manual||(t.score??5)-(e.score??5)||n(t)-n(e)||e.id-t.id}function _qotnBuildGroups(e){const t=new Map;for(const a of e){const s=_qotnKeyFor(a);t.has(s)||t.set(s,[]),t.get(s).push(a)}const n=new Map;for(const[a,s]of t)n.set(a,s.sort(_qotnRank).map(i=>i.id));return n}function _qotnSeededShuffle(e,t){let n=Number(t)>>>0||1;const a=()=>{n=n+1831565813>>>0;let i=n;return i=Math.imul(i^i>>>15,i|1),i^=i+Math.imul(i^i>>>7,i|61),((i^i>>>14)>>>0)/4294967296},s=e.slice();for(let i=s.length-1;i>0;i--){const o=Math.floor(a()*(i+1));[s[i],s[o]]=[s[o],s[i]]}return s}function _qotnDefaultRows(e){const t=new Map(e.pool.map(c=>[c.id,c])),n=c=>t.get((e.groups.get(c)||[])[0]),a=[...e.groups.keys()].filter(c=>c!==_QOTN_NPC_KEY).sort((c,l)=>Number(c.slice(3))-Number(l.slice(3)));let s=_qotnSeededShuffle(a,e.sessionId);const i=(e.groups.get(_QOTN_NPC_KEY)||[]).length>0&&a.length<e.minCandidates,o=e.maxCandidates-(i?1:0);if(s.length>o){const c=new Set(a.slice().sort((l,d)=>_qotnRank(n(l),n(d))).slice(0,o));s=s.filter(l=>c.has(l))}const r=s.map(c=>({key:c,qid:e.groups.get(c)[0]}));i&&r.push({key:_QOTN_NPC_KEY,qid:e.groups.get(_QOTN_NPC_KEY)[0]});for(const c of e.groups.keys())for(;r.length<e.minCandidates;){const l=new Set(r.map(u=>u.qid)),d=(e.groups.get(c)||[]).find(u=>!l.has(u));if(d==null)break;r.push({key:c,qid:d})}return r}function _qotnRowChoices(e,t){const n=e.rows[t],a=new Set(e.rows.filter((s,i)=>i!==t).map(s=>s.qid));return(e.groups.get(n.key)||[]).filter(s=>!a.has(s))}function _qotnFirstFree(e,t){const n=new Set(e.rows.map(s=>s.qid)),a=(e.groups.get(t)||[]).find(s=>!n.has(s));return a??null}function _qotnRowSpeaker(e){if(e===_QOTN_NPC_KEY)return{name:"NPCs",type:"npc",ent:null};const t=_qotnEntity(e.slice(3));return{name:t?t.name:"Unknown",type:"pc",ent:t}}function _qotnPortrait(e){const t=ENTITY_CFG[e.type]||ENTITY_CFG.npc,n=e.ent?_entityImageUrl(e.ent):null,a=n?`<img src="${escHtml(n)}" alt="" loading="lazy">`:entityGlyphRef(e.type,"qotn-portrait-glyph");return`<span class="qotn-portrait" style="--entity-color:${t.color}" aria-hidden="true">${a}</span>`}function _qotnDurLabel(e){return{1:"one hour",24:"24 hours",48:"two days",72:"three days"}[e]||e+" hours"}function _qotnRowHtml(e,t,n,a,s){const i=a.get(t.qid)||{id:t.qid,quote:"",context:""}
689,o=_qotnRowSpeaker(t.key),r=escHtml(o.name),c=_qotnRowChoices(e,n),l=Math.max(0,c.indexOf(t.qid)),d=c.length,u=e.editing===n,p=(i.speaker_name||"").trim()||"The party",m=t.key===_QOTN_NPC_KEY||p.toLowerCase()!==o.name.toLowerCase(),f=d>1?`<div class="qotn-cycle" role="group" data-slot="${n}" aria-label="Lines from ${r}">
690        <button type="button" class="qotn-cycle-btn" data-qotn-prev="${n}" data-qotn-focus="prev-${n}" aria-label="Previous line from ${r}"${l<=0?" disabled":""}>${_QOTN_SVG_PREV}</button>
691        <span class="qotn-cycle-pos">${l+1} of ${d}</span>
692        <button type="button" class="qotn-cycle-btn" data-qotn-next="${n}" data-qotn-focus="next-${n}" aria-label="Next line from ${r}"${l>=d-1?" disabled":""}>${_QOTN_SVG_NEXT}</button>
693      </div>`:'<span class="qotn-only">Only line</span>',g=d>1&&l===0?'<span class="tf-badge tf-badge--gold qotn-top-pick">Top pick</span>':"",h=e.discordConfigured&&_qotnAnswerLen(i)>_QOTN_POLL_ANSWER_MAX,v=u?`<div class="qotn-edit">
694        <label class="qotn-field-label" for="qotn-q-${n}">Quote</label>
695        <textarea id="qotn-q-${n}" class="qotn-quote" data-slot="${n}" data-qotn-focus="quote-${n}" rows="3">${escHtml(i.quote||"")}</textarea>
696        <label class="qotn-field-label" for="qotn-c-${n}">Context</label>
697        <input id="qotn-c-${n}" type="text" class="qotn-context" data-slot="${n}" data-qotn-focus="context-${n}" value="${escHtml(i.context||"")}" placeholder="When or where it was said">
698        <label class="qotn-field-label" for="qotn-s-${n}">Said by</label>
699        <select id="qotn-s-${n}" class="qotn-speaker" data-slot="${n}" data-qotn-focus="speaker-${n}">${_qotnSpeakerOptions(i.speaker_entity_id)}</select>
700      </div>`:`<p class="qotn-row-quote">\u201C${escHtml(i.quote||"")}\u201D</p>
701      ${i.context?`<p class="qotn-row-ctx">${escHtml(i.context)}</p>`:""}
702      ${h?'<p class="qotn-row-note">The poll button shortens this line. The full quote is in the post above it.</p>':""}`;return`<li class="qotn-row${u?" is-editing":""}" data-slot="${n}">
703    <span class="qotn-row-num" aria-hidden="true">${n+1}</span>
704    ${_qotnPortrait(o)}
705    <div class="qotn-row-main">
706      <div class="qotn-row-head">
707        <span class="qotn-row-name">${r}</span>
708        ${m?`<span class="qotn-row-said">${escHtml(p)}</span>`:""}
709        ${g}
710        ${u?"":f}
711      </div>
712      ${v}
713    </div>
714    <div class="qotn-row-actions">
715      <button type="button" class="btn-ghost btn-xs qotn-edit-btn" data-qotn-edit="${n}" data-qotn-focus="edit-${n}" aria-expanded="${u?"true":"false"}" aria-label="${u?"Done editing":"Edit"} ${r}\u2019s line">${u?"Done":"Edit"}</button>
716      <button type="button" class="qotn-remove" data-qotn-remove="${n}" data-qotn-focus="remove-${n}" aria-label="Remove ${r} from the ballot"${s?"":` disabled title="A vote needs at least ${e.minCandidates} lines"`}>${_QOTN_SVG_REMOVE}</button>
717      <span class="qotn-saved hidden" data-qid="${i.id}">Saved</span>
718    </div>
719  </li>`}function _qotnAddChipsHtml(e){if(e.rows.length>=e.maxCandidates)return`<p class="qotn-add-full">The ballot is full at ${e.maxCandidates} lines, the most a Discord poll holds.</p>`;const t=[...e.groups.keys()].filter(n=>_qotnFirstFree(e,n)!=null).map(n=>({k:n,name:_qotnRowSpeaker(n).name})).sort((n,a)=>(n.k===_QOTN_NPC_KEY)-(a.k===_QOTN_NPC_KEY)||n.name.localeCompare(a.name));return t.length?`<span class="qotn-add-label" id="qotn-add-label">Add a row</span>
720    <div class="qotn-add-chips" role="group" aria-labelledby="qotn-add-label">${t.map(n=>`<button type="button" class="btn-new btn-sm qotn-add-chip" data-qotn-add="${escHtml(n.k)}" data-qotn-focus="add-${escHtml(n.k)}" aria-label="Add a row for ${escHtml(n.name)}">+ ${escHtml(n.name)}</button>`).join("")}</div>`:""}function _qotnRestoreFocus(e,t){const n=s=>!!s&&!s.disabled&&s.offsetParent!==null;let a=e.querySelector(`[data-qotn-focus="${t}"]`);if(!n(a)){const s=/^([a-z]+)-(\d+)$/.exec(t),i=[];if(s){const o=s[1],r=+s[2];o==="prev"&&i.push(`next-${r}`),o==="next"&&i.push(`prev-${r}`),i.push(`${o}-${r-1}`,`edit-${r}`,`edit-${r-1}`)}a=null;for(const o of i){const r=e.querySelector(`[data-qotn-focus="${o}"]`);if(n(r)){a=r;break}}}n(a)||(a=[...e.querySelectorAll("[data-qotn-focus]")].find(n)||null),a&&a.focus()}
720function _qotnRenderModal(){const e=_qotnModalState,t=document.getElementById("qotn-modal");if(!e||!t)return;const n=document.getElementById("qotn-lines"),a=document.getElementById("qotn-add"),s=t.querySelector(".qotn-options"),i=document.getElementById("qotn-npc-toggle"),o=document.getElementById("qotn-intro"),r=t.querySelector("[data-qotn-send]"),c=t.querySelector("[data-qotn-copy]"),l=document.getElementById("qotn-foot-helper"),d=document.getElementById("qotn-duration-row");if(!n)return;const u=document.activeElement,p=e.focusNext||(u&&t.contains(u)&&u.dataset?u.dataset.qotnFocus:null);e.focusNext=null,d&&(d.classList.toggle("hidden",e.alreadySent||!e.discordConfigured),d.querySelectorAll(".qotn-dur-btn").forEach(h=>{const v=+h.dataset.qotnDur===e.durationHours;h.classList.toggle("is-active",v),h.setAttribute("aria-pressed",v?"true":"false")}));const m=new Map(e.pool.map(h=>[h.id,h]));if(e.alreadySent){s?.classList.add("hidden"),o?.classList.add("hidden"),a&&(a.innerHTML="");const h=(e.sentIds||[]).map(b=>m.get(b)).filter(Boolean),v=(e.sentIds||[]).length-h.length;n.innerHTML=h.map((b,y)=>`
721      <li class="qotn-row qotn-row--ro">
722        <span class="qotn-row-num" aria-hidden="true">${y+1}</span>
723        ${_qotnPortrait(_qotnRowSpeaker(_qotnKeyFor(b)))}
724        <div class="qotn-row-main">
725          <div class="qotn-row-head"><span class="qotn-row-name">${escHtml((b.speaker_name||"").trim()||"The party")}</span></div>
726          <p class="qotn-row-quote">\u201C${escHtml(b.quote||"")}\u201D</p>
727          ${b.context?`<p class="qotn-row-ctx">${escHtml(b.context)}</p>`:""}
728        </div>
729      </li>`).join("")+(v?`<li class="qotn-empty">${h.length?`${v} of the lines from this vote were`:"The lines from this vote were"} replaced when quotes were pulled again, so ${h.length?"they":"the ballot"} can\u2019t be shown here.</li>`:""),r?.classList.add("hidden"),c?.classList.add("hidden"),l&&(l.textContent=e.pollStatus&&e.pollStatus!=="open"?e.pollHasWinner?"This vote has closed. The winner is crowned on the chapter.":"This vote has closed without a winner.":"This poll has been sent. Results post automatically when the vote closes.");return}const f=e.pool.length>=e.minCandidates;if(s?.classList.toggle("hidden",!f),o?.classList.toggle("hidden",!f),!f)n.innerHTML='<li class="qotn-empty">This session doesn\u2019t have enough quotes for a vote yet. Refresh quotes to take another pass through the transcript.</li>',a&&(a.innerHTML=""),s&&s.classList.remove("hidden"),s?.querySelector(".qotn-npc-toggle")?.classList.add("hidden");else{s?.querySelector(".qotn-npc-toggle")?.classList.remove("hidden");const h=(e.groups.get(_QOTN_NPC_KEY)||[]).length;if(i){i.checked=e.rows.some(y=>y.key===_QOTN_NPC_KEY),i.disabled=!h;const b=i.closest(".qotn-npc-toggle");b&&(b.classList.toggle("is-disabled",!h),b.title=h?"":"No NPC lines in this session")}const v=e.rows.length>e.minCandidates;n.innerHTML=e.rows.map((b,y)=>_qotnRowHtml(e,b,y,m,v)).join(""),a&&(a.innerHTML=_qotnAddChipsHtml(e))}const g=f&&e.rows.length>=e.minCandidates;r&&(r.classList.remove("hidden"),r.disabled=!g||!e.discordConfigured||e.sending,r.textContent=e.sending?"Sending\u2026":"Send Poll to Discord"),c&&(c.classList.remove("hidden"),c.disabled=!g||e.sending),l&&(l.textContent=e.discordConfigured?`Your players get ${_qotnDurLabel(e.durationHours)} to vote. The winning line becomes tonight\u2019s card.`:"No Discord bot connected. \u201CCopy vote\u201D gives you a paste-ready ballot your table reacts to."),p&&_qotnRestoreFocus(t,p)}function _qotnAnnounce(e){const t=document.getElementById("qotn-live");t&&(t.textContent="",setTimeout(()=>{t.textContent=e},40))}function _qotnFlashSaved(e){const t=document.querySelector(`#qotn-lines .qotn-saved[data-qid="${e}"]`);t&&(t.classList.remove("hidden"),clearTimeout(t._t),t._t=setTimeout(()=>t.classList.add("hidden"),1500))}async function _qotnSaveField(e,t,n){const a=_qotnModalState;if(!a||!a.rows[e])return;const s=a.rows[e].qid,i=a.pool.find(c=>c.id===s);if(!i)return;const o={speaker_entity_id:i.speaker_entity_id,speaker_name:i.speaker_name,quote:i.quote,context:i.context,is_manual:i.is_manual},r=()=>{a.groups=_qotnBuildGroups(a.pool);const c=a.rows.find(l=>l.qid===s);c&&(c.key=_qotnKeyFor(i))};try{if(t==="speaker"){const c=n==="party"?null:+n,l=c?_qotnEntity(c):null;i.speaker_entity_id=c,i.speaker_name=l?l.name:"The Party",i.is_manual=!0,r(),_qotnRenderModal(),await api("PUT",`/api/campaigns/${a.campaignId}/quotes/${s}`,{new_entity_id:c,speaker_name:i.speaker_name})}else{i[t]=n,i.is_manual=!0;const c={};c[t]=n,await api("PATCH",`/api/campaigns/${a.campaignId}/quotes/${s}`,c)}if(_qotnModalState!==a)return;_qotnFlashSaved(s),_qotnAnnounce("Saved")}catch(c){Object.assign(i,o),_qotnModalState===a&&(r(),_qotnRenderModal()),showToast(c.message||"Could not save that edit.",4e3)}}function _qotnCycle(e,t){const n=_qotnModalState;if(!n||!n.rows[e])return;
729const a=_qotnRowChoices(n,e),s=a.indexOf(n.rows[e].qid),i=a[s+t];i!=null&&(n.rows[e].qid=i,_qotnRenderModal(),_qotnAnnounce(`${_qotnRowSpeaker(n.rows[e].key).name}, line ${s+t+1} of ${a.length}`))}function _qotnToggleEdit(e){const t=_qotnModalState;if(!t||!t.rows[e])return;const n=t.editing!==e;t.editing=n?e:null,t.focusNext=n?`quote-${e}`:`edit-${e}`,_qotnRenderModal()}function _qotnRemoveRow(e){const t=_qotnModalState;if(!t||t.rows.length<=t.minCandidates)return;const[n]=t.rows.splice(e,1);t.editing===e?t.editing=null:t.editing!=null&&t.editing>e&&(t.editing-=1),_qotnRenderModal(),_qotnAnnounce(`Removed ${_qotnRowSpeaker(n.key).name}. ${t.rows.length} lines on the ballot.`)}function _qotnAddRow(e){const t=_qotnModalState;if(!t||t.rows.length>=t.maxCandidates)return;const n=_qotnFirstFree(t,e);if(n==null)return;t.rows.push({key:e,qid:n});const a=t.rows.length-1;t.focusNext=`edit-${a}`,_qotnRenderModal(),document.querySelector(`#qotn-lines .qotn-row[data-slot="${a}"]`)?.scrollIntoView({block:"nearest"}),_qotnAnnounce(`Added ${_qotnRowSpeaker(e).name}. ${t.rows.length} lines on the ballot.`)}function _qotnSetNpcRow(e){const t=_qotnModalState;if(!t)return;const n=t.rows.some(a=>a.key===_QOTN_NPC_KEY);if(e&&!n){if(t.rows.length>=t.maxCandidates){showToast(`The ballot is full at ${t.maxCandidates} lines. Remove a row first.`,3500),_qotnRenderModal();return}const a=_qotnFirstFree(t,_QOTN_NPC_KEY);a!=null&&t.rows.push({key:_QOTN_NPC_KEY,qid:a})}else if(!e&&n){const a=t.rows.filter(s=>s.key!==_QOTN_NPC_KEY);if(a.length<t.minCandidates){showToast(`A vote needs at least ${t.minCandidates} lines.`,3e3),_qotnRenderModal();return}t.rows=a,t.editing=null}_qotnRenderModal(),_qotnAnnounce(`${t.rows.length} lines on the ballot.`)}function _qotnMarkSent(e){document.querySelectorAll(`.ft-qotn-cta[data-qotn-session="${e}"], .qotn-drawer-btn[data-qotn-open="${e}"]`).forEach(t=>_qotnPaintTrigger(t,{already_sent:!0},t.classList.contains("ft-qotn-cta")))}async function _qotnSend(){const e=_qotnModalState;if(!e||e.sending)return;const t=e.rows.map(n=>n.qid);if(t.length<e.minCandidates){showToast(`Pick at least ${e.minCandidates} lines.`,3e3);return}e.sending=!0,_qotnRenderModal();try{await api("POST",`/api/campaigns/${e.campaignId}/quote-night/send`,{session_id:e.sessionId,quote_ids:t,duration_hours:e.durationHours}),showToast(`Poll is live. Your table has ${_qotnDurLabel(e.durationHours)}.`,4e3),_qotnCloseModal(),_qotnMarkSent(e.sessionId)}catch(n){e.sending=!1,_qotnRenderModal(),showToast(n.message||"Could not send the poll.",5e3)}}async function _qotnCopyVote(){const e=_qotnModalState;if(!e)return;const t=e.rows.map(n=>n.qid);if(t.length<e.minCandidates){showToast(`Pick at least ${e.minCandidates} lines.`,3e3);return}try{const n=await api("POST",`/api/campaigns/${e.campaignId}/quote-night/copy-text`,{session_id:e.sessionId,quote_ids:t});_tfCopyText(n.text||"","Copied. Paste into Discord, then add number reactions (1\uFE0F\u20E3 2\uFE0F\u20E3 \u2026) for your table to vote.")}catch(n){showToast(n.message||"Could not copy the vote.",5e3)}}function _qotnCloseModal(){const e=document.getElementById("qotn-modal");e&&e.classList.add("hidden"),_qotnModalState=null}async function _qotnRefresh(){const e=_qotnModalState;if(!e||e.refreshing)return;const t=document.querySelector("#qotn-modal [data-qotn-refresh]");e.refreshing=!0,t&&(t.disabled=!0,t.textContent="Refreshing quotes\u2026");try{await api("POST",`/api/campaigns/${e.campaignId}/sessions/${e.sessionId}/extract-quotes`,{});const n=await api("GET",`/api/campaigns/${e.campaignId}/quote-night/candidates?session_id=${e.sessionId}`);if(_campaignStale(e.campaignId)||_qotnModalState!==e)return;e.pool=n.quotes||[],e.groups=_qotnBuildGroups(e.pool);const a=[];for(const s of e.rows){const i=e.groups.get(s.key)||[],o=new Set(a.map(c=>c.qid)),r=i.includes(s.qid)&&!o.has(s.qid)?s.qid:i.find(c=>!o.has(c));r!=null&&a.push({key:s.key,qid:r})}e.rows=a.length>=e.minCandidates?a:_qotnDefaultRows(e),e.editing=null,showToast("Quotes refreshed. Your edits were kept.",3500)}catch(n){showToast(n.message||"Couldn\u2019t refresh quotes, try again in a moment.",5e3)}finally{e.refreshing=!1,t&&(t.disabled=!1,t.textContent="Refresh quotes"),_qotnModalState===e&&_qotnRenderModal()}}function _qotnWireModal(){if(_qotnModalWired)return;const e=document.getElementById("qotn-modal");if(!e)return;
729_qotnModalWired=!0,e.addEventListener("click",n=>{if(n.target===e||n.target.closest("[data-qotn-close]")){_qotnCloseModal();return}const a=n.target.closest("[data-qotn-prev],[data-qotn-next],[data-qotn-edit],[data-qotn-remove],[data-qotn-add]");!a||a.disabled||(a.dataset.qotnPrev!=null?_qotnCycle(+a.dataset.qotnPrev,-1):a.dataset.qotnNext!=null?_qotnCycle(+a.dataset.qotnNext,1):a.dataset.qotnEdit!=null?_qotnToggleEdit(+a.dataset.qotnEdit):a.dataset.qotnRemove!=null?_qotnRemoveRow(+a.dataset.qotnRemove):a.dataset.qotnAdd!=null&&_qotnAddRow(a.dataset.qotnAdd))}),document.addEventListener("keydown",n=>{_tfModalEsc(n,e)&&_qotnCloseModal()}),e.addEventListener("keydown",n=>{if(n.key!=="ArrowLeft"&&n.key!=="ArrowRight")return;const a=n.target.closest&&n.target.closest(".qotn-cycle");a&&(n.preventDefault(),_qotnCycle(+a.dataset.slot,n.key==="ArrowLeft"?-1:1))}),e.querySelector("[data-qotn-send]")?.addEventListener("click",_qotnSend),e.querySelector("[data-qotn-copy]")?.addEventListener("click",_qotnCopyVote),e.querySelector("[data-qotn-refresh]")?.addEventListener("click",_qotnRefresh),document.getElementById("qotn-npc-toggle")?.addEventListener("change",n=>_qotnSetNpcRow(n.target.checked)),document.getElementById("qotn-duration-row")?.addEventListener("click",n=>{const a=n.target.closest(".qotn-dur-btn"),s=_qotnModalState;!a||!s||s.sending||(s.durationHours=+a.dataset.qotnDur,_qotnRenderModal())});const t=document.getElementById("qotn-lines");t?.addEventListener("change",n=>{const a=n.target.closest(".qotn-speaker");a&&_qotnSaveField(+a.dataset.slot,"speaker",a.value)}),t?.addEventListener("blur",n=>{const a=_qotnModalState;if(!a||!n.target.closest)return;const s=n.target.closest(".qotn-quote"),i=n.target.closest(".qotn-context");if(s){const o=a.pool.find(c=>c.id===(a.rows[+s.dataset.slot]||{}).qid),r=s.value.trim();o&&r&&r!==(o.quote||"")?_qotnSaveField(+s.dataset.slot,"quote",r):o&&!r&&(s.value=o.quote||"")}else if(i){const o=a.pool.find(c=>c.id===(a.rows[+i.dataset.slot]||{}).qid),r=i.value.trim();o&&r!==(o.context||"")&&_qotnSaveField(+i.dataset.slot,"context",r)}},!0)}async function _qotnOpenModal(e){const t=currentCampaign?.id;if(!t||!e)return;const n=document.getElementById("qotn-modal");if(!n)return;let a;try{a=await api("GET",`/api/campaigns/${t}/quote-night/candidates?session_id=${e}`)}catch(c){showToast(c.message||"Could not load quotes.",5e3);return}if(_campaignStale(t))return;const s=a.quotes||[],i=a.duration_choices||[1,24,48,72],o={campaignId:t,sessionId:e,sessionTitle:a.session_title||"",pool:s,groups:_qotnBuildGroups(s),rows:[],editing:null,focusNext:null,discordConfigured:!!a.discord_configured,alreadySent:!!a.already_sent,sentIds:Array.isArray(a.sent_quote_ids)?a.sent_quote_ids:[],pollStatus:a.poll_status||null,pollHasWinner:!!a.poll_has_winner,minCandidates:a.min_candidates||2,maxCandidates:Math.min(a.max_candidates||10,10),sending:!1,refreshing:!1,durationHours:i.includes(a.duration_hours)?a.duration_hours:24};o.rows=_qotnDefaultRows(o),_qotnModalState=o,_qotnWireModal(),_qotnRenderModal(),n.classList.remove("hidden");const r=document.getElementById("qotn-lines");r&&(r.scrollTop=0)}function renderChronicleTimeline(){const e=currentCampaign&&currentCampaign.id;if(!e)return;const t=document.getElementById("tab-party2"),n=document.getElementById("tab-codex2"),a=document.getElementById("tab-atlas2");if(t&&!t.classList.contains("hidden")||n&&!n.classList.contains("hidden")||a&&!a.classList.contains("hidden")||_tfPendingLanding&&_tfPendingLanding!=="sessions")return;const s=document.querySelector(".tab-btn.active[data-tab], .nav-utility-btn.active[data-tab]");if(s&&s.dataset.tab&&s.dataset.tab!=="sessions")return;const i=_chr2Panel();if(allSessions===null){i.innerHTML='<div class="chron-shell no-rail"><div class="chron-right"><div class="chron-page"><p class="placeholder--padded">Loading chronicle\u2026</p></div></div></div>',_chr2Show(i,`#chronicle/${e}`);return}_chr2Show(i,`#chronicle/${e}`);const o=escHtml(currentCampaign&&currentCampaign.name||"Campaign"),r=(allSessions||[]).filter(B=>!B.is_session_zero).map(B=>({s:B,num:_chr2SessionNum(B)})).sort((B,H)=>B.num-H.num),c=r.length,l=r.length?r[r.length-1]:null,d=l?l.s.id:null,u=escHtml(_chr2TitleOrName()),p=B=>(B||"").trim().toLowerCase().replace(/[’‘]/g,"'").replace(/[“”]/g,'"').replace(/[–—]/g,"-").replace(/\s+/g," "),f=p(_chr2TitleOrName())===p(currentCampaign&&currentCampaign.name||"")?"Chronicle":`${o}
729 \xB7 Chronicle`,g=_chr2DefaultLead(),h=escHtml((currentCampaign.chronicle_lead||"").trim()||g),v=_chr2MonthSpan(r),b=[`<span><b>${c}</b> chapter${c!==1?"s":""}</span>`,v?`<span class="dot"></span><span><b>${v}</b> month${v!==1?"s":""}</span>`:""].filter(Boolean).join(""),y=l?(()=>{const{s:B,num:H}=l,A=_chr2DateShort(B),x=escHtml(_chr2OneLine(B)||""),M=_chr2Beats(B.summary),D=_chr2Roman(H),O=_chr2HeaderArtUrl(B),N=`<div class="ft-art"><div class="art art--dissolve-r ${O?"scene-img":"art--scene"}" role="img" aria-label="${O?"Cover art":"Scene"}"${O?` style="background-image:url('${encodeURI(O)}')"`:""}></div></div>`,G=_recapGate(B);if(_isGm()&&!B.summary&&!G.hide){const z=_isRecapInProgress(B);z&&_maybeStartRecapPoll(B.id);const V=z?'disabled title="Recap is generating\u2026"':G.disabled?`disabled title="${escHtml(G.tip)}"`:"",K=z?"\u23F3 Writing\u2026":"\u2726 Write This Chapter",se=`<button type="button" class="ft-generate-btn${G.disabled?" ft-generate-btn--disabled":""}" ${G.disabled?"":`data-chr-generate-index="${B.id}"`} ${V}>${K}</button>
730            <button type="button" class="ft-review-link" data-chr-review="${B.id}">Review transcript first</button>
731            <span class="ft-generate-status" id="ft-gen-status-${B.id}"></span>`;return`<div class="index-feature index-feature--awaiting">
732        ${N}
733        <div class="ft-body">
734          <span class="ft-tag ft-tag--awaiting"><span class="live-dot"></span>Latest \xB7 Awaiting recap</span>
735          <div class="ft-num">Chapter ${D}${A?` \xB7 ${escHtml(A)}`:""}</div>
736          <h2 class="ft-title">${escHtml(B.title||"Untitled session")}</h2>
737          ${x?`<p class="ft-teaser">${x}</p>`:""}
738          <div class="ft-awaiting-actions">
739            ${se}
740          </div>
741        </div>
742      </div>`}const F=_isGm()?`<button type="button" class="btn-new btn-sm ft-qotn-cta hidden" data-qotn-cta data-qotn-session="${B.id}" title="Pick the lines and send your table a vote">${_QOTN_SVG_TROPHY}<span>Quote of the Night</span></button>`:"",W=B.qotn&&(B.qotn.quote||"").trim()?`<div class="ft-qotn-won" data-qc-open="${B.qotn.quote_id}" role="button" tabindex="0" title="View the quote card">
743          <span class="ft-qotn-won-crown">\u{1F3C6} Quote of the Night</span>
744          <span class="ft-qotn-won-quote">\u201C${escHtml(B.qotn.quote)}\u201D</span>
745          <span class="ft-qotn-won-attr">\u2014 ${escHtml((B.qotn.speaker||"").trim()||"The party")}${(B.qotn.context||"").trim()?` \xB7 ${escHtml(B.qotn.context.trim())}`:""}</span>
746        </div>`:"";return`<div class="index-feature" role="button" tabindex="0" data-open="${B.id}">
747      <button type="button" class="ft-share" data-ft-share="${B.id}" title="Share this chapter" aria-label="Share this chapter"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m8 7 4-4 4 4"/><path d="M5 13v6a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6"/></svg><span>Share</span></button>
748      ${N}
749      <div class="ft-body">
750        <span class="ft-tag"><span class="live-dot"></span>Latest session</span>${_chr2NewRecapBadge(B.id)}
751        <div class="ft-num">Chapter ${D}${A?` \xB7 ${escHtml(A)}`:""}</div>
752        <h2 class="ft-title">${escHtml(B.title||"Untitled session")}</h2>
753        ${x?`<p class="ft-teaser">${x}</p>`:""}
754        ${W}
755        <div class="ft-meta">${M.length?`<span>${M.length} beat${M.length!==1?"s":""}</span>`:""}${F}<span class="open">Read the chapter &rarr;</span></div>
756      </div>
757    </div>`})():"",_=r.filter(B=>!B.s.summary).length,w=_isGm()&&_>=2?`<div class="index-awaiting-badge">${_} sessions awaiting recap</div>`:"",$=_isGm()&&c===0?`<button class="index-new" data-chr-new>
758    <span class="plus">+</span> New Session<span class="sub">Record the next chapter</span>
759  </button>`:"",E=_isGm()&&c===0?'<p class="index-new-hint">Paste your notes or record live, even a few sentences from memory is enough to start. Threadfall builds your Codex and Chronicle from there.</p>':"",k=_isGm()&&c>0?'<button class="index-new-pill" data-chr-new title="Record the next chapter">+ New Session</button>':"",C=k?`<div class="index-utility-row${w?"":" index-utility-row--end"}">${w}${k}</div>`:w,S=`<div class="colophon">
760    <span class="mark">${_CHR2_MARK_SVG} Threadfall Chronicle</span>
761    <span>${c} chapter${c!==1?"s":""} recorded</span>
762  </div>`,q=_isGm(),I=q?'<button class="chr2-edit-pen" data-edit="title" title="Edit title" aria-label="Edit chronicle title">\u270E</button>':"",T=q?'<button class="chr2-edit-pen" data-edit="lead" title="Edit story lead" aria-label="Edit story lead">\u270E</button>':"";i.innerHTML='<div id="tf-setup-slot"></div>'+((allSessions||[]).length===0?_introBannerHtml("chronicle"):"")+`<div class="chron-shell no-rail">
763    <div class="chron-right">
764      <div class="chron-page" id="chron-index">
765        <div class="index-inner">
766          <header class="index-head">
767            <div class="index-eyebrow">${f}</div>
768            <div class="chr2-field-row chr2-field-title">
769              <h1 class="index-title" id="chr2-title-el">${u}</h1>
770              ${I}
771            </div>
772            <div class="chr2-field-row chr2-field-lead">
773              <p class="index-lead" id="chr2-lead-el">${h}</p>
774              ${T}
775            </div>
776            <div class="index-stats">${b}</div>
777            <div class="ornament index-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div>
778          </header>
779          ${y}
780          ${$}
781          ${E}
782          ${C}
783          <div id="chron-toc-acts"></div>
784          ${S}
785        </div>
786      </div>
787    </div>
788  </div>`,_setupCardRefresh();const P=i.querySelector(".index-feature[data-open]");if(P){const B=()=>
788{_chr2ClearNewRecapBadge(+P.dataset.open,P),renderChronicleRecap(+P.dataset.open)};P.addEventListener("click",A=>{A.target.closest(".ft-qotn-cta")||A.target.closest("[data-qc-open]")||A.target.closest(".ft-share")||B()});const H=P.querySelector(".ft-share");H&&H.addEventListener("click",A=>{A.stopPropagation(),_chrShareMenu(+H.dataset.ftShare,H)}),P.addEventListener("keydown",A=>{if(A.key!=="Enter"&&A.key!==" ")return;const x=A.target.closest("[data-qc-open]");if(x){A.preventDefault(),x.click();return}A.target.closest(".ft-qotn-cta")||(A.preventDefault(),B())}),P.querySelector(".ft-qotn-cta")?.addEventListener("click",A=>{A.preventDefault(),A.stopPropagation();const x=A.currentTarget;x.disabled||_qotnOpenModal(+x.dataset.qotnSession)}),_qotnDecorateCard(P)}i.querySelector("[data-chr-generate-index]")?.addEventListener("click",function(){const B=+this.dataset.chrGenerateIndex;_fireRecapForSession(B,{btnEl:this,statusEl:i.querySelector(`#ft-gen-status-${B}`),onComplete:()=>renderChronicleRecap(B,{background:!0})})}),i.querySelector("[data-chr-review]")?.addEventListener("click",function(){_chr2OpenGmDrawer(+this.dataset.chrReview)}),_chr2RenderActsToc(i),_chr2RenderIndexTallyStats(i),i.querySelector("[data-chr-new]")?.addEventListener("click",()=>{if(!allSessions||allSessions.length===0){document.getElementById("first-session-choice-modal")?.classList.remove("hidden");return}typeof openSessionModal=="function"&&openSessionModal()}),q&&(i.querySelector('[data-edit="title"]')?.addEventListener("click",()=>_chr2OpenFieldEditor(i,"title")),i.querySelector('[data-edit="lead"]')?.addEventListener("click",()=>_chr2OpenFieldEditor(i,"lead")))}const _CHR2_RAIL_SORTS={act:"Acts",oldest:"Oldest",newest:"Newest"};function _chr2GetRailSort(){try{const e=localStorage.getItem("tf_chr_railsort");if(["act","oldest","newest"].includes(e))return e}catch{}return"act"}function _chr2RenderRail(e,t){const n=e.querySelector("#chron-list");if(!n)return;const a=e._chrRailSort||"act",s=e.querySelector("[data-chron-sort]");s&&(s.textContent=_CHR2_RAIL_SORTS[a]||"Acts");const i=(allSessions||[]).filter(g=>!g.is_session_zero).map(g=>({s:g,num:_chr2SessionNum(g)})).sort((g,h)=>g.num-h.num),o=i.length?i[i.length-1].s.id:null,r=_chr2BuildSections(i,currentCampaign&&currentCampaign.chronicle_acts),c=_chr2Zeros(),l={};for(const{s:g}of[...c,...i])l[g.id]=`${g.title||""} ${_chr2DateShort(g)||""} ${g.session_blurb||""} ${g.summary||""}`.toLowerCase();e._chrSearchIdx=l;const d=({s:g,num:h})=>{const v=g.id===o,b=v?'<span class="chron-badge live">Latest</span>':'<span class="chron-check" title="Recorded" aria-label="Recorded">\u2713</span>';return`<button class="chron-item${g.id===t?" active":""}${v?" is-live":""}" data-open="${g.id}">
789      <span class="chron-num">${h}</span>
790      <span class="chron-tw"><span class="chron-ti">${escHtml(g.title||"Untitled")}</span><span class="chron-tm">${escHtml(_chr2DateShort(g)||"No date")}</span></span>
791      ${b}
792    </button>`},u=c.map(d).join(""),p=c.length?`<div class="chron-group"><span>Prologue</span><span class="gc">${c.length}</span></div>`+u:"";let m="";if(a==="act"&&(r.length||c.length))if(r.length){const g=r[0].kind==="prologue",h=r.map(y=>({sec:y,rows:[]})),v={};h.forEach(y=>{v[y.sec.act]=y});for(const y of i){const _=_chr2SecForN(r,y.num);_&&v[_.act]&&v[_.act].rows.push(y)}let b=c.length&&!g?p:"";b+=h.map((y,_)=>{const w=c.length&&g&&_===0?u:"",$=y.rows.length+(w?c.length:0);return`<div class="chron-group"><span>${escHtml(_chr2SecLabel(r,y.sec))}${y.sec.name?" \xB7 "+escHtml(y.sec.name):""}</span><span class="gc">${$}</span></div>`+w+y.rows.map(d).join("")}).join(""),m=b}else m=p+i.map(d).join("");else{const g=[...c,...i];m=(a==="oldest"?g:g.slice().reverse()).map(d).join("")}n.innerHTML=m||'<p class="placeholder--padded">No chapters yet.</p>',n.querySelectorAll(".chron-item[data-open]").forEach(g=>g.addEventListener("click",()=>renderChronicleRecap(+g.dataset.open))),_chr2ApplyRailFilter(e);const f=n.querySelector(".chron-item.active");f&&f.scrollIntoView({block:"nearest"})}function _chr2ApplyRailFilter(e){const t=e.querySelector("#chron-list");if(!t)return;const n=e.querySelector(".chron-rail-search"),a=(n&&n.value||"").trim().toLowerCase(),s=e._chrSearchIdx||{},i=[...t.children];
792i.forEach(o=>{if(o.classList.contains("chron-item")){const r=s[+o.dataset.open]||"";o.style.display=!a||r.includes(a)?"":"none"}});for(let o=0;o<i.length;o++){if(!i[o].classList.contains("chron-group"))continue;let r=!1;for(let c=o+1;c<i.length&&!i[c].classList.contains("chron-group");c++)if(i[c].classList.contains("chron-item")&&i[c].style.display!=="none"){r=!0;break}i[o].style.display=r?"":"none"}}function _chr2HeaderArtUrl(e){let t=e&&e.header_image_url||"";if(!t&&e&&e.header_entity_id){const n=(allEntities||[]).find(a=>a.id===e.header_entity_id);t=n&&n.image_url||""}return t}function _chr2HeaderArt(e){const t=_chr2HeaderArtUrl(e),n=e&&e.header_focus_y!=null?e.header_focus_y:33,a=(e&&e.header_caption||"").trim(),i=`<div class="scene-cap" id="cr-herocap">${a?`<p class="scene-cap-text">${escHtml(a)}</p>${_isGm()?`<button type="button" class="scene-cap-edit" data-cap-edit title="Edit caption" aria-label="Edit caption">${_CHR2_PENCIL_SVG}</button>`:""}`:_isGm()?'<button type="button" class="scene-cap-add" data-cap-edit>+ Add a caption</button>':""}</div>`;if(t){const o=_isGm(),r="art art--dissolve-b scene-img"+(o?" chr2-cover--draggable":""),c=o?'<span class="chr2-cover-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v16"/><path d="M8 8l4-4 4 4"/><path d="M8 16l4 4 4-4"/></svg>Drag to reposition</span>':"";return`<figure class="scene scene-banner"><div class="${r}" role="img" aria-label="Chapter art" style="background-image:url('${encodeURI(t)}'); --cover-focus-y:${n}%">${c}</div></figure>${i}`}return _isGm()?`<figure class="scene scene-banner"><div class="art art--scene art--dissolve-b" role="img" aria-label="Scene art"><span class="art-drop">+ promote or drop art here</span></div></figure>${i}`:""}function _chr2WireCoverDrag(e,t){if(!_isGm())return;const n=e.querySelector(".scene-img.chr2-cover--draggable");if(!n)return;const a=currentCampaign&&currentCampaign.id,s=()=>{const p=parseFloat((n.style.getPropertyValue("--cover-focus-y")||"33").replace("%",""));return isNaN(p)?33:p};let i=!1,o=!1,r=0,c=33,l=33,d=1;n.addEventListener("pointerdown",p=>{if(!(p.button!==void 0&&p.button!==0)){p.preventDefault(),i=!0,o=!1,r=p.clientY,c=s(),l=c,d=n.getBoundingClientRect().height||1;try{n.setPointerCapture(p.pointerId)}catch{}n.classList.add("is-dragging")}}),n.addEventListener("pointermove",p=>{if(!i)return;const m=p.clientY-r;!o&&Math.abs(m)>4&&(o=!0),o&&(l=Math.max(0,Math.min(100,c-m/d*100)),n.style.setProperty("--cover-focus-y",l.toFixed(1)+"%"))});const u=async p=>{if(!i)return;i=!1,n.classList.remove("is-dragging");try{n.releasePointerCapture(p.pointerId)}catch{}if(!o){_chr2OpenHeaderArtModal(t);return}const m=Math.round(l);if(m!==Math.round(c)){t.header_focus_y=m;try{await api("POST",`/api/campaigns/${a}/sessions/${t.id}/header/focus`,{focus_y:m})}catch{}}};n.addEventListener("pointerup",u),n.addEventListener("pointercancel",u)}const _TF_DEFAULT_HEADER_STYLE=`Painterly digital fantasy illustration, semi-realistic anime and graphic-novel
793hybrid. Crisp inked contours with soft airbrushed gradients, cel-shaded forms
794blended into painted texture, clean high-detail rendering with no visible grain.
795
796Lighting: cinematic and dramatic. Strong warm-cool split, glowing magical or
797firelight sources as the primary illumination, deep rim light and backlight,
798volumetric haze and god rays, dark vignetted framing.
799
800Palette: desaturated slate blue, teal, cool grey, weathered stone and sandstone
801base, punctuated by one saturated accent glow (ember orange, arcane cyan,
802spectral green, or crimson).
803
804Atmosphere: drifting particles in the air, embers, ash, dust motes, rain, or
805petals. Shallow depth of field with soft bokeh falloff in the background.
806Layered foreground, midground, background for depth.
807
808Mood: moody, epic, quietly ominous. Painterly key art, not photorealism.
809Negative: text, watermark, characters, portraits, flat lighting, harsh saturation.`;function _tfCampaignStyleSeed(e){const t=e||(typeof currentCampaign<"u"?currentCampaign:null),n=(t&&t.session_header_image_style||"").trim();return n&&n!==_TF_DEFAULT_HEADER_STYLE?n:""}
809function _chr2OpenHeaderArtModal(e){if(!_isGm()||!(currentCampaign&&currentCampaign.id))return;let n=document.getElementById("chr2-art-modal");if(!n){n=document.createElement("div"),n.id="chr2-art-modal",n.className="modal-overlay chr2-art-modal hidden",n.innerHTML=`<div class="modal-box modal-box--lg chr2-am-box">
810      <div class="chr2-am-hdr">
811        <span class="chr2-am-title">Chapter art</span>
812        <button type="button" class="chr2-am-close" aria-label="Close">&times;</button>
813      </div>
814      <div class="chr2-am-body"></div>
815    </div>`,document.body.appendChild(n);const a=()=>n.classList.add("hidden");n.querySelector(".chr2-am-close").addEventListener("click",a),n.addEventListener("click",s=>{s.target===n&&a()}),document.addEventListener("keydown",s=>{_tfModalEsc(s,n)&&a()})}n._chrSid=e.id,n.querySelector(".chr2-am-body").innerHTML='<div class="chr2-am-loading">Loading art\u2026</div>',n.classList.remove("hidden"),_chr2FillHeaderArtModal(n,e)}async function _chr2FillHeaderArtModal(e,t){const n=currentCampaign&&currentCampaign.id,a=e.querySelector(".chr2-am-body");let s=[];try{const E=await api("GET",`/api/campaigns/${n}/sessions/${t.id}/debut-entities`);s=(E&&E.entities||[]).filter(k=>Array.isArray(k.image_urls)&&k.image_urls.length||k.image_url)}catch{}if(e._chrSid!==t.id)return;const i=new Set(s.map(E=>E.id)),o=(allEntities||[]).filter(E=>!i.has(E.id)&&(Array.isArray(E.image_urls)&&E.image_urls.length||E.image_url)).sort((E,k)=>(E.name||"").localeCompare(k.name||"")),r=t.header_image_url||null,c=t.header_entity_id||null,l=E=>(Array.isArray(E.image_urls)&&E.image_urls.length?E.image_urls:E.image_url?[E.image_url]:[]).map((C,S)=>({eid:E.id,name:E.name,url:C,primary:S===0})),d=E=>r?E.url===r:c===E.eid&&E.primary,u=E=>`<button type="button" class="chr2-am-thumb${d(E)?" is-current":""}" data-eid="${E.eid}" data-url="${escHtml(E.url)}" title="${escHtml(E.name||"")}">
816      <img src="${encodeURI(E.url)}" alt="" loading="lazy">
817      <span class="chr2-am-thumb-name">${escHtml(E.name||"")}</span>
818    </button>`,p=(E,k)=>`<div class="chr2-am-section"><span class="chr2-am-eyebrow">${E}</span>${k}</div>`,m=E=>`<div class="chr2-am-grid">${E.flatMap(l).map(u).join("")}</div>`,f=s.length?p("Introduced this chapter",m(s)):"",g=p("All campaign art",o.length?m(o):'<p class="chr2-am-none">No other entity art yet. Upload one below, or add art to a Codex entry.</p>'),h=p("From your library",`<button type="button" class="chr2-am-liblink" id="chr2-hdr-lib">
819      <span class="chr2-am-liblink-ico">&#128247;</span>
820      <span class="chr2-am-liblink-txt"><strong>Browse the campaign image library</strong><span>Reuse a header, cover, map, or upload from this campaign</span></span>
821      <span class="chr2-am-liblink-arr">&rsaquo;</span></button>`),v=p("Upload your own",`<label class="chr2-am-upload">
822      <input type="file" accept="image/*" class="hidden">
823      <span class="chr2-am-upload-cta">Drop an image here, or click to choose</span>
824      <span class="chr2-am-upload-hint">JPG \xB7 PNG \xB7 GIF \xB7 WebP \xB7 10&nbsp;MB max (a dedicated banner, not tied to an entity)</span>
825    </label>`),b=p("Generate with AI",`<div class="chr2-am-gen" id="chr2-am-gen">
826      <button type="button" class="chr2-am-gen-start"><svg class="tf-glyph" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.5 9.6 6.4 14.5 8 9.6 9.6 8 14.5 6.4 9.6 1.5 8 6.4 6.4Z"/></svg>Generate Session Header</button>
827      <p class="chr2-am-gen-sub">TF suggests a scene from this session and renders it in your campaign&rsquo;s art style, never depicting characters.</p>
828    </div>`);a.innerHTML=f+g+h+v+b;const y=a.querySelector("#chr2-hdr-lib");y&&y.addEventListener("click",()=>_campLibOpen({target:"header",session_id:t.id,filter:"header",title:"Choose a chapter header",subtitle:"Pick a loose campaign image to use as this chapter\u2019s header.",onPick:E=>{t.header_image_url=E.url,t.header_entity_id=null,e.classList.add("hidden"),renderChronicleRecap(t.id)}})),a.querySelectorAll(".chr2-am-thumb[data-eid]").forEach(E=>E.addEventListener("click",async()=>{const k=+E.dataset.eid,C=E.dataset.url||"";E.classList.add("is-busy");try{const S=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/header/promote`,{entity_id:k,image_url:C});t.header_entity_id=S&&S.header_entity_id||null,t.header_image_url=S&&S.header_image_url||null}catch(S){E.classList.remove("is-busy"),showToast(S&&S.message||"Could not set the cover.",4e3);return}e.classList.add("hidden"),renderChronicleRecap(t.id)}));const _=a.querySelector(".chr2-am-upload"),w=_.querySelector('input[type="file"]');w.addEventListener("change",()=>{const E=w.files&&w.files[0];E&&_chr2UploadHeaderArt(t,E,e)}),_.addEventListener("dragover",E=>{E.preventDefault(),_.classList.add("drag-over")}),_.addEventListener("dragleave",()=>_.classList.remove("drag-over")),_.addEventListener("drop",E=>{E.preventDefault(),_.classList.remove("drag-over");const k=E.dataTransfer&&E.dataTransfer.files&&E.dataTransfer.files[0];k&&_chr2UploadHeaderArt(t,k,e)});const $=a.querySelector(".chr2-am-gen-start");$&&$.addEventListener("click",()=>_chr2OpenHeaderGenPanel(a,t,e))}function _tfRenderAiImagePanel(e,t){e.innerHTML=`
829    <label class="chr2-am-gen-field">
830      <span class="chr2-am-gen-lbl chr2-am-gen-lbl--row">${escHtml(t.sceneLabel||"Scene")}
831        <button type="button" class="chr2-am-gen-suggest">&#10022; ${escHtml(t.suggestLabel||"Suggest")}</button>
832      </span>
833      <textarea class="chr2-am-gen-scene" rows="3" placeholder="${escHtml(t.scenePlaceholder||"")}"></textarea>
834    </label>
835    <label class="chr2-am-gen-field">
836      <span class="chr2-am-gen-lbl">Style <span class="chr2-am-gen-hint2">(edit for this image only)</span></span>
837      <textarea class="chr2-am-gen-style" rows="3" placeholder="(using the Threadfall default painterly style)"></textarea>
838    </label>
839    <div class="chr2-am-gen-actions">
840      <button type="button" class="chr2-am-gen-go">Generate image</button>
841      <span class="chr2-am-gen-status"></span>
842    </div>`;const n=e.querySelector(".chr2-am-gen-scene"),a=e.querySelector(".chr2-am-gen-style"),s=e.querySelector(".chr2-am-gen-go"),i=e.querySelector(".chr2-am-gen-suggest"),o=e.querySelector(".chr2-am-gen-status");t.styleSeed&&(a.value=t.styleSeed),i.addEventListener("click",async()=>{i.disabled=!0;const r=i.innerHTML;i.textContent="Composing\u2026";try{const c=await api("POST",t.suggestUrl,{});c&&c.scene&&(n.value=c.scene),o.textContent=""}catch(c){o.textContent=c&&c.message||"Could not suggest, type your own."}i.disabled=!1,i.innerHTML=r}),s.addEventListener("click",async()=>{const r=(n.value||"").trim();if(!r){o.textContent="Describe it first.";return}s.disabled=!0,o.textContent=t.busyMsg||"Generating image: this can take ~15\u201330s\u2026";try{const c=await api("POST",t.generateUrl,{scene:r,style:(a.value||"").trim()});await t.onDone(c)}catch(c){s.disabled=!1,o.textContent=c&&c.message||"Generation failed, try again or edit the description."}})}async function _chr2OpenHeaderGenPanel(e,t,n){const a=currentCampaign&&currentCampaign.id,s=e.querySelector("#chr2-am-gen");if(!s)return;s.innerHTML=`
843    <label class="chr2-am-gen-field">
844      <span class="chr2-am-gen-lbl chr2-am-gen-lbl--row">Scene
845        <button type="button" class="chr2-am-gen-suggest">&#10022; Suggest from this session</button>
846      </span>
847      <textarea class="chr2-am-gen-scene" rows="3" placeholder="Describe the scene to depict (characters are never shown), or use Suggest."></textarea>
848    </label>
849    <label class="chr2-am-gen-field">
850      <span class="chr2-am-gen-lbl">Style <span class="chr2-am-gen-hint2">(from Campaign Settings; edit for this image only)</span></span>
851      <textarea class="chr2-am-gen-style" rows="3" placeholder="(using the Threadfall default painterly style)"></textarea>
852    </label>
853    <div class="chr2-am-gen-actions">
854      <button type="button" class="chr2-am-gen-go">Generate image</button>
855      <span class="chr2-am-gen-status"></span>
856    </div>`;const i=s.querySelector(".chr2-am-gen-scene"),o=s.querySelector(".chr2-am-gen-style"),r=s.querySelector(".chr2-am-gen-go"),c=s.querySelector(".chr2-am-gen-suggest"),l=s.querySelector(".chr2-am-gen-status");o.value=_tfCampaignStyleSeed(),c.addEventListener("click",async()=>{c.disabled=!0;const d=c.innerHTML;c.textContent="Composing\u2026";try{const u=await api("POST",`/api/campaigns/${a}/sessions/${t.id}/header/suggest-scene`,{});u&&u.scene&&(i.value=u.scene),l.textContent=""}catch(u){l.textContent=u&&u.message||"Could not suggest a scene, type your own."}c.disabled=!1,c.innerHTML=d}),r.addEventListener("click",async()=>{const d=(i.value||"").trim();if(!d){l.textContent="Describe a scene first.";return}r.disabled=!0,l.textContent="Generating image: this can take ~15\u201330s\u2026";try{const u=await api("POST",`/api/campaigns/${a}/sessions/${t.id}/header/generate`,{scene:d,style:(o.value||"").trim()});t.header_image_url=u&&u.header_image_url||t.header_image_url,t.header_entity_id=null,n&&n.classList.add("hidden"),renderChronicleRecap(t.id)}catch(u){r.disabled=!1,l.textContent=u&&u.message||"Generation failed, try again or edit the scene."}})}async function _chr2UploadHeaderArt(e,t,n){if(!t||!(t.type||"").startsWith("image/")){showToast("Please choose an image file (JPG, PNG, GIF, or WebP).",4e3);return}if(t.size>TF_UPLOAD_MAX_BYTES){showToast("Image must be 10 MB or smaller.",4e3);return}const a=currentCampaign&&currentCampaign.id,s=n&&n.querySelector(".chr2-am-body");s&&s.classList.add("is-uploading");try{const i=await _tfUploadDirect(t,{paramsPath:`/api/campaigns/${a}/upload-params`,kind:"header",endpoint:`/api/campaigns/${a}/sessions/${e.id}/header/upload`,label:"Header image"});e.header_image_url=i&&i.header_image_url||e.header_image_url,e.header_entity_id=null}catch{s&&s.classList.remove("is-uploading"),showToast("Image upload failed.",4e3);return}n&&n.classList.add("hidden"),renderChronicleRecap(e.id)}function _tfHandleAutoInline(e){const t=e&&e.auto_inline||[];if(!Array.isArray(t)||!t.length)return;t.forEach(s=>{const i=(Array.isArray(allSessions)?allSessions:[]).find(l=>l&&l.id===s.session_id);if(!i)return;const o=Array.isArray(i.inline_entity_ids)?i.inline_entity_ids.slice():[];o.indexOf(s.entity_id)===-1&&o.push(s.entity_id),i.inline_entity_ids=o,i.inline_beats=Object.assign({},i.inline_beats&&typeof i.inline_beats=="object"?i.inline_beats:{},{[String(s.entity_id)]:s.beat});const r=i.inline_auto&&typeof i.inline_auto=="object"?i.inline_auto:{},c=Array.isArray(r.ids)?r.ids.slice():[];c.indexOf(s.entity_id)===-1&&c.push(s.entity_id),i.inline_auto={ids:c,excluded:Array.isArray(r.excluded)?r.excluded:[]}});const n=t[0],a=n.session_title?`\u201C${n.session_title}\u201D`:"their debut chapter";showToast(`Portrait placed in-line in ${a}, open the chapter to adjust or remove it.`,6e3)}async function _chr2RenderDebutBoxes(e,t){const n=e.querySelector("#cr-debuts");if(!n)return;
856const a=_isGm(),s=currentCampaign&&currentCampaign.id;if(!s)return;let i=[];try{const m=await api("GET",`/api/campaigns/${s}/sessions/${t.id}/debut-entities`);i=m&&m.entities||[]}catch{return}const o=a?i:i.filter(m=>!_hiddenFromViewer(m)).map(_teaseView);if(!o.length&&!a){n.hidden=!0;return}const r=t.header_entity_id||null,c=new Set(Array.isArray(t.inline_entity_ids)?t.inline_entity_ids:[]),l=_chr2InlineCap(t),d=a?`<div class="debut-more-row">
857        <span class="debut-more-label">Other campaign art</span>
858        <button type="button" class="debut-promote debut-action" data-more-cover>&#9733; Set cover&hellip;</button>
859        <button type="button" class="debut-inline debut-action" data-more-inline>&#9634; Add in-line&hellip;</button>
860      </div>`:"";n.innerHTML=`<div class="cast-hdr">Introduced this Chapter${a?` <span class="debut-cap">cover + up to ${l} in-line</span>`:""}</div>
861    <div class="debut-grid">${o.map(m=>{const f=Array.isArray(m.image_urls)&&m.image_urls.length?m.image_urls:m.image_url?[m.image_url]:[],g=f.length>0,h=r&&m.id===r,v=c.has(m.id),b=g?f.length===1?`<div class="debut-art" style="background-image:url('${encodeURI(f[0])}')"></div>`:`<div class="debut-art-strip">${f.map(_=>`<div class="debut-art debut-art--multi" style="background-image:url('${encodeURI(_)}')"></div>`).join("")}</div>`:`<div class="debut-art debut-art--empty">${a?"<span>no art yet</span>":`<svg class="debut-art-glyph" aria-hidden="true"><use href="#tf-glyph-${_CX2_GL[m.type]||"npc"}"></use></svg>`}</div>`,y=a&&g?`<div class="debut-ctrls">
862             <button type="button" class="debut-promote${h?" is-on":""}" data-cover="${m.id}">${h?"\u2605 cover":"\u2606 cover"}</button>
863             <button type="button" class="debut-inline${v?" is-on":""}" data-inline="${m.id}">${v?"\u25A3 in-line":"\u25A2 in-line"}</button>
864           </div>`:"";return`<div class="debut-box${h?" is-promoted":""}" data-eid="${m.id}" role="button" tabindex="0">
865        ${b}
866        <div class="debut-meta">
867          <a class="debut-name" data-eid="${m.id}" role="button" tabindex="0">${escHtml(m.name||"")}</a>
868          <span class="debut-sub">${escHtml(m.type||"")} \xB7 introduced this session</span>
869          ${_revealTagHtml(m)}
870        </div>
871        ${y}
872      </div>`}).join("")}</div>${d}`,n.hidden=!1;const u=new Set(o.map(m=>m.id)),p=o.length?e.querySelector(".cast-row"):null;if(p){p.querySelectorAll(".cx2-rail-item[data-eid]").forEach(f=>{u.has(+f.dataset.eid)&&f.remove()});const m=e.querySelector(".cast .cast-hdr");m&&(m.textContent="Others In This Chapter"),p.querySelector(".cx2-rail-item[data-eid]")||p.closest(".cast")?.classList.add("hidden")}n.querySelectorAll(".debut-box[data-eid]").forEach(m=>_chr2Activate(m,()=>{const f=+m.dataset.eid;f&&typeof renderCodexEntry=="function"&&renderCodexEntry(f)})),n.querySelectorAll(".debut-promote[data-cover]").forEach(m=>m.addEventListener("click",async f=>{f.stopPropagation();const g=+m.dataset.cover,h=m.classList.contains("is-on");try{h?(await api("DELETE",`/api/campaigns/${s}/sessions/${t.id}/header`),t.header_entity_id=null,t.header_image_url=null):(await api("POST",`/api/campaigns/${s}/sessions/${t.id}/header/promote`,{entity_id:g}),t.header_entity_id=g,t.header_image_url=null)}catch(v){showToast(v&&v.message||"Could not update the cover.",4e3);return}renderChronicleRecap(t.id)})),n.querySelectorAll(".debut-inline[data-inline]").forEach(m=>m.addEventListener("click",async f=>{f.stopPropagation();const g=+m.dataset.inline,h=Array.isArray(t.inline_entity_ids)?t.inline_entity_ids.slice():[],v=h.indexOf(g);if(v>=0)h.splice(v,1);else if(h.length>=l){showToast(`This chapter has room for ${l} in-line image${l!==1?"s":""} (set by its beat count). Demote one to make room for this.`,5e3);return}else h.push(g);try{const b=await api("POST",`/api/campaigns/${s}/sessions/${t.id}/inline`,{entity_ids:h});t.inline_entity_ids=b&&b.inline_entity_ids||h,b&&b.inline_auto&&(t.inline_auto=b.inline_auto)}catch(b){showToast(b&&b.message||"Could not update in-line images.",4e3);return}renderChronicleRecap(t.id)})),n.querySelector("[data-more-cover]")?.addEventListener("click",m=>{m.stopPropagation(),_chr2OpenHeaderArtModal(t)}),n.querySelector("[data-more-inline]")?.addEventListener("click",m=>{m.stopPropagation(),_chr2OpenInlinePicker(t)}),a&&n.querySelectorAll(".debut-box[data-eid]").forEach(m=>{m.addEventListener("dragover",f=>{f.preventDefault(),m.classList.add("drag-over")}),m.addEventListener("dragleave",()=>m.classList.remove("drag-over")),m.addEventListener("drop",async f=>{f.preventDefault(),m.classList.remove("drag-over");const g=f.dataTransfer&&f.dataTransfer.files&&f.dataTransfer.files[0];if(!(!g||!(g.type||"").startsWith("image/"))){m.classList.add("uploading");try{const h=await _tfUploadDirect(g,{paramsPath:`/api/campaigns/${s}/upload-params`,kind:"entity",endpoint:`/api/campaigns/${s}/entities/${+m.dataset.eid}/upload-image`});Array.isArray(allEntities)&&(allEntities=allEntities.map(v=>v.id===h.id?h:v)),_tfHandleAutoInline(h),(h.auto_inline||[]).some(v=>v&&v.session_id===t.id)?renderChronicleRecap(t.id):_chr2RenderDebutBoxes(e,t)}catch{m.classList.remove("uploading"),showToast("Image upload failed.",4e3)}}})})}
872function _chr2OpenInlinePicker(e){if(!_isGm()||!(currentCampaign&&currentCampaign.id))return;let n=document.getElementById("chr2-inline-modal");if(!n){n=document.createElement("div"),n.id="chr2-inline-modal",n.className="modal-overlay chr2-art-modal hidden",n.innerHTML=`<div class="modal-box modal-box--lg chr2-am-box">
873      <div class="chr2-am-hdr">
874        <span class="chr2-am-title">Add in-line art</span>
875        <button type="button" class="chr2-am-close" aria-label="Close">&times;</button>
876      </div>
877      <div class="chr2-am-body"></div>
878    </div>`,document.body.appendChild(n);const a=()=>n.classList.add("hidden");n.querySelector(".chr2-am-close").addEventListener("click",a),n.addEventListener("click",s=>{s.target===n&&a()}),document.addEventListener("keydown",s=>{_tfModalEsc(s,n)&&a()})}n._chrSid=e.id,n._libImages=null,_chr2FillInlinePicker(n,e),n.classList.remove("hidden")}async function _chr2FillInlinePicker(e,t){const n=currentCampaign&&currentCampaign.id,a=e.querySelector(".chr2-am-body"),s=_chr2InlineCap(t),i=new Set(Array.isArray(t.inline_entity_ids)?t.inline_entity_ids:[]),o=(allEntities||[]).filter(g=>Array.isArray(g.image_urls)&&g.image_urls.length||g.image_url).sort((g,h)=>(g.name||"").localeCompare(h.name||"")),r=t.inline_image_overrides&&typeof t.inline_image_overrides=="object"?t.inline_image_overrides:{},c=g=>Array.isArray(g.image_urls)&&g.image_urls.length?g.image_urls:g.image_url?[g.image_url]:[],l=g=>{const h=r[String(g.id)]||r[g.id];return h&&c(g).indexOf(h)!==-1?h:c(g)[0]||g.image_url},d=(g,h,v,b)=>`<button type="button" class="chr2-am-thumb${i.has(g.id)&&l(g)===h?" is-current":""}" data-eid="${g.id}" data-url="${escHtml(h)}" title="${escHtml(g.name||"")}${b?v===0?" \xB7 main image":" \xB7 alternate image":""}">
879      <img src="${encodeURI(h)}" alt="" loading="lazy">
880      <span class="chr2-am-thumb-name">${escHtml(g.name||"")}</span>
881    </button>`,u=g=>{const h=c(g);return h.map((v,b)=>d(g,v,b,h.length>1)).join("")};if(!Array.isArray(e._libImages))try{const g=await api("GET",`/api/campaigns/${n}/images`);e._libImages=g&&g.images||[]}catch{e._libImages=[]}const p=e._libImages||[],m=new Set((Array.isArray(t.inline_images)?t.inline_images:[]).map(g=>g&&g.url).filter(Boolean)),f=g=>`<button type="button" class="chr2-am-thumb${m.has(g.url)?" is-current":""}" data-liburl="${encodeURIComponent(g.url)}" title="${escHtml(g.label||g.kind||"Library image")}">
882      <img src="${encodeURI(g.url)}" alt="" loading="lazy">
883      <span class="chr2-am-thumb-name">${escHtml(g.label||g.kind||"Image")}</span>
884    </button>`;a.innerHTML=`<div class="chr2-am-section">
885      <span class="chr2-am-eyebrow" id="chr2-inline-count">${i.size} of ${s} in-line \xB7 pick the exact image for this chapter \xB7 click a lit image to remove it</span>
886      ${o.length?`<div class="chr2-am-grid">${o.map(u).join("")}</div>`:'<p class="chr2-am-none">No campaign art yet. Add art to a Codex entry first.</p>'}
887    </div>
888    <div class="chr2-am-section chr2-am-section--lib">
889      <span class="chr2-am-eyebrow">From your library \xB7 click to add (with a caption)</span>
890      ${p.length?`<div class="chr2-am-grid">${p.map(f).join("")}</div>`:'<p class="chr2-am-none">No library images yet. Covers, session headers, maps & uploads appear here.</p>'}
891    </div>`,a.querySelectorAll(".chr2-am-thumb[data-eid]").forEach(g=>g.addEventListener("click",async()=>{const h=+g.dataset.eid,v=g.dataset.url||"",b=Array.isArray(t.inline_entity_ids)?t.inline_entity_ids.slice():[],y=b.indexOf(h),_=(allEntities||[]).find(w=>w.id===h);g.classList.add("is-busy");try{if(y>=0&&_&&l(_)!==v){const w=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/inline/${h}/image`,{image_url:v});t.inline_image_overrides=w&&w.inline_image_overrides||t.inline_image_overrides||{}}else if(y>=0){b.splice(y,1);const w=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/inline`,{entity_ids:b});t.inline_entity_ids=w&&w.inline_entity_ids||b,w&&w.inline_auto&&(t.inline_auto=w.inline_auto),w&&w.inline_image_overrides&&(t.inline_image_overrides=w.inline_image_overrides)}else{if(b.length>=s){g.classList.remove("is-busy");const $=a.querySelector("#chr2-inline-count");$&&($.textContent=`In-line is full (${s}/${s}), remove one first`);return}b.push(h);const w=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/inline`,{entity_ids:b});if(t.inline_entity_ids=w&&w.inline_entity_ids||b,w&&w.inline_auto&&(t.inline_auto=w.inline_auto),w&&w.inline_image_overrides&&(t.inline_image_overrides=w.inline_image_overrides),_&&v&&v!==(c(_)[0]||_.image_url)){const $=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/inline/${h}/image`,{image_url:v});t.inline_image_overrides=$&&$.inline_image_overrides||t.inline_image_overrides||{}}}}catch(w){g.classList.remove("is-busy"),showToast(w&&w.message||"Could not update in-line images.",4e3);return}typeof _chr2InvalidatePlacementSpecs=="function"&&_chr2InvalidatePlacementSpecs(),renderChronicleRecap(t.id),_chr2FillInlinePicker(e,t)})),a.querySelectorAll(".chr2-am-thumb[data-liburl]").forEach(g=>g.addEventListener("click",async()=>{const h=decodeURIComponent(g.dataset.liburl||"");if(!h)return;const v=(Array.isArray(t.inline_images)?t.inline_images:[]).slice(),b=v.findIndex(y=>y&&y.url===h);if(b>=0)v.splice(b,1);else{const y=typeof _chr2Beats=="function"&&_chr2Beats(t.summary).length||1;v.push({url:h,caption:"",beat:Math.min(v.length,Math.max(0,y-1))})}g.classList.add("is-busy");try{const y=await api("POST",`/api/campaigns/${n}/sessions/${t.id}/inline-images`,{images:v});t.inline_images=y&&y.inline_images||v}catch(y){g.classList.remove("is-busy"),showToast(y&&y.message||"Could not update in-line art.",4e3);return}renderChronicleRecap(t.id),_chr2FillInlinePicker(e,t)}))}function _chr2InlineCap(e){const t=_chr2Beats(e&&e.summary).length||0;return _chr2PickLayout(t)==="broadsheet"?1:Math.max(1,Math.min(5,Math.floor(t/2)))}function _chr2ImageSlots(e,t){if(!t||!e)return[];const n=Math.max(1,Math.floor(e/t)),a=[];for(let s=0;s<t;s++)a.push(Math.min(1+n*s,e-1));return a}
891async function _chr2RenderInlineImages(e,t){const n=Array.isArray(t.inline_entity_ids)?t.inline_entity_ids:[];if(!n.length)return;const a=e.querySelector("#cr-body");if(!a)return;const s=currentCampaign&&currentCampaign.id,i=n.map(h=>(allEntities||[]).find(v=>v.id===h)).filter(h=>h&&h.image_url);if(!i.length)return;const o=[...a.querySelectorAll(".session-beat-header")];if(!o.length)return;const r=Math.min(i.length,o.length),c=_chr2ImageSlots(o.length,r),l={};try{const h=await _chr2GetPlacementSpecs(s,t.id);if((location.hash||"")!==`#chronicle/${s}/${t.id}`)return;i.forEach(v=>{l[v.id]=h[v.id]||null})}catch{}const d=_codex2IsGm()||!!(currentUser&&currentUser.is_admin),u=t.inline_beats&&typeof t.inline_beats=="object"?t.inline_beats:{},p=o.length-1,m={},f=new Set,g=(h,v)=>{let b=Math.max(0,Math.min(p,v));if(f.has(b))for(let y=1;y<=o.length;y++){if(b+y<=p&&!f.has(b+y)){b+=y;break}if(b-y>=0&&!f.has(b-y)){b-=y;break}}m[h.id]=b,f.add(b)};i.forEach(h=>{const v=h&&u[h.id]!=null?parseInt(u[h.id]):null;v!=null&&!isNaN(v)&&g(h,v)}),c.forEach((h,v)=>{const b=i[v];b&&m[b.id]==null&&g(b,h)}),c.forEach((h,v)=>{const b=i[v];if(!b)return;const y=m[b.id],_=o[y];if(!_)return;const w=l[b.id],$=!!(w&&(w.mode||"")==="dissolve"&&w.matte_url&&!(w.dist_urls&&Object.keys(w.dist_urls).length));if(d&&typeof _cx2BakeOwed=="function"&&_cx2BakeOwed(w)&&!$){const H=v%2===0?"left":"right",A=document.createElement("figure");A.className=`chr2-inset chr2-inset--${H} chr2-inset--plate chr2-inset--baking`,A.style.cursor="pointer",A.innerHTML=`<img src="${encodeURI(w&&w.image_url||b.image_url)}" alt="${escHtml(b.name||"")}" loading="lazy">`+(typeof _cx2BakeOverlayHtml=="function"?_cx2BakeOverlayHtml(w.baked_treatment):"")+`<figcaption>${escHtml(w&&w.label||b.name||"")}</figcaption>`,A.addEventListener("click",()=>{d&&typeof openCx2ImageModal=="function"?openCx2ImageModal(b.id,null,{sessionId:t.id}):typeof renderCodexEntry=="function"&&renderCodexEntry(b.id)}),_.parentNode.insertBefore(A,_.nextSibling),typeof _cx2BakeWatch=="function"&&_cx2BakeWatch(s,b.id,{sessionId:t.id});return}const E=$?"cutout":w&&w.mode||"fullbleed",k=w&&w.edge||null,C=!!(window.CodexHero&&E==="dissolve"&&w.matte_url&&k&&w.dist_urls&&w.dist_urls[k]),S=!!(window.CodexHero&&E==="cutout"&&w.matte_url),q=C||S,I=C&&(k==="left"||k==="right"),T=I?k==="left"?"right":"left":v%2===0?"left":"right",P=T==="left"?"right":"left",B=document.createElement("figure");if(B.className=`chr2-inset chr2-inset--${T} `+(q?"chr2-inset--webgl":"chr2-inset--plate"),B.style.cursor="pointer",B.innerHTML=(q?"<canvas></canvas>":`<img src="${encodeURI(w&&w.image_url||b.image_url)}" alt="${escHtml(b.name||"")}" loading="lazy">`)+`<figcaption>${escHtml(w&&w.label||b.name||"")}</figcaption>`,d){const H=T,A=y>0?'<button type="button" class="chr2-inset-mv chr2-inset-up" title="Move to an earlier section" aria-label="Move image up">\u25B2</button>':"",x=y<p?'<button type="button" class="chr2-inset-mv chr2-inset-down" title="Move to a later section" aria-label="Move image down">\u25BC</button>':"";if(B.insertAdjacentHTML("beforeend",`<div class="chr2-inset-ctrls chr2-inset-ctrls--${H}">${A}${x}<button type="button" class="chr2-inset-mv chr2-inset-edit" title="Edit / change image" aria-label="Edit image"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l4-4 3 3 4-5 3 4"/><circle cx="8.5" cy="9.5" r="1.1"/></svg></button><button type="button" class="chr2-inset-mv chr2-inset-x" title="Remove this image from the chapter" aria-label="Remove image from chapter">&times;</button></div>`),((t.inline_auto||{}).ids||[]).indexOf(b.id)!==-1){const O=B.querySelector("figcaption");O&&O.insertAdjacentHTML("beforeend",' <span class="chr2-inset-autotag" title="Placed automatically at this character\u2019s debut beat. Move (\u25B2/\u25BC) or remove (\xD7), a removal is never re-added.">auto</span>')}if(w&&w.overridden&&(w.overridden.image||w.overridden.look)){const O=B.querySelector("figcaption");O&&O.insertAdjacentHTML("beforeend",' <span class="chr2-inset-autotag" title="This chapter shows its own image or look for this entry. Open the editor (\u270E) to change it or reset to the Codex.">chapter</span>')}}if(B.addEventListener("click",()=>{d&&typeof openCx2ImageModal=="function"?openCx2ImageModal(b.id,null,{sessionId:t.id}):typeof renderCodexEntry=="function"&&renderCodexEntry(b.id)}),d){B.querySelector(".chr2-inset-edit")?.addEventListener("click",A=>{A.stopPropagation(),typeof openCx2ImageModal=="function"&&openCx2ImageModal(b.id,null,{sessionId:t.id})}),B.querySelector(".chr2-inset-x")?.addEventListener("click",async A=>{A.stopPropagation();const x=(Array.isArray(t.inline_entity_ids)?t.inline_entity_ids:[]).filter(D=>D!==b.id),M=Object.assign({},u);delete M[b.id];try{const D=await api("POST",`/api/campaigns/${s}/sessions/${t.id}/inline`,{entity_ids:x,beats:M});t.inline_entity_ids=D&&D.inline_entity_ids||x,t.inline_beats=D&&D.inline_beats||M,D&&D.inline_auto&&(t.inline_auto=D.inline_auto)}catch(D){showToast(D&&D.message||"Could not remove the image.",4e3);return}renderChronicleRecap(t.id)});const H=async A=>{const x=Math.max(0,Math.min(p,y+A));if(x===y)return;const M=Object.assign({},u);M[b.id]=x;const D=Array.isArray(t.inline_entity_ids)?t.inline_entity_ids.slice():[];try{const O=await api("POST",`/api/campaigns/${s}/sessions/${t.id}/inline`,{entity_ids:D,beats:M});t.inline_entity_ids=O&&O.inline_entity_ids||D,t.inline_beats=O&&O.inline_beats||M,O&&O.inline_auto&&(t.inline_auto=O.inline_auto)}catch(O){showToast(O&&O.message||"Could not move the image.",4e3);return}renderChronicleRecap(t.id)};B.querySelector(".chr2-inset-up")?.addEventListener("click",A=>{A.stopPropagation(),H(-1)}),B.querySelector(".chr2-inset-down")?.addEventListener("click",A=>{A.stopPropagation(),H(1)})}if(_.parentNode.insertBefore(B,_.nextSibling),d&&$&&typeof _cx2BakeWatch=="function"&&_cx2BakeWatch(s,b.id,{sessionId:t.id}),q){const H=CodexHero.mount(B.querySelector("canvas"));H&&((e._chrInsetHeros=e._chrInsetHeros||[]).push(H),H.show({mode:C?"dissolve":"cutout",image_url:w.image_url,matte_url:w.matte_url,dist_urls:w.dist_urls,edge:k,depth:w.depth,feather:w.feather,deckle:w.deckle,scale:w.scale}).catch(()=>{}),I&&typeof _codex2SetGeodesicShape=="function"?_codex2SetGeodesicShape(B,w.dist_urls[k],w.depth,P):S&&typeof _codex2SetMatteShape=="function"&&_codex2SetMatteShape(B,w.matte_url,P))}})}function _chr2RenderInlineLibraryImages(e,t){const n=Array.isArray(t.inline_images)?t.inline_images:[];if(!n.length)return;const a=e.querySelector("#cr-body");if(!a)return;const s=currentCampaign&&currentCampaign.id,i=[...a.querySelectorAll(".session-beat-header")];if(!i.length)return;const o=i.length-1,r=_codex2IsGm()||!!(currentUser&&currentUser.is_admin),c=async l=>{try{const d=await api("POST",`/api/campaigns/${s}/sessions/${t.id}/inline-images`,{images:l});t.inline_images=d&&d.inline_images||l}catch(d){showToast(d&&d.message||"Could not update in-line art.",4e3);return}renderChronicleRecap(t.id)};
891n.forEach((l,d)=>{if(!l||!l.url)return;const u=Math.max(0,Math.min(o,parseInt(l.beat)||0)),p=i[u];if(!p)return;const m=d%2===0?"right":"left",f=document.createElement("figure");f.className=`chr2-inset chr2-inset--${m} chr2-inset--plate chr2-inset--lib`;const g=l.caption?`<span class="chr2-inset-cap-text">${escHtml(l.caption)}</span>${r?` <button type="button" class="chr2-inset-cap-edit" title="Edit caption" aria-label="Edit caption">${_CHR2_PENCIL_SVG}</button>`:""}`:r?'<button type="button" class="chr2-inset-cap-edit chr2-inset-cap-add" title="Add a caption">+ Add a caption</button>':"";if(f.innerHTML=`<img src="${encodeURI(l.url)}" alt="${escHtml(l.caption||"")}" loading="lazy"><figcaption class="chr2-inset-cap${l.caption?"":" chr2-inset-cap--empty"}">${g}</figcaption>`,r){const h=u>0?'<button type="button" class="chr2-inset-mv chr2-inset-up" title="Move to an earlier section" aria-label="Move image up">\u25B2</button>':"",v=u<o?'<button type="button" class="chr2-inset-mv chr2-inset-down" title="Move to a later section" aria-label="Move image down">\u25BC</button>':"";f.insertAdjacentHTML("beforeend",`<div class="chr2-inset-ctrls chr2-inset-ctrls--${m}">${h}${v}<button type="button" class="chr2-inset-mv chr2-inset-x" title="Remove this image from the chapter" aria-label="Remove image from chapter">&times;</button></div>`)}if(p.parentNode.insertBefore(f,p.nextSibling),r){f.querySelector(".chr2-inset-x")?.addEventListener("click",v=>{v.stopPropagation();const b=n.slice();b.splice(d,1),c(b)});const h=v=>{const b=Math.max(0,Math.min(o,u+v));if(b===u)return;const y=n.map((_,w)=>w===d?Object.assign({},_,{beat:b}):_);c(y)};f.querySelector(".chr2-inset-up")?.addEventListener("click",v=>{v.stopPropagation(),h(-1)}),f.querySelector(".chr2-inset-down")?.addEventListener("click",v=>{v.stopPropagation(),h(1)}),f.querySelector(".chr2-inset-cap-edit")?.addEventListener("click",v=>{v.stopPropagation(),_chr2EditInlineCaption(f,t,d)})}})}function _chr2EditInlineCaption(e,t,n){const a=e.querySelector(".chr2-inset-cap");if(!a||a.dataset.editing==="1")return;const s=(Array.isArray(t.inline_images)?t.inline_images:[])[n];if(!s)return;a.dataset.editing="1",a.classList.remove("chr2-inset-cap--empty");const i=currentCampaign&&currentCampaign.id,o=a.innerHTML,r=document.createElement("input");r.type="text",r.className="chr2-inline-input chr2-inset-cap-input",r.maxLength=240,r.value=s.caption||"",r.placeholder="Caption\u2026",a.innerHTML="",a.appendChild(r),r.focus(),r.select();let c=!1;const l=async d=>{if(c)return;if(c=!0,!d){a.innerHTML=o,a.dataset.editing="",s.caption||a.classList.add("chr2-inset-cap--empty");return}const u=r.value.trim().slice(0,240),p=(Array.isArray(t.inline_images)?t.inline_images:[]).map((m,f)=>f===n?Object.assign({},m,{caption:u}):m);try{const m=await api("POST",`/api/campaigns/${i}/sessions/${t.id}/inline-images`,{images:p});t.inline_images=m&&m.inline_images||p}catch(m){showToast(m&&m.message||"Could not save the caption.",4e3),a.innerHTML=o,a.dataset.editing="";return}renderChronicleRecap(t.id)};r.addEventListener("keydown",d=>{d.key==="Enter"?(d.preventDefault(),l(!0)):d.key==="Escape"&&(d.preventDefault(),l(!1))}),r.addEventListener("blur",()=>l(!0))}function _chr2RerenderIfOpen(){try{typeof _chr2InvalidatePlacementSpecs=="function"&&_chr2InvalidatePlacementSpecs();const e=document.getElementById("tab-chronicle2");if(!e||e.classList.contains("hidden"))return;const t=(location.hash||"").match(/^#chronicle\/(\d+)\/(\d+)$/);t&&typeof renderChronicleRecap=="function"&&renderChronicleRecap(+t[2],{background:!0})}catch{}}const _CHR2_GM_TABS_REGULAR=[{key:"transcript",label:"Transcript"},{key:"live",label:"Live Entries"},{key:"journal",label:"Journal"},{key:"world",label:"World Building"},{key:"tally",label:"The Spoils"}],_CHR2_GM_TABS_ZERO=[{key:"backgrounds",label:"Backgrounds"},{key:"world",label:"World Building"},{key:"lines",label:"Lines & Veils"},{key:"transcript",label:"Transcript"}];function _chr2GmTabsFor(e){return e&&e.is_session_zero?_CHR2_GM_TABS_ZERO:_CHR2_GM_TABS_REGULAR}let _chr2GmDrawerSid=null,_chr2DeletedFromDrawer=!1;function _chr2EnsureGmDrawer(){let e=document.getElementById("chr2-gm-drawer");if(e)return e;e=document.createElement("div"),e.id="chr2-gm-drawer",e.className="
891chr2-gm-drawer hidden",e.innerHTML=`<div class="chr2-gm-scrim" data-gm-close></div>
892    <aside class="chr2-gm-panel" role="dialog" aria-modal="true" aria-label="Session tools">
893      <header class="chr2-gm-hdr">
894        <span class="chr2-gm-title">Session Tools</span>
895        <button type="button" class="chr2-gm-close" data-gm-close aria-label="Close">&times;</button>
896      </header>
897      <nav class="chr2-gm-tabs"></nav>
898      <div class="chr2-gm-body" id="chr2-gm-body"></div>
899      <footer class="chr2-gm-foot">
900        <button type="button" class="btn-ghost btn-xs" id="chr2-gm-merge" title="Fold this session together with another \u2014 for a night that got split across two recordings"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6c6 0 6 6 12 6"/><path d="M4 18c6 0 6-6 12-6"/><path d="M13 9l3 3-3 3"/></svg> Merge sessions</button>
901        <button type="button" class="btn-ghost btn-xs btn-ghost--danger" id="chr2-gm-delete" title="Permanently delete this session and its chapter">&#128465; Delete session</button>
902      </footer>
903    </aside>`,document.body.appendChild(e),e.querySelectorAll("[data-gm-close]").forEach(n=>n.addEventListener("click",_chr2CloseGmDrawer)),e.querySelector("#chr2-gm-delete")?.addEventListener("click",_chr2DeleteSession),e.querySelector("#chr2-gm-merge")?.addEventListener("click",_chr2OpenMergeSessions);const t=e.querySelector(".chr2-gm-body");return t&&(t.addEventListener("click",_spkDismissHandler),t.addEventListener("change",_spkRelabelHandler)),document.addEventListener("keydown",n=>{n.key==="Escape"&&!n.__tfModalEsc&&!_tfTopModal()&&e&&!e.classList.contains("hidden")&&_chr2CloseGmDrawer()}),e}function _chr2GmRenderTabs(e,t){const n=(allSessions||[]).find(i=>i.id===t),a=_chr2GmTabsFor(n),s=e.querySelector(".chr2-gm-tabs");return s&&(s.innerHTML=a.map((i,o)=>`<button type="button" class="chr2-gm-tab${o===0?" on":""}" data-gm-tab="${i.key}">${escHtml(i.label)}</button>`).join(""),s.querySelectorAll(".chr2-gm-tab").forEach(i=>i.addEventListener("click",()=>{s.querySelectorAll(".chr2-gm-tab").forEach(o=>o.classList.toggle("on",o===i)),_chr2GmRenderSection(t,i.dataset.gmTab)}))),a}function _chr2OpenGmDrawer(e){if(!_isGm())return;_chr2GmDrawerSid=e;const t=_chr2EnsureGmDrawer(),n=_chr2GmRenderTabs(t,e);_chr2GmRenderSection(e,n[0].key),t.classList.remove("hidden"),requestAnimationFrame(()=>t.classList.add("open"))}let _chr2MergePick=null;function _chr2MergeFmtDate(e){if(!e)return"";const t=_chr2SessionNum(e),n=e.is_session_zero?"Prologue":t?"Ch. "+_chr2Roman(t):"",a=e.date?new Date(e.date+"T00:00:00").toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"}):"No date";return[n,a].filter(Boolean).join(" \xB7 ")}function _chr2MergeRank(e){return[e.id]}function _chr2MergeEarlier(e,t){const n=_chr2MergeRank(e),a=_chr2MergeRank(t);for(let s=0;s<n.length;s++)if(n[s]!==a[s])return n[s]<a[s]?e:t;return e}function _chr2OpenMergeSessions(){const e=_chr2GmDrawerSid,t=(allSessions||[]).find(o=>o.id===e),n=document.getElementById("chr2-merge-modal");if(!t||!n)return;_chr2MergePick=null;const a=n.querySelector("#chr2-merge-list"),s=(allSessions||[]).filter(o=>o.id!==e);a.innerHTML=s.length?s.map(o=>`<button type="button" class="tf-card tf-card--sm chr2-merge-row" data-sid="${o.id}">
904        <span class="chr2-merge-row-title">${escHtml(o.title||"Untitled session")}</span>
905        <span class="chr2-merge-row-meta">${escHtml(_chr2MergeFmtDate(o))}</span>
906      </button>`).join(""):'<p class="placeholder--padded">This campaign has no other session to merge with.</p>',n.querySelector("#chr2-merge-picker").classList.remove("hidden"),n.querySelector("#chr2-merge-summary").classList.add("hidden"),n.querySelector("#chr2-merge-back").classList.add("hidden");const i=n.querySelector("#chr2-merge-confirm");i.disabled=!0,i.textContent="Merge Sessions",a.querySelectorAll(".chr2-merge-row").forEach(o=>o.addEventListener("click",()=>{_chr2MergeShowSummary(t,s.find(r=>r.id===+o.dataset.sid))})),n.classList.remove("hidden")}function _chr2MergeShowSummary(e,t){const n=document.getElementById("chr2-merge-modal");if(!n||!t)return;const a=_chr2MergeEarlier(e,t),s=a===e?t:e;_chr2MergePick={keepId:a.id,dropId:s.id,fromId:e.id},n.querySelector("#chr2-merge-keep-title").textContent=a.title||"Untitled session",n.querySelector("#chr2-merge-keep-meta").textContent=_chr2MergeFmtDate(a),n.querySelector("#chr2-merge-drop-title").textContent=s.title||"Untitled session",n.querySelector("#chr2-merge-drop-meta").textContent=_chr2MergeFmtDate(s),n.querySelector("#chr2-merge-explain").textContent=`${s.title||"That session"}'s transcript is appended to ${a.title||"the survivor"}, in order. Live entries, journal notes, world building, quotes, and The Spoils all carry over. The recap is cleared so you can regenerate one clean chapter \u2014 the old one stays restorable.`;const i=[];
906if((e.is_session_zero||t.is_session_zero)&&i.push("One of these is Session Zero \u2014 merging folds character-creation talk into a game chapter."),e.date&&t.date){const o=Math.abs(new Date(e.date)-new Date(t.date))/864e5;o>=1&&i.push(`These sessions are dated ${Math.round(o)} day${Math.round(o)===1?"":"s"} apart \u2014 check this is really one night.`)}n.querySelector("#chr2-merge-warnings").innerHTML=i.map(o=>`<p class="form-helper-text form-helper-text--warn">${escHtml(o)}</p>`).join(""),n.querySelector("#chr2-merge-picker").classList.add("hidden"),n.querySelector("#chr2-merge-summary").classList.remove("hidden"),n.querySelector("#chr2-merge-back").classList.remove("hidden"),n.querySelector("#chr2-merge-confirm").disabled=!1}function _chr2CloseMergeModal(){document.getElementById("chr2-merge-modal")?.classList.add("hidden"),_chr2MergePick=null}async function _chr2MergeConfirm(){const e=document.getElementById("chr2-merge-modal"),t=_chr2MergePick;if(!e||!t)return;const n=e.querySelector("#chr2-merge-confirm");n.disabled=!0,n.textContent="Merging\u2026";const a=currentCampaign&&currentCampaign.id;try{const s=await api("POST",`/api/campaigns/${a}/sessions/${t.keepId}/merge`,{absorb_session_id:t.dropId});_chr2CloseMergeModal(),_chr2CloseGmDrawer(),_campaignStale(a)||(showToast(`Merged into ${s.title||"the earlier session"}. Regenerate its recap when you're ready.`,5e3),delete sessionTallyCache[t.dropId],delete sessionTallyCache[t.keepId],delete sessionEntries[t.dropId],delete sessionEntries[t.keepId],await loadSessions(),typeof renderChronicleTimeline=="function"&&renderChronicleTimeline())}catch(s){n.disabled=!1,n.textContent="Merge Sessions",showToast("Merge failed: "+(s.message||"try again"),5e3)}}function _chr2CloseGmDrawer(){const e=document.getElementById("chr2-gm-drawer");e&&(e.classList.remove("open"),setTimeout(()=>e.classList.add("hidden"),240))}function _chr2DeleteSession(){const e=_chr2GmDrawerSid;if(!e)return;const t=(allSessions||[]).find(a=>a.id===e),n=t&&(t.title||t.name)||"this session";_chr2DeletedFromDrawer=!0,_chr2CloseGmDrawer(),setTimeout(()=>_openSessionDeleteModal(e,n),260)}async function _tfSessionTranscript(e,t){const n=(allSessions||[]).find(a=>a.id===e);if(n&&typeof n.raw_transcript=="string")return n.raw_transcript;if(!currentCampaign||!e)return"";try{const a=await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${e}`),s=a&&typeof a.raw_transcript=="string"?a.raw_transcript:"";return n&&(n.raw_transcript=s),s}catch(a){return console.error("[transcript] fetch failed for session",e,a),!(t&&t.quiet)&&typeof showToast=="function"&&showToast("Could not load this session\u2019s transcript. Check your connection and try again.",6e3),null}}async function _chr2GmRenderSection(e,t){const n=document.getElementById("chr2-gm-body");if(!n)return;const a=(allSessions||[]).find(s=>s.id===e);if(!a){n.innerHTML='<p class="chr2-gm-empty">Session not found.</p>';return}if(t==="transcript"&&typeof a.raw_transcript!="string"&&(n.innerHTML='<p class="chr2-gm-empty">Loading transcript\u2026</p>',await _tfSessionTranscript(e)===null)){n.innerHTML='<p class="chr2-gm-empty">Transcript unavailable \u2014 try reopening.</p>';return}if(t==="transcript"){const s=a.raw_transcript||"",i=!!a.is_live_transcript,o=/^\[\d+:\d+\] [^:\n]+:/m.test(s)||/^[A-H]: \S/m.test(s),r=i&&s.trim()?`<button class="btn-ghost btn-xs transcript-unlock-btn" data-sid="${e}" title="Edit transcript">\u270E Edit</button>`:"",c=currentUser&&currentUser.is_admin?`<button class="btn-ghost btn-xs transcript-replace-btn" data-sid="${e}" title="Replace transcript by uploading an audio file (admin only)">
906<span class="admin-pip">Admin</span>\u2191 Re-transcribe</button><input type="file" class="transcript-replace-input hidden" data-sid="${e}" accept=".m4a,.mp3,.mp4,.wav,.ogg,.webm,audio/*">`:"",l=currentUser&&currentUser.is_admin&&s.trim()?`<button class="btn-ghost btn-xs extract-quotes-btn" data-sid="${e}" title="Re-extract quotes only: skips the full recap + the unchanged-transcript dedupe (admin only)"><span class="admin-pip">Admin</span>\u275D Extract Quotes</button>`:"",d=i?`<pre class="session-section-pre session-live-transcript" data-sid="${e}">${escHtml(s)}</pre>`:`<textarea class="session-section-box" data-id="${e}" data-key="raw_transcript" placeholder="Session notes or transcript\u2026">${escHtml(s)}</textarea>`,u=(i||o)&&s.trim()?_buildSpeakerBar(e,s):"";let p="";if(a.summary){const g=_isRecapInProgress(a),h=_recapGate(a),v=g?'disabled title="Recap is generating\u2026"':h.hasTx?_recapUpToDateLocal[e]?'disabled title="Already up to date, edit the transcript above to enable."':"":`disabled title="${escHtml(h.tip)}"`,b=g?"\u23F3 Generating\u2026":"\u21BA Regenerate Recap",y=!!(a.title&&a.title.trim()),_=!!_campDiscordConfig?.recap_channel_id,$=`<button type="button" class="btn-ghost btn-xs btn-publish-discord" data-id="${e}" ${y?"":"disabled"} title="${escHtml(y?_?"Publish this recap to your Discord recap channel":"Configure a Discord recap channel in Manage \u2192 Discord first":"Add a chapter title before publishing: untitled posts read as system noise")}">\u{1F517} Publish to Discord</button>`,E=a.has_prev_recap||a.prev_recap?`<button type="button" class="btn-ghost btn-xs chr2-gm-restore-btn" data-sid="${e}" title="Bring back the previous recap: the chapter this regenerate replaced">\u21B6 Restore previous</button>`:"",k=`<button type="button" class="btn-ghost btn-xs chr2-gap-fill-btn" data-sid="${e}" ${g?'disabled title="Recap is generating\u2026"':'title="Run only the recap steps that were skipped or cut short (long sessions): codex updates, quest sync, without rewriting the chapter."'}>Fill Gaps</button>`;let C="";if(!a.discord_parent_message_id&&a.recap_autopost_status==="pending"){let T="soon";try{a.recap_autopost_at&&(T=new Date(a.recap_autopost_at).toLocaleString([],{weekday:"short",hour:"numeric",minute:"2-digit"}))}catch{}C=`<span class="chr2-autopost-chip" data-sid="${e}">Auto-posts to Discord \xB7 ${escHtml(T)}
907          <button type="button" class="btn-ghost btn-xs autopost-now-btn" data-sid="${e}" title="Post the chapter to your recap channel now instead of waiting">Post now</button>
908          <button type="button" class="btn-ghost btn-xs autopost-hold-btn" data-sid="${e}" title="Cancel the scheduled auto-post \u2014 the Publish button still works any time">Hold</button></span>`}else!a.discord_parent_message_id&&a.recap_autopost_status==="held"&&(C=`<span class="chr2-autopost-chip chr2-autopost-chip--held" data-sid="${e}">Auto-post held</span>`);const S=a.qotn&&(a.qotn.quote||"").trim()?`<button type="button" class="btn-ghost btn-xs qotn-drawer-btn" data-qotn-winner="${a.qotn.quote_id}" title="Open the winning quote card">${_QOTN_SVG_TROPHY}<span>Quote of the Night winner</span></button>`:`<button type="button" class="btn-ghost btn-xs qotn-drawer-btn" data-qotn-open="${e}" title="Pick the lines and send your table a vote">${_QOTN_SVG_TROPHY}<span>Quote of the Night</span></button>`,q=_chapterBuildState(a,_isGm()),I=q==="ready"||q==="stale"?`<button type="button" class="btn-ghost btn-xs chr2-chapter-build-btn" data-sid="${e}" ${g?"disabled":""}>${_CHAPTER_BUILD_LABEL}</button>`:"";p=`<div class="chr2-gm-regen">
909        <button type="button" class="btn-ghost btn-xs chr2-gm-regen-btn" data-sid="${e}" ${v}>${b}</button>
910        ${I}
911        ${k}
912        ${$}
913        ${S}
914        ${C}
915        ${E}
916        <span class="chr2-gm-regen-status"></span>
917        <p class="form-helper-text">Re-runs the AI over this transcript and overwrites the current chapter prose.</p>
918      </div>`}const m=(a.test_transcript||"").split(`
919`).filter(g=>g.trim()!==_TEST_LABEL).join(`
920`).trim(),f=m?`<div class="session-test-recording">
921        <div class="session-test-recording-hdr">
922          <span class="form-section-label">Test recording <span class="session-test-note">not included in recaps</span></span>
923          ${i||!a.last_recap_at?`<button type="button" class="btn-ghost btn-xs test-promote-btn" data-sid="${e}" title="Add this test recording into the session so your recap includes it">Add to session</button>`:""}
924        </div>
925        <pre class="session-section-pre session-test-transcript">${escHtml(m)}</pre>
926      </div>`:"";n.innerHTML=`<div class="session-section chr2-gm-transcript-section">${u}${d}${r}${c}${l}</div>${f}${p}`,_chr2GmWireTranscript(n,e)}else t==="live"?_chr2GmRenderLive(n,e):t==="journal"||t==="world"||t==="backgrounds"?_chr2GmRenderMulti(n,e,t):t==="lines"?_chr2GmRenderLinesVeils(n,e):t==="tally"?_chr2GmRenderTally(n,e):n.innerHTML=`<p class="chr2-gm-empty">${escHtml(t)}: not available for this session.</p>`}function _chr2GmRenderLinesVeils(e,t){const n=currentCampaign?.id;e.innerHTML=`<div class="session-entries-list">
927    <div class="entry-item">
928      <span class="form-section-label">Table rules</span>
929      <textarea class="entry-box chr2-table-rules-box" rows="8"
930        placeholder="House rules, standing rulings, and safety procedures (X-card, communication norms, age requirements). Paste anything: \u2726 AI Sync sorts it."></textarea>
931    </div>
932    <div class="entry-item">
933      <span class="form-section-label">Lines &amp; veils</span>
934      <textarea class="entry-box chr2-lines-veils-box" rows="8"
935        placeholder="Content boundaries. Lines: never depicted. Veils: happens off-screen."></textarea>
936      <div class="entry-item-actions">
937        <button type="button" class="btn-ghost chr2-lv-save-btn">Save</button>
938        <div class="entry-sync-group">
939          <button type="button" class="btn-ghost chr2-lv-sync-btn" title="One sync over both boxes plus anything the table agreed on-mic: rules and procedures land in Table Rules, content boundaries in Lines &amp; Veils; characters and scheduling are dropped">\u2726 AI Sync</button>
940        </div>
941      </div>
942      <p class="form-helper-text">Set at session zero, pinned to your GM Notebook's Table Rules card at every session. Players never see this page.</p>
943    </div>
944  </div>`;const a=e.querySelector(".chr2-lines-veils-box"),s=e.querySelector(".chr2-table-rules-box"),i=async()=>_campaignStale(n)?!1:(await api("PUT",`/api/campaigns/${n}/table-rules`,{lines_veils:a.value,house_rules:s.value}),_tableRulesData&&(_tableRulesData.lines_veils=a.value,_tableRulesData.house_rules=s.value),!0);e.querySelector(".chr2-lv-save-btn")?.addEventListener("click",async()=>{try{await i(),showToast("Saved.")}catch(o){showToast("Save failed: "+(o&&o.message||o),4e3)}}),e.querySelector(".chr2-lv-sync-btn")?.addEventListener("click",async o=>{const r=o.currentTarget;r.disabled=!0,r.textContent="\u2726 Syncing\u2026";try{await i();const c=await api("POST",`/api/campaigns/${n}/table-rules/sync-lines-veils`,{session_id:t});if(_campaignStale(n))return;a.value=c.lines_veils||"",s.value=c.house_rules||"",_tableRulesData&&(_tableRulesData.lines_veils=a.value,_tableRulesData.house_rules=s.value),typeof _renderNotebookRules=="function"&&_renderNotebookRules(),showToast("Distilled into Table Rules and Lines & Veils.")}catch(c){showToast(c&&c.message||"Nothing to distill yet.",5e3)}r.disabled=!1,r.textContent="\u2726 AI Sync"}),(async()=>{try{const o=await api("GET",`/api/campaigns/${n}/table-rules`);if(_campaignStale(n))return;a.value=o.lines_veils||"",s.value=o.house_rules||"",_tableRulesData=o,_tableRulesLoadedCid=n}catch{}}
944)(),[a,s].forEach(o=>o.addEventListener("blur",async()=>{if(!_campaignStale(n))try{await i(),typeof _renderNotebookRules=="function"&&_renderNotebookRules()}catch(r){showToast("Save failed: "+(r&&r.message||r),4e3)}}))}function _chr2GmWireTranscript(e,t){const n=s=>s.addEventListener("blur",async()=>{const i=(allSessions||[]).find(r=>r.id===t),o=s.dataset.key||"raw_transcript";if(!(!i||s.value===(i[o]||"")))try{const r=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}`,{[o]:s.value||null});allSessions=(allSessions||[]).map(c=>c.id===t?r:c)}catch(r){showToast("Save failed: "+(r&&r.message||r),4e3)}});e.querySelectorAll(".session-section-box").forEach(n),e.querySelectorAll(".transcript-unlock-btn").forEach(s=>s.addEventListener("click",async i=>{i.stopPropagation();const o=(allSessions||[]).find(l=>l.id===t),r=s.closest(".session-section").querySelector(".session-section-pre");if(!o||!r||typeof liveSession<"u"&&liveSession&&liveSession.id===t&&typeof confirmDialog=="function"&&!await confirmDialog({title:"This chapter is recording live",message:"The live recording is writing to this transcript: anything you type here can be overwritten on the next auto-save (every few seconds). Stop or pause recording first to edit safely. Edit anyway?",confirmLabel:"Edit anyway",cancelLabel:"Keep recording"}))return;const c=document.createElement("textarea");c.className="session-section-box",c.dataset.id=t,c.dataset.key="raw_transcript",c.value=o.raw_transcript||"",r.replaceWith(c),s.remove(),c.focus(),n(c)})),e.querySelectorAll(".transcript-replace-btn").forEach(s=>s.addEventListener("click",i=>{i.stopPropagation();const o=s.closest(".session-section").querySelector(".transcript-replace-input");o&&o.click()})),e.querySelectorAll(".transcript-replace-input").forEach(s=>s.addEventListener("change",async()=>{const i=s.files[0];if(!i)return;const o=s.closest(".session-section").querySelector(".transcript-replace-btn"),r=await _reTranscribeAudio(i,t,o);s.value="",r&&_chr2GmRenderSection(t,"transcript")})),e.querySelector(".extract-quotes-btn")?.addEventListener("click",async s=>{s.stopPropagation();const i=s.currentTarget,o=i.innerHTML;i.disabled=!0,i.textContent="\u275D Extracting\u2026";try{const r=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/extract-quotes`),c=r&&r.quotes||[];showToast(`Quotes re-extracted: ${c.length} on this chapter now.`,4500),typeof renderChronicleRecap=="function"&&renderChronicleRecap(t,{background:!0})}catch(r){showToast("Quote extraction failed: "+(r&&r.message||r),5e3)}finally{i.disabled=!1,i.innerHTML=o}});const a=e.querySelector(".qotn-drawer-btn");a&&a.dataset.qotnWinner?a.addEventListener("click",s=>{s.stopPropagation();const i=a.dataset.qotnWinner;_chr2CloseGmDrawer(),typeof _openQuoteCardModal=="function"&&_openQuoteCardModal(i)}):a&&(a.addEventListener("click",s=>{s.stopPropagation(),_qotnOpenModal(t)}),_qotnDecorateDrawerBtn(a,t)),e.querySelector(".chr2-gm-regen-btn")?.addEventListener("click",s=>{s.stopPropagation(),_fireRecapForSession(t,{btnEl:s.currentTarget,statusEl:e.querySelector(".chr2-gm-regen-status"),onComplete:()=>{typeof renderChronicleRecap=="function"&&renderChronicleRecap(t,{background:!0}),_chr2GmRenderSection(t,"transcript")}})}),e.querySelector(".chr2-chapter-build-btn")?.addEventListener("click",s=>{s.stopPropagation(),_fireRecapForSession(t,{from_chapter:!0,btnEl:s.currentTarget,statusEl:e.querySelector(".chr2-gm-regen-status"),onComplete:()=>{typeof renderChronicleRecap=="function"&&renderChronicleRecap(t,{background:!0}),_chr2GmRenderSection(t,"transcript")}})}),e.querySelector(".chr2-gap-fill-btn")?.addEventListener("click",s=>{s.stopPropagation(),_fireGapFillForSession(t,s.currentTarget,{onComplete:()=>{typeof renderChronicleRecap=="function"&&renderChronicleRecap(t,{background:!0})}})}),e.querySelector(".chr2-gm-restore-btn")?.addEventListener("click",async s=>{s.stopPropagation();const i=s.currentTarget,o=i.innerHTML;i.disabled=!0,i.textContent="\u21B6 Restoring\u2026";try{const r=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/recap/restore-previous`);r&&r.session&&(allSessions=(allSessions||[]).map(c=>c.id===t?{...c,...r.session}:c)),showToast("Previous recap restored.",4e3),typeof renderChronicleRecap=="function"&&renderChronicleRecap(t,{background:!0}),_chr2GmRenderSection(t,"transcript")}catch(r){showToast("Restore failed: "+(r&&r.message||r),5e3),i.disabled=!1,i.innerHTML=o}})}function _leBodyHtml(e,t){const n=t==="loading",a=Array.isArray(t)?t:null,s={note:{icon:"\u{1F4DD}",label:"Note",cls:"le-note"},key_event:{icon:"\u2B50",label:"Key",cls:"le-key"},travel:{icon:"\u{1F6B6}",label:"Travel",cls:"le-travel"},gif:{icon:"\u{1F39E}",label:"GIF",cls:"le-gif"},system_event:{icon:"\u2726",label:"Story",cls:"le-story"},combat_event:{icon:"\u2694",label:"Combat",cls:"le-combat"},context_event:{icon:"\u{1F9E0}",label:"Context",cls:"le-context"}},i=_isGm()||!!currentUser?.is_admin;return n?'<p class="le-empty">Loading\u2026</p>':!a||a.length===0?'<p class="le-empty">No live entries recorded for this session.</p>':`<div class="le-list">${a.map(o=>{const r=o.timestamp_seconds||0,c=String(Math.floor(r/3600)).padStart(2,"0"),l=String(Math.floor(r%3600/60)).padStart(2,"0"),d=String(r%60).padStart(2,"0"),u=s[o.tag]||s.note,p=o.tag==="gif",m=p?`<img class="live-log-gif" src="${escHtml(o.content)}" alt="gif" loading="lazy">`:escHtml(o.content),f=i?`<span class="le-actions">
945           ${p?"":`<button class="le-edit-btn btn-ghost btn-xs" data-sid="${e}" data-eid="${o.id}" title="Edit">\u270E</button>`}
946           <button class="le-del-btn btn-ghost btn-ghost--danger btn-xs" data-sid="${e}" data-eid="${o.id}" title="Delete">\u2715</button>
947         </span>`:"";return`<div class="le-row" data-eid="${o.id}">
948      <span class="le-ts">${c}:${l}:${d}</span>
949      <span class="le-tag ${u.cls}" title="${u.label}">${u.icon}</span>
950      <span class="le-content" data-eid="${o.id}">${m}</span>
951      ${f}
952    </div>`}).join("")}</div>`}function _chr2GmRenderLive(e,t){const n=liveEntriesCache[t];if(n==null){liveEntriesCache[t]="loading",e.innerHTML='<p class="chr2-gm-empty">Loading\u2026</p>',api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${t}/log-entries`).then(a=>{liveEntriesCache[t]=a,_chr2GmDrawerSid===t&&_chr2GmRenderLive(e,t)}).catch(()=>{delete liveEntriesCache[t],e.innerHTML='<p class="chr2-gm-empty">Couldn\u2019t load live entries.</p>'});return}e.innerHTML=`<div class="le-panel">${_leBodyHtml(t,n)}</div>`,_chr2GmWireLive(e,t)}function _chr2GmWireLive(e,t){const n=()=>{_chr2GmDrawerSid===t&&_chr2GmRenderSection(t,"live")};e.querySelectorAll(".le-edit-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation();const i=parseInt(a.dataset.eid),o=a.closest(".le-row"),r=o?.querySelector(".le-content");if(!o||!r)return;const c=(liveEntriesCache[t]||[]).find(m=>m.id===i);if(!c)return;const l=document.createElement("textarea");l.className="le-edit-box",l.value=c.content||"",l.rows=Math.max(2,Math.min(8,Math.ceil((c.content||"").length/80)));const d=r.innerHTML;r.replaceWith(l),a.disabled=!0,l.focus(),l.setSelectionRange(l.value.length,l.value.length);let u=!1;const p=()=>{if(u)return;u=!0;const m=document.createElement("span");m.className="le-content",m.dataset.eid=i,m.innerHTML=d,l.replaceWith(m),a.disabled=!1};l.addEventListener("keydown",m=>{m.key==="Escape"&&(m.preventDefault(),p())}),l.addEventListener("blur",async()=>{if(u)return;const m=l.value;if(m===(c.content||"")){p();return}u=!0;try{const f=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}/log-entries/${i}`,{content:m}),g=liveEntriesCache[t];if(Array.isArray(g)){const h=g.findIndex(v=>v.id===i);h>=0&&(g[h]={...g[h],...f})}showToast("\u270E Beat updated"),n()}catch(f){showToast("Save failed: "+f.message),p()}})})}),e.querySelectorAll(".le-del-btn").forEach(a=>{a.addEventListener("click",async s=>{s.stopPropagation();const i=parseInt(a.dataset.eid),o=(liveEntriesCache[t]||[]).find(c=>c.id===i),r=o?(o.content||"").slice(0,80):"";if(await confirmDialog({title:"Delete Beat",message:`Delete this beat?
953
954${r}${r.length===80?"\u2026":""}`,confirmLabel:"Delete"})){a.disabled=!0;try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/sessions/${t}/log-entries/${i}`);const c=liveEntriesCache[t];Array.isArray(c)&&(liveEntriesCache[t]=c.filter(l=>l.id!==i)),showToast("\u2715 Beat deleted"),n()}catch(c){a.disabled=!1,showToast("Delete failed: "+c.message)}}})})}function _multiBodyHtml(e,t,n){const a=(sessionEntries[e]||[]).filter(d=>d.section===t),s=t==="journal",i=t==="background",o=allEntities.filter(d=>d.type==="pc"),r=(d,u)=>{if(!s&&!i)return"";const p=i?'<option value="__new__">+ Create new character\u2026</option>':"",m=i?'<input type="text" class="entry-new-pc-name hidden" placeholder="Character name\u2026" aria-label="New character name" maxlength="60" autocomplete="off" />':"";return`<select class="${u}" required>
955      <option value="">(select PC)</option>
956      ${o.map(f=>`<option value="${f.id}"${f.id===d?" selected":""}>${f.name}</option>`).join("")}
957      ${p}
958    </select>${m}`},c=d=>{if(!i)return"";const u=(_rosterPcs||[]).find(f=>f.entity_id===d),p=u?u.assigned_user_id:null;return`<select class="entry-owner-select" title="The player account that owns this character. They and you (GM) keep seeing the backstory even while it's sealed from the rest of the table; everyone else can't, until you reveal it. Leave blank if there's no account for this PC yet.">
959      <option value="">(owner account, optional)</option>${(_rosterMembers||[]).filter(f=>f.role!=="observer").map(f=>`<option value="${f.user_id}"${f.user_id===p?" selected":""}>${escHtml(f.display_name||f.email)}${f.role==="gm"?" (GM)":""}</option>`).join("")}
960    </select>`},l=()=>i?`<label class="checkbox-label gm-portal--violet entry-seal-label" title="Keep this backstory private to you and the assigned player until it comes up in play. The character's Codex entry stays visible: only the backstory prose is restricted (other players see no backstory, not a 'hidden' placeholder). Any NEW people or places the backstory names are also hidden until revealed."><input type="checkbox" class="entry-seal-chk"><span class="gm-portal"><span class="gm-portal-ico" aria-hidden="true">${_CX2_ACT_ICO.book}</span><span class="gm-portal-txt">Restricted to GM + PC</span></span></label>`:`<label class="checkbox-label gm-portal--gold entry-stage-label" title="Hide any NEW entities this 
960sync creates from players until you reveal them. Entities already visible to players are not affected."><input type="checkbox" class="entry-stage-chk"><span class="gm-portal"><span class="gm-portal-ico" aria-hidden="true">${_CX2_ACT_ICO.eyeClosed}</span><span class="gm-portal-txt">Stage for Reveal</span></span></label>`;return`<div class="session-entries-list">
961    ${a.map(d=>`
962      <div class="entry-item" data-entry-id="${d.id}">
963        ${s||i?`<div class="entry-pc-row">${r(d.pc_id,"entry-pc-select")}${c(d.pc_id)}</div>`:""}
964        <textarea class="entry-box" data-entry-id="${d.id}">${d.content}</textarea>
965        <div class="entry-item-actions">
966          <button class="entry-save-btn btn-ghost" data-entry-id="${d.id}" data-sid="${e}" data-section="${d.section}">Save</button>
967          <div class="entry-sync-group">
968            <button class="entry-compendium-btn btn-ghost" data-entry-id="${d.id}" data-sid="${e}" data-section="${d.section}" title="Send to AI Compendium">\u2726 AI Sync</button>
969            ${l()}
970          </div>
971          <button class="entry-delete-btn btn-ghost btn-ghost--danger" data-entry-id="${d.id}" data-sid="${e}">Delete</button>
972        </div>
973      </div>
974    `).join("")}
975    <div class="entry-item entry-new">
976      ${s||i?`<div class="entry-pc-row">${r(null,"entry-new-pc-select")}</div>`:""}
977      <textarea class="entry-new-box" placeholder="${n}"></textarea>
978      <div class="entry-item-actions">
979        <button class="entry-add-btn btn-new" data-sid="${e}" data-section="${t}">+ Add Entry</button>
980        <button type="button" class="entry-upload-btn btn-ghost btn-sm" title="Bring in .txt, .md or .docx files. Each one becomes its own entry.">Upload notes</button>
981        <input type="file" class="entry-upload-input hidden" multiple accept=".txt,.md,.markdown,.docx" aria-hidden="true" tabindex="-1">
982        <button type="button" class="entry-gdoc-btn btn-ghost btn-sm" title="Take a one-time snapshot of a public Google Doc">Google Doc</button>
983      </div>
984      <div class="entry-gdoc-row hidden">
985        <input type="text" class="entry-gdoc-input" placeholder="Paste a public Google Docs link" aria-label="Public Google Docs link">
986        <button type="button" class="entry-gdoc-go btn-new btn-sm">Bring it in</button>
987      </div>
988      <p class="form-helper-text entry-upload-note">Word, text or Markdown. Each file becomes its own entry, so you can assign and sync them one at a time. We don&rsquo;t keep the file. Only this text stays.</p>
989    </div>
990  </div>`}function _chr2GmRenderMulti(e,t,n){const a=n==="journal"?"journal":n==="backgrounds"?"background":"world_building",s=n==="journal"?"In-character journal or narrative entry\u2026":n==="backgrounds"?"Character backgrounds and backstories\u2026":"World lore, lore drops, setting details\u2026";if(!sessionEntries[t]){e.innerHTML='<p class="chr2-gm-empty">Loading\u2026</p>',loadSessionEntries(t).then(()=>{_chr2GmDrawerSid===t&&_chr2GmRenderMulti(e,t,n)});return}if(n==="backgrounds"&&!(_rosterMembers&&_rosterMembers.length)){e.innerHTML='<p class="chr2-gm-empty">Loading\u2026</p>',Promise.all([api("GET",`/api/campaigns/${currentCampaign.id}/roster`).catch(()=>null),api("GET",`/api/campaigns/${currentCampaign.id}/members`).catch(()=>null)]).then(([i,o])=>{i&&(_rosterPcs=i),o&&(_rosterMembers=o),_chr2GmDrawerSid===t&&_chr2GmRenderMulti(e,t,n)});return}e.innerHTML=_multiBodyHtml(t,a,s),_chr2GmWireMulti(e,t,n),applyCompendiumDoneStates(e)}async function _createPcFromEntry(e){const t=e.querySelector(".entry-new-pc-name"),n=(t&&t.value||"").trim();if(!n)return t&&t.focus(),showToast("Name the new character first."),null;const a=(allEntities||[]).find(s=>s.type==="pc"&&(s.name||"").trim().toLowerCase()===n.toLowerCase());if(a)return showToast(`\u201C${a.name}\u201D already exists, attaching the backstory to them.`),a.id;try{const s=await api("POST",`/api/campaigns/${currentCampaign.id}/entities`,{type:"pc",name:n});return allEntities.find(i=>i.id===s.id)||allEntities.push(s),showToast(`\u2728 Created ${s.name}`),s.id}catch(s){return showToast("Could not create the character: "+s.message,5e3),null}}async function _wireEntryUpload(e,t,n,a){const s=n==="journal"?"journal":n==="backgrounds"?"background":"world_building",i=e.querySelector(".entry-upload-btn"),o=e.querySelector(".entry-upload-input"),r=e.querySelector(".entry-gdoc-btn"),c=e.querySelector(".entry-gdoc-row"),l=e.querySelector(".entry-gdoc-input"),d=e.querySelector(".entry-gdoc-go");if(!i||!o)return;const u=async(p,m,f)=>{const g=m?m.textContent:null;m&&(m.disabled=!0,m.textContent=f);try{const h=await fetch(`/api/campaigns/${currentCampaign.id}/sessions/${t}/entries/upload`,{method:"POST",headers:{Authorization:`Bearer ${localStorage.getItem("tf_jwt")}`},body:p});if(h.status===401)throw setStoredToken(null),currentRole=null,showAuthView("login"),showScreen("auth-screen"),new Error("Session expired, please log in again.");const v=await h.json().catch(()=>null);
990if(!h.ok)throw new Error(v&&v.detail||"That could not be brought in.");const b=(v.created||[]).length,y=v.failed||[];b&&(await loadSessionEntries(t),a(),showToast(b===1?"Pulled in. Review it below, then run AI Sync.":`Pulled in ${b} entries. Review them below, then run AI Sync.`)),y.forEach((_,w)=>setTimeout(()=>showToast(`${_.name}: ${_.error}`,6e3),400*(w+1))),!b&&!y.length&&showToast("There was nothing to bring in.")}catch(h){showToast(h.message||"That could not be brought in.",5e3)}finally{m&&(m.disabled=!1,m.textContent=g)}};if(i.addEventListener("click",()=>o.click()),o.addEventListener("change",async()=>{const p=[...o.files||[]];if(!p.length)return;const m=new FormData;m.append("section",s),p.forEach(f=>m.append("files",f)),await u(m,i,"Reading\u2026"),o.value=""}),r&&c&&l&&d){r.addEventListener("click",()=>{c.classList.toggle("hidden"),c.classList.contains("hidden")||l.focus()});const p=async()=>{const m=(l.value||"").trim();if(!m){l.focus();return}const f=new FormData;f.append("section",s),f.append("gdoc",m),await u(f,d,"Reading\u2026"),l.value=""};d.addEventListener("click",p),l.addEventListener("keydown",m=>{m.key==="Enter"&&(m.preventDefault(),p())})}}function _chr2GmWireMulti(e,t,n){const a=()=>{_chr2GmDrawerSid===t&&_chr2GmRenderSection(t,n)};_wireEntryUpload(e,t,n,a),e.querySelectorAll(".entry-pc-select, .entry-new-pc-select").forEach(o=>{o.addEventListener("change",()=>{const r=o.parentElement&&o.parentElement.querySelector(".entry-new-pc-name");if(!r)return;const c=o.value==="__new__";r.classList.toggle("hidden",!c),c&&r.focus()})}),e.querySelectorAll(".entry-new-pc-name").forEach(o=>{o.addEventListener("keydown",r=>{if(r.key==="Enter"){r.preventDefault();const c=o.closest(".entry-item");(c&&(c.querySelector(".entry-add-btn")||c.querySelector(".entry-save-btn"))||null)?.click()}else if(r.key==="Escape"){const c=o.parentElement&&o.parentElement.querySelector("select");c&&(c.value=""),o.classList.add("hidden"),c&&c.focus()}})}),e.querySelectorAll(".entry-save-btn").forEach(o=>{o.addEventListener("click",async r=>{r.stopPropagation();const c=parseInt(o.dataset.entryId),l=o.closest(".entry-item"),d=l.querySelector(".entry-box"),u=l.querySelector(".entry-pc-select"),p=d.value.trim();if(!p)return;let m;if(u&&u.value==="__new__"){if(m=await _createPcFromEntry(l),m==null)return}else if(m=u&&parseInt(u.value)||null,u&&!m){u.focus(),alert("Please select a PC for this entry.");return}try{const f=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}/entries/${c}`,{content:p,pc_id:m});sessionEntries[t]=(sessionEntries[t]||[]).map(g=>g.id===c?f:g),clearCompendiumDone(c),a()}catch(f){alert("Save failed: "+f.message)}})}),e.querySelectorAll(".entry-compendium-btn").forEach(o=>{o.addEventListener("click",async r=>{r.stopPropagation();const c=o.dataset.section,l=o.closest(".entry-item"),d=l.querySelector(".entry-box"),u=l.querySelector(".entry-pc-select"),p=d.value.trim();if(!p)return;let m;if(u&&u.value==="__new__"){if(m=await _createPcFromEntry(l),m==null)return}else m=u&&parseInt(u.value)||null;o.disabled=!0,o.textContent="\u2726 Working\u2026";const f=o.dataset.entryId||`${t}-${c}`,g={text:p};if(m&&(c==="background"||c==="journal")&&(g.pc_id=m),c==="background"){const h=l.querySelector(".entry-owner-select");if(m&&h){const b=parseInt(h.value)||null,y=(_rosterPcs||[]).find(w=>w.entity_id===m),_=y?y.assigned_user_id:null;if(b!==_)try{await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${m}/owner`,{user_id:b}),y&&(y.assigned_user_id=b)}catch(w){showToast("\u26A0 Could not set owner account: "+w.message,5e3)}}const v=l.querySelector(".entry-seal-chk");v&&v.checked&&(g.seal_backstory=!0)}else{const h=l.querySelector(".entry-stage-chk");h&&h.checked&&(g.stage_new=!0)}try{const h=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/apply-world-building`,g),v=h.created||[],b=h.updated||[],y=h.crosslinked||[];for(const w of b)allEntities=allEntities.map($=>$.id===w.id?w:$);for(const w of y)allEntities=allEntities.map($=>$.id===w.id?w:$);for(const w of v)allEntities.find($=>$.id===w.id)||allEntities.push(w);const _=[];v.length&&_.push(`\u2728 Created ${v.length} ${v.length===1?"entry":"entries"}${g.stage_new||g.seal_backstory?", hidden until revealed":""} (${v.map(w=>w.name).join(", ")})`),b.length&&_.push(`\u{1F517} Updated ${b.length} ${b.length===1?"entry":"entries"}`),g.seal_backstory&&_.push("\u{1F512} Backstory kept private to you + the assigned player, reveal it later from the Codex"),y.length&&_.push(`\u2194 Crosslinked ${y.length}`),h.enrich_error&&_.push("\u26A0 AI enrichment partially unavailable"),showToast(_.join(" \xB7 ")||"\u2713 Synced with compendium.",_.length?8e3:3e3),o.classList.add("compendium-btn-done"),o.textContent="\u2705 AI Sync",markCompendiumDone(f)}
990catch(h){showToast(`\u26A0 Compendium sync failed: ${h.message}`,6e3),o.textContent="\u2726 AI Sync"}finally{o.disabled=!1}})});const s="tf_stagereveal_"+(currentCampaign?currentCampaign.id:"x");e.querySelectorAll(".entry-stage-chk").forEach(o=>{try{o.checked=localStorage.getItem(s)==="1"}catch{}o.onchange=()=>{try{localStorage.setItem(s,o.checked?"1":"0")}catch{}e.querySelectorAll(".entry-stage-chk").forEach(r=>{r.checked=o.checked})}});const i="tf_sealbackstory_"+(currentCampaign?currentCampaign.id:"x");e.querySelectorAll(".entry-seal-chk").forEach(o=>{try{const r=localStorage.getItem(i);o.checked=r===null?!0:r==="1"}catch{o.checked=!0}o.onchange=()=>{try{localStorage.setItem(i,o.checked?"1":"0")}catch{}e.querySelectorAll(".entry-seal-chk").forEach(r=>{r.checked=o.checked})}}),e.querySelectorAll(".entry-pc-select").forEach(o=>{o.addEventListener("change",()=>{const r=o.closest(".entry-item"),c=r&&r.querySelector(".entry-owner-select");if(!c)return;const l=parseInt(o.value)||null,d=(_rosterPcs||[]).find(u=>u.entity_id===l);c.value=d&&d.assigned_user_id?String(d.assigned_user_id):""})}),e.querySelectorAll(".entry-delete-btn").forEach(o=>{o.addEventListener("click",async r=>{if(r.stopPropagation(),!await confirmDialog({title:"Delete Entry",message:"Delete this entry?",confirmLabel:"Delete"}))return;const c=parseInt(o.dataset.entryId);try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/sessions/${t}/entries/${c}`),sessionEntries[t]=(sessionEntries[t]||[]).filter(l=>l.id!==c),a()}catch(l){showToast("Delete failed: "+l.message,6e3)}})}),e.querySelectorAll(".entry-add-btn").forEach(o=>{o.addEventListener("click",async r=>{r.stopPropagation();const c=o.dataset.section,l=o.closest(".entry-new"),d=l.querySelector(".entry-new-box"),u=l.querySelector(".entry-new-pc-select"),p=d.value.trim();if(!p){d.focus();return}let m;if(u&&u.value==="__new__"){if(m=await _createPcFromEntry(l),m==null)return}else if(m=u&&parseInt(u.value)||null,u&&!m){u.focus(),alert("Please select a PC for this entry.");return}try{const f=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/entries`,{section:c,content:p,pc_id:m});sessionEntries[t]||(sessionEntries[t]=[]),sessionEntries[t].push(f),a()}catch(f){alert("Add failed: "+f.message)}})})}function _tallyBodyHtml(e,t){const n=sessionTallyCache[e],a=n==="loading",s=n&&n!=="loading"?n:null,i=_tallyDraft[e],o=i||s,r=_isGm()||!!currentUser?.is_admin,c=y=>(y||[]).reduce((_,w)=>_+(parseInt(w.count)||0),0);function l(y,_){return!y||y.length===0?r?'<div class="tally-empty">No entries yet.</div>':'<div class="tally-empty">None recorded.</div>':y.map((w,$)=>{const E=escHtml(w.name||""),k=parseInt(w.count)||0,C=escHtml(w.note||"");if(!r){const q=w.note?`<span class="tally-note-ro">\xB7 ${C}</span>`:"";return`<div class="tally-row tally-row-ro">
991          <span class="tally-count-ro">\xD7${k}</span>
992          <span>${E}${w.note?" ":""}${q}</span>
993        </div>`}const S=_==="kills"&&k===1&&w.note?`<button class="tally-honor btn-ghost btn-xs" data-name="${E}" data-note="${C}" title="Promote this feat to the Honors Roll">\u2606</button>`:"";return`<div class="tally-row" data-sid="${e}" data-sub="${_}" data-idx="${$}">
994        <input class="tally-name"  data-sid="${e}" data-sub="${_}" data-idx="${$}" type="text" value="${E}" placeholder="Name">
995        <input class="tally-count" data-sid="${e}" data-sub="${_}" data-idx="${$}" type="number" min="0" value="${k}" title="Count">
996        <input class="tally-note"  data-sid="${e}" data-sub="${_}" data-idx="${$}" type="text" value="${C}" placeholder="Note (optional)">
997        ${S}<button class="tally-del btn-ghost btn-xs" data-sid="${e}" data-sub="${_}" data-idx="${$}" title="Remove">\u2715</button>
998      </div>`}).join("")}function d(y,_,w){const $=o&&Array.isArray(o[y])?o[y]:[],E=r?`<button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="${y}">+ Add ${_}</button>`:"";return`<div class="tally-subsection" data-sub="${y}">
999      <div class="tally-subhdr">${w} ${_} <span class="tally-subcount">(${c($)})</span></div>
1000      <div class="tally-rows">${l($,y)}</div>
1001      ${E}
1002    </div>`}if(a)return'<p class="le-empty">Loading\u2026</p>';if(!o){const y=_isGm()?'<p class="tally-hint">No tally yet. Click \u2694 Re-extract Tally to auto-populate Kills, Collateral, Conquests, and Near-Wipes from the transcript, or add rows manually below.</p>':'<p class="tally-hint">No tally recorded for this session.</p>',_=r?`<div class="tally-save-row">
1003           <button class="btn-new tally-extract-btn" data-sid="${e}" ${t?'title="Extract Kills, Collateral, Conquests, and Near-Wipes from the transcript"':'disabled title="Add some content first"'}>\u2694 Re-extract Tally</button>
1004         </div>`:"";return y+_+(r?_emptyTallyShell(e):"")}const u=r?`<div class="tally-save-row">
1005         <button class="btn-new tally-extract-btn" data-sid="${e}" ${t?'title="Re-run AI extraction over Kills, Collateral, Conquests, and Near-Wipes: overwrites the current tally"':'disabled title="Add some content first"'}>\u2694 Re-extract Tally</button>
1006         <button class="btn-new tally-save-btn" data-sid="${e}" ${i?"":"disabled"}>Save Tally</button>
1007         ${i?`<button class="tally-discard-btn btn-ghost" data-sid="${e}">Discard</button>`:""}
1008         ${s&&s.ai_generated_at?`<span class="tally-meta">AI-generated ${_fmtRelTime(s.ai_generated_at)}</span>`:""}
1009       </div>`:"",p=o&&o.coin?o.coin:{},m=["pp","gp","ep","sp","cp"].some(y=>parseInt(p[y])>0),f=!!_tallyCoinMore[e]||["pp","ep","sp","cp"].some(y=>parseInt(p[y])>0),h=r?(f?["pp","gp","ep","sp","cp"]:["gp"]).map(y=>`<span class="tally-coin-cell"><input class="tally-coin" data-sid="${e}" data-denom="${y}" type="number" min="0" value="${parseInt(p[y])||0}"><span class="tally-coin-unit">${y}</span></span>`).join("")+(f?"":`<button class="tally-coin-more btn-ghost btn-xs" data-sid="${e}">+ other coin</button>`):["pp","gp","ep","sp","cp"].filter(y=>parseInt(p[y])>0).map(y=>`<span class="tally-coin-ro">${parseInt(p[y])} <span class="tally-coin-unit">${y}</span></span>`).join("")||'<span class="tally-empty">None recorded.</span>',b=r||m?`<div class="tally-subsection" data-sub="coin">
1010      <div class="tally-subhdr">\u{1FA99} Plunder</div>
1011      <div class="tally-coin-row">${h}</div>
1012      ${r?`<p class="form-helper-text tally-coin-note">Add up the party's take here if you'd like it in the chronicle. Otherwise, leave it be.</p>`:""}
1013    </div>`:"";return`<div class="tally-panel">
1014    ${d("kills","Kills","\u{1F5E1}")}
1015    ${d("collateral","Collateral Damage","\u2620")}
1016    ${d("conquests","Conquests","\u2665")}
1017    ${d("near_wipes","Near-Wipes (party)","\u{1F480}")}
1018    ${d("downs","Death's Door (PC)","\u{1FA78}")}
1019    ${d("legends","Legends (PC)","\u{1F3C6}")}
1020    ${d("key_items","Key Items","\u{1F48E}")}
1021    ${b}
1022    ${u}
1023  </div>`}let _tallyCoinMore={};function _chr2GmRenderTally(e,t){if(sessionTallyCache[t]==null){sessionTallyCache[t]="loading",e.innerHTML='<p class="chr2-gm-empty">Loading\u2026</p>',api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${t}/tally`).then(i=>{sessionTallyCache[t]=i,_chr2GmDrawerSid===t&&_chr2GmRenderTally(e,t)}).catch(()=>{delete sessionTallyCache[t],e.innerHTML='<p class="chr2-gm-empty">Couldn\u2019t load the tally.</p>'});return}const a=(allSessions||[]).find(i=>i.id===t),s=!!(a&&(a.transcript_chars>0||a.summary));e.innerHTML=_tallyBodyHtml(t,s),_chr2GmWireTally(e,t),_tallyRenderFlareClaims(e,t)}async function _tallyRenderFlareClaims(e,t){const n=currentCampaign&&currentCampaign.id;if(n==null)return;let a=null;try{a=await api("GET",`/api/campaigns/${n}/sessions/${t}/moments/unclaimed`)}catch{return}if(_campaignStale(n)||!e.isConnected)return;const s=a&&a.unclaimed||[];if(!s.length||e.querySelector(".tally-flares"))return;const i=(allEntities||[]).filter(c=>c.type==="pc"),o=s.map(c=>{const l=_HM_KIND[c.flag_kind]||_HM_KIND.rule_of_cool;return`<div class="tally-flare-row" data-log="${c.id}">
1024      <span class="hm-node hm-node--sm hm-tone--${l.tone}" title="${escHtml(l.label)}"><svg width="13" height="13" aria-hidden="true"><use href="#${l.glyph}"/></svg></span>
1025      <span class="tally-flare-kind">${escHtml(l.label)}${c.timestamp_seconds!=null?" \xB7 "+fmtTime(c.timestamp_seconds):""}</span>
1026      <select class="tally-flare-pc form-input"><option value="">Whose moment?</option>${i.map(d=>`<option value="${d.id}">${escHtml(d.name)}</option>`).join("")}</select>
1027      <input class="tally-flare-beat" type="text" maxlength="300" placeholder="The beat, one line" title="${escHtml((c.snippet||"").slice(-160))}">
1028      <button class="tally-flare-claim btn-ghost btn-xs" data-log="${c.id}">Claim</button>
1029    </div>`}).join(""),r=document.createElement("div");r.className="tally-subsection tally-flares",r.innerHTML=`<div class="tally-subhdr">\u2691 Flares awaiting a claim <span class="tally-subcount">(${s.length})</span></div>
1030    <div class="tally-rows">${o}</div>
1031    <p class="form-helper-text">A flare marks a table moment. Tag whose it was and it joins their Hero Moments.</p>`,(e.querySelector(".tally-panel")||e).appendChild(r),r.querySelectorAll(".tally-flare-claim").forEach(c=>c.addEventListener("click",async()=>{const l=c.closest(".tally-flare-row"),d=parseInt(l.querySelector(".tally-flare-pc").value,10),u=l.querySelector(".tally-flare-beat").value.trim();if(!d){showToast("Pick whose moment it was.");return}if(!u){showToast("Give the moment a one-line beat.");return}c.disabled=!0;try{await api("POST",`/api/campaigns/${n}/moments/claim`,{log_entry_id:+l.dataset.log,entity_id:d,beat:u}),l.remove(),showToast("Claimed. It joins their Hero Moments.",3500)}catch(p){c.disabled=!1,showToast("Claim failed: "+(p.message||"try again"))}}))}function _chr2GmWireTally(e,t){const n=()=>{_chr2GmDrawerSid===t&&_chr2GmRenderSection(t,"tally")};e.querySelectorAll(".tally-name, .tally-count, .tally-note").forEach(a=>{a.addEventListener("input",s=>{const i=s.target.dataset.sub,o=parseInt(s.target.dataset.idx),r=_ensureTallyDraft(t);if(!r[i][o])return;s.target.classList.contains("tally-name")&&(r[i][o].name=s.target.value),s.target.classList.contains("tally-count")&&(r[i][o].count=Math.max(0,parseInt(s.target.value)||0)),s.target.classList.contains("tally-note")&&(r[i][o].note=s.target.value);const c=e.querySelector(".tally-save-btn");c&&(c.disabled=!1)})}),e.querySelectorAll(".tally-del").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault();const i=a.dataset.sub,o=parseInt(a.dataset.idx),r=_ensureTallyDraft(t);r[i][o]&&(r[i].splice(o,1),n())})}),e.querySelectorAll(".tally-add-row").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault();const i=a.dataset.sub;_ensureTallyDraft(t)[i].push({name:"",count:1,note:""}),n()})}),e.querySelectorAll(".tally-save-btn").forEach(a=>{a.addEventListener("click",async s=>{s.preventDefault();const i=_tallyDraft[t];if(!i)return;a.disabled=!0;const o=a.textContent;a.textContent="Saving\u2026";try{const r=d=>(d||[]).filter(u=>(u.name||"").trim()).map(u=>{const p={name:u.name.trim(),count:Math.max(0,parseInt(u.count)||0),note:(u.note||"").trim()||void 0};return u.consumed&&(p.consumed=!0),Array.isArray(u.pcs_down)&&u.pcs_down.length&&(p.pcs_down=u.pcs_down),p}),c={kills:r(i.kills),collateral:r(i.collateral),conquests:r(i.conquests),near_wipes:r(i.near_wipes),key_items:r(i.key_items),downs:r(i.downs),legends:r(i.legends),coin:i.coin||null},l=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}/tally`,c);sessionTallyCache[t]=l,delete _tallyDraft[t],_refreshCampaignTally(),showToast("\u2694 Tally saved"),n()}catch(r){a.disabled=!1,a.textContent=o,showToast("Save failed: "+r.message)}})}),e.querySelectorAll(".tally-discard-btn").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault(),delete _tallyDraft[t],n()})}),e.querySelectorAll(".tally-honor").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault();const i=a.closest(".tally-row");if(!i||i.nextElementSibling?.classList?.contains("hm-promote-form"))return;const o=(allSessions||[]).find(d=>d.id===t),c=(o?_chr2SessionNum(o):null)||null,l=document.createElement("div");l.className="hm-promote-form hm-promote-form--tally",l.innerHTML=`
1032        <input type="text" class="hm-pf-text" maxlength="120" value="${esc
1032Html(("The Party felled "+a.dataset.name).slice(0,120))}" title="The honor line, as the roll will read it">
1033        <span class="hm-pf-acts"><button type="button" class="btn-new btn-xs hm-pf-go">Enshrine</button><button type="button" class="btn-ghost btn-xs hm-pf-cancel">Cancel</button></span>`,i.after(l),l.querySelector(".hm-pf-cancel").addEventListener("click",()=>l.remove()),l.querySelector(".hm-pf-go").addEventListener("click",async()=>{const d=l.querySelector(".hm-pf-go");d.disabled=!0;try{await api("POST",`/api/campaigns/${currentCampaign.id}/honors/promote`,{title:l.querySelector(".hm-pf-text").value.trim(),detail:a.dataset.note||"",chapter:c}),l.remove(),showToast("Enshrined on the Honors Roll.",3500)}catch(u){d.disabled=!1,showToast("Failed: "+(u.message||"try again"))}})})}),e.querySelectorAll(".tally-coin").forEach(a=>{a.addEventListener("input",s=>{const i=_ensureTallyDraft(t);i.coin=i.coin||{},i.coin[s.target.dataset.denom]=Math.max(0,parseInt(s.target.value)||0);const o=e.querySelector(".tally-save-btn");o&&(o.disabled=!1)})}),e.querySelectorAll(".tally-coin-more").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault(),_tallyCoinMore[t]=!0,n()})}),e.querySelectorAll(".tally-extract-btn").forEach(a=>{a.addEventListener("click",async s=>{if(s.stopPropagation(),_tallyDraft[t]){if(!await confirmDialog({title:"Discard unsaved tally edits?",message:"You have unsaved tally edits. Re-extracting will overwrite them with a fresh AI pass.",confirmLabel:"Re-extract"}))return;delete _tallyDraft[t]}const i=a.textContent;a.disabled=!0,a.textContent="\u2694 Extracting\u2026";try{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/extract-tally`);sessionTallyCache[t]=o;try{campaignTally=await api("GET",`/api/campaigns/${currentCampaign.id}/campaign-tally`)}catch{}const r=(o.kills?.length||0)+(o.collateral?.length||0)+(o.conquests?.length||0)+(o.near_wipes?.length||0);showToast(r?`\u2694 Tally updated: ${r} entries`:"\u2694 Tally updated, no new entries",4e3),n()}catch(o){showToast("Tally extraction failed: "+o.message),a.disabled=!1,a.textContent=i}})})}function _tfRomanToNum(e){const t=/([IVXLCDM]+)\s*$/i.exec(String(e||""));if(!t)return null;const n={I:1,V:5,X:10,L:50,C:100,D:500,M:1e3};let a=0;const s=t[1].toUpperCase();for(let i=0;i<s.length;i++){const o=n[s[i]]||0,r=n[s[i+1]]||0;a+=o<r?-o:o}return a||null}function _tfHonorRibbonSvg(e){const t=String(e||"").toUpperCase(),a=Math.max(Math.round(t.length*9.4)+32,40)+60,s=i=>a-i;
1033return`<svg class="hon-ribbon" viewBox="0 0 ${a} 36" width="${a}" height="36" role="img" aria-label="${escHtml(t)}">
1034    <g class="hr-lift">
1035      <path class="hr-body" d="M30,8 L2,4 L14,18 L2,32 L30,28 Z"/>
1036      <path class="hr-body" d="M${s(30)},8 L${s(2)},4 L${s(14)},18 L${s(2)},32 L${s(30)},28 Z"/>
1037      <rect class="hr-body" x="30" y="8" width="${a-60}" height="20"/>
1038      <path class="hr-flap" d="M16,15 L24,18 L16,21 Z"/>
1039      <path class="hr-flap" d="M${s(16)},15 L${s(24)},18 L${s(16)},21 Z"/>
1040      <path class="hr-line" d="M30,8 L2,4 M2,32 L30,28"/>
1041      <path class="hr-line" d="M${s(30)},8 L${s(2)},4 M${s(2)},32 L${s(30)},28"/>
1042      <line class="hr-line" x1="30" y1="8" x2="${s(30)}" y2="8"/>
1043      <line class="hr-line" x1="30" y1="28" x2="${s(30)}" y2="28"/>
1044      <text class="hr-text" x="${a/2}" y="22.5" text-anchor="middle">${escHtml(t)}</text>
1045    </g>
1046  </svg>`}const _HM_KIND={death_return:{label:"Death and return",glyph:"tf-glyph-moment-brush",tone:"ember",major:!0},legend:{label:"The stuff of legend",glyph:"tf-glyph-moment-laurel",tone:"gold",major:!0},death_door:{label:"Death's door",glyph:"tf-glyph-moment-brush",tone:"ember"},kill:{label:"Killing blow",glyph:"tf-glyph-moment-kill",tone:"gold"},conquest:{label:"Conquest",glyph:"tf-glyph-moment-heart",tone:"ember"},key_item:{label:"Claimed",glyph:"tf-glyph-moment-gem",tone:"teal"},manual:{label:"Remembered",glyph:"tf-glyph-moment-quill",tone:"teal"},plot_twist:{label:"Plot twist",glyph:"tf-glyph-react-twist",tone:"teal"},rule_of_cool:{label:"Rule of cool",glyph:"tf-glyph-react-cool",tone:"cyan"},clutch_save:{label:"Clutch save",glyph:"tf-glyph-react-clutch",tone:"gold"},epic_fail:{label:"Epic fail",glyph:"tf-glyph-react-fail",tone:"ember"}};async function _cx2RenderHeroMoments(e){const t=currentCampaign&&currentCampaign.id,n=document.querySelector(`.cx2-moments-mount[data-entity-id="${e}"]`);if(!t||!n)return;let a=null;try{a=await api("GET",`/api/campaigns/${t}/entities/${e}/moments`)}catch{return}if(_campaignStale(t))return;const s=document.querySelector(`.cx2-moments-mount[data-entity-id="${e}"]`);if(!s)return;const i=(a&&a.moments||[]).filter(g=>g.status==="active");if(!i.length&&!a.can_edit){s.innerHTML="";return}const o={};(allSessions||[]).filter(g=>!g.is_session_zero).forEach(g=>{o[g.id]=_chr2SessionNum(g)});const r=g=>o[g]?"Chapter "+_chr2Roman(o[g]):"",c=r(a.introduced_session_id),l=_isGm(),d=g=>a.can_edit?`<div class="hm-add" data-sid="${g??""}"><button type="button" class="hm-add-btn" title="Add your own moment here">+</button></div>`:"",u=i.map((g,h)=>{const v=_HM_KIND[g.kind]||_HM_KIND.rule_of_cool,b=h%2===0?"l":"r",y=a.can_edit?`<button type="button" class="hm-act hm-edit" data-mid="${g.id}" title="Edit this moment">\u270E</button><button type="button" class="hm-act hm-quote-btn" data-mid="${g.id}" data-has="${g.quote?"1":""}" title="${g.quote?"Change or unlink the quote":"Link a quote from this chapter"}">&quot;</button><button type="button" class="hm-act hm-hide" data-mid="${g.id}" title="Remove from the record">\u2715</button>`:"";let _="";return g.honor==="honored"?_=`<span class="hm-honor hm-honor--done" title="On the company's Honors Roll">\u2605 Honored</span>`:g.honor?_=`<span class="hm-honor" title="${g.honor==="candidate"?"Auto-flagged as an honors candidate":"Nominated for the Honors Roll"}">\u2606 ${g.honor==="candidate"?"Candidate":"Nominated"}</span>`+(l?`<button type="button" class="hm-act hm-promote" data-mid="${g.id}" data-kind="${g.kind}" data-beat="${escHtml(g.beat)}" data-sid="${g.session_id||""}" title="Enshrine on the Honors Roll">Promote</button>`:""):a.can_edit&&g.status!=="hidden"&&(_=`<button type="button" class="hm-act hm-nominate" data-mid="${g.id}" title="Nominate for the Honors Roll">\u2606</button>`),`<div class="hm-entry hm-entry--${b}" data-mid="${g.id}">
1047      <span class="hm-node hm-tone--${v.tone}${v.major?" hm-node--major":""}" title="${escHtml(v.label)}"><svg width="16" height="16" aria-hidden="true"><use href="#${v.glyph}"/></svg></span>
1048      <div class="hm-card">
1049        <span class="hm-chap">${escHtml(r(g.session_id)||v.label)}</span>
1050        ${g.quote?`<p class="hm-quote">"${escHtml(g.quote)}"</p>`:""}
1051        <p class="hm-beat" data-mid="${g.id}">${escHtml(g.beat)}</p>
1052        <span class="hm-card-acts">${_}${y}</span>
1053      </div>
1054    </div>${d(g.session_id)}`}).join(""),p=a.state==="dead"?`<div class="hm-anchor hm-anchor--end hm-anchor--dead"><span class="hm-sq">\u2620</span><span class="hm-anchor-lbl">Fell${r(a.end_session_id)?" \xB7 "+escHtml(r(a.end_session_id)):""}</span></div>`:a.state==="departed"?`<div class="hm-anchor hm-anchor--end hm-anchor--gone"><span class="hm-sq"></span><span class="hm-anchor-lbl">Departed${r(a.end_session_id)?" \xB7 "+escHtml(r(a.end_session_id)):""}</span></div>`:'<div class="hm-anchor hm-anchor--end hm-anchor--open"><span class="hm-dot"></span><span class="hm-anchor-lbl">To Be Continued</span></div>';s.innerHTML=`<div class="cx2-moments open${a.state!=="dead"&&a.state!=="departed"?" cx2-moments--open-end":""}">
1055    <button type="button" class="hm-toggle" aria-expanded="true">
1056      <h2 class="cx2-section cx2-section--spaced">Hero Moments</h2>
1057      <span class="hm-count">(${i.filter(g=>g.status==="active").length})</span>
1058      <span class="hm-chev" aria-hidden="true">\u203A</span>
1059    </button>
1060    <div class="hm-body">
1061      <div class="hm-track">
1062        <span class="hm-spine" aria-hidden="true"></span>
1063        <div class="hm-anchor hm-anchor--start"><span class="hm-sq"></span><span class="hm-anchor-lbl">Joined the Party${c?" \xB7 "+escHtml(c):""}</span></div>
1064        ${d(a.introduced_session_id!=null?a.introduced_session_id:i[0]&&i[0].session_id)}
1065        ${u}
1066        ${p}
1067      </div>
1068    </div>
1069  </div>`;const m=s.querySelector(".cx2-moments");m.querySelector(".hm-toggle").addEventListener("click",()=>{const h=!m.querySelector(".hm-body").classList.toggle("hidden");m.querySelector(".hm-toggle").setAttribute("aria-expanded",h?"true":"false"),m.classList.toggle("open",h)});const f=()=>_cx2RenderHeroMoments(e).then(()=>{const g=document.querySelector(`.cx2-moments-mount[data-entity-id="${e}"] .cx2-moments`);g&&(g.querySelector(".hm-body")?.classList.remove("hidden"),g.classList.add("open"),g.querySelector(".hm-toggle")?.setAttribute("aria-expanded","true"))});m.querySelectorAll(".hm-nominate").forEach(g=>g.addEventListener("click",async h=>{h.stopPropagation(),g.disabled=!0;try{await api("POST",`/api/campaigns/${t}/moments/${g.dataset.mid}`,{honor:"nominate"}),_campaignStale(t)||(showToast("Nominated for the Honors Roll. The GM decides what ascends.",3500),f())}catch(v){g.disabled=!1,showToast("Failed: "+(v.message||"try again"))}})),m.querySelectorAll(".hm-promote").forEach(g=>g.addEventListener("click",h=>{h.stopPropagation();const v=g.closest(".hm-card");if(!v||v.querySelector(".hm-promote-form"))return;const b=g.dataset.beat||"",y=b.startsWith(a.name)?b:`${a.name}: ${b}`,_=g.dataset.kind==="kill"&&/^Felled ([^:]+)/.test(b)?b.match(/^Felled ([^:]+)/)[1].trim():"",w=o[+g.dataset.sid]||null,$=document.createElement("div");$.className="hm-promote-form",$.innerHTML=`
1070      <input type="text" class="hm-pf-text" maxlength="120" value="${escHtml(y.slice(0,120))}" title="The honor line, as the roll will read it">
1071      <input type="text" class="hm-pf-title" maxlength="60" placeholder="Grant a title (optional)" value="${_?escHtml("Bane of "+_):""}">
1072      <span class="hm-pf-acts"><button type="button" class="btn-new btn-xs hm-pf-go">Enshrine</button><button type="button" class="btn-ghost btn-xs hm-pf-cancel">Cancel</button></span>`,v.appendChild($),$.querySelector(".hm-pf-cancel").addEventListener("click",()=>$.remove()),$.querySelector(".hm-pf-go").addEventListener("click",async()=>{const E=$.querySelector(".hm-pf-go");E.disabled=!0;try{if(await api("POST",`/api/campaigns/${t}/moments/${g.dataset.mid}`,{honor:"promote",honor_text:$.querySelector(".hm-pf-text").value.trim(),honorific:$.querySelector(".hm-pf-title").value.trim()||void 0,chapter:w}),!_campaignStale(t)){if(showToast("Enshrined on the Honors Roll.",3500),$.querySelector(".hm-pf-title").value.trim())try{await loadEntities()}catch{}_campaignStale(t)||renderCodexEntry(e,{keepScroll:!0})}}catch(k){E.disabled=!1,showToast("Failed: "+(k.message||"try again"))}})})),m.querySelectorAll(".hm-hide").forEach(g=>g.addEventListener("click",async h=>{h.stopPropagation(),g.disabled=!0;try{await api("POST",`/api/campaigns/${t}/moments/${g.dataset.mid}`,{status:"hidden"}),_campaignStale(t)||(showToast("Removed from the record.",3e3),f())}catch(v){g.disabled=!1,showToast("Failed: "+(v.message||"try again"))}})),m.querySelectorAll(".hm-edit").forEach(g=>g.addEventListener("click",h=>{h.stopPropagation();const v=g.closest(".hm-card"),b=v&&v.querySelector(".hm-beat");if(!b||v.querySelector(".hm-edit-form"))return;const y=b.textContent,_=document.createElement("div");_.className="hm-edit-form",_.innerHTML=`
1073      <input type="text" class="hm-ef-text" maxlength="300" value="${esc
1073Html(y)}">
1074      <span class="hm-pf-acts"><button type="button" class="btn-new btn-xs hm-ef-save">Save</button><button type="button" class="btn-ghost btn-xs hm-ef-cancel">Cancel</button></span>`,b.classList.add("hidden"),b.after(_);const w=_.querySelector(".hm-ef-text");w.focus(),_.querySelector(".hm-ef-cancel").addEventListener("click",()=>{_.remove(),b.classList.remove("hidden")}),_.querySelector(".hm-ef-save").addEventListener("click",async()=>{const $=w.value.trim();if(!$){w.focus();return}const E=_.querySelector(".hm-ef-save");E.disabled=!0;try{await api("POST",`/api/campaigns/${t}/moments/${g.dataset.mid}`,{beat:$}),_campaignStale(t)||f()}catch(k){E.disabled=!1,showToast("Failed: "+(k.message||"try again"))}})})),m.querySelectorAll(".hm-quote-btn").forEach(g=>g.addEventListener("click",async h=>{h.stopPropagation();const v=g.closest(".hm-card");if(!v||v.querySelector(".hm-quote-pick"))return;g.disabled=!0;let b=null;try{b=await api("GET",`/api/campaigns/${t}/moments/${g.dataset.mid}/quote-suggestions`)}catch{g.disabled=!1;return}if(g.disabled=!1,_campaignStale(t)||!v.isConnected)return;const y=b&&b.quotes||[],_=document.createElement("div");_.className="hm-quote-pick",_.innerHTML=(y.length?y.map(E=>`<button type="button" class="hm-qp-row" data-q="${escHtml(E.quote)}"><span class="hm-qp-spk">${escHtml(E.speaker_name||"Unknown")}</span> "${escHtml((E.quote||"").slice(0,120))}"</button>`).join(""):'<p class="placeholder">No quotes captured for this chapter.</p>')+`<span class="hm-pf-acts">${g.dataset.has?'<button type="button" class="btn-ghost btn-xs hm-qp-clear">Unlink quote</button>':""}<button type="button" class="btn-ghost btn-xs hm-qp-cancel">Cancel</button></span>`,v.appendChild(_),_.querySelector(".hm-qp-cancel").addEventListener("click",()=>_.remove());const w=async E=>{try{await api("POST",`/api/campaigns/${t}/moments/${g.dataset.mid}`,{quote:E}),_campaignStale(t)||f()}catch(k){showToast("Failed: "+(k.message||"try again"))}};_.querySelectorAll(".hm-qp-row").forEach(E=>E.addEventListener("click",()=>w(E.dataset.q)));const $=_.querySelector(".hm-qp-clear");$&&$.addEventListener("click",()=>w(""))})),m.querySelectorAll(".hm-add-btn").forEach(g=>g.addEventListener("click",h=>{h.stopPropagation();const v=g.closest(".hm-add");if(!v||v.querySelector(".hm-add-form"))return;const b=document.createElement("div");b.className="hm-add-form",b.innerHTML=`
1075      <input type="text" class="hm-af-text" maxlength="300" placeholder="What happened \u2014 one line, this PC at the center">
1076      <span class="hm-pf-acts"><button type="button" class="btn-new btn-xs hm-af-go">Add</button><button type="button" class="btn-ghost btn-xs hm-af-cancel">Cancel</button></span>`,v.appendChild(b);const y=b.querySelector(".hm-af-text");y.focus(),b.querySelector(".hm-af-cancel").addEventListener("click",()=>b.remove()),b.querySelector(".hm-af-go").addEventListener("click",async()=>{const _=y.value.trim();if(!_){y.focus();return}const w=b.querySelector(".hm-af-go");w.disabled=!0;try{await api("POST",`/api/campaigns/${t}/entities/${e}/moments`,{beat:_,session_id:v.dataset.sid?+v.dataset.sid:null}),_campaignStale(t)||f()}catch($){w.disabled=!1,showToast("Failed: "+($.message||"try again"))}})}))}function _tfContentFilterOn(){return localStorage.getItem("tf_profanity_filter")==="true"}function _chr2SpoilsHtml(e){if(!e||e==="loading")return"";const t=(()=>{const l={};return(Array.isArray(e.downs)?e.downs:[]).forEach(d=>{if(!d||!(d.name||"").trim())return;const u=d.name.trim(),p=l[u]=l[u]||{name:u,count:0,notes:[]};p.count+=Math.max(1,parseInt(d.count)||1),d.note&&p.notes.push(d.note)}),Object.values(l).map(d=>({name:d.name,count:d.count,note:d.notes[0]||""}))})(),n=[{key:"kills",cls:"kills",label:"Kills",sum:!0},{key:"collateral",cls:"coll",label:"Collateral",sum:!0},{key:"conquests",cls:"conq",label:"Conquests",sum:!1},{key:"near_wipes",cls:"wipe",label:"Near-Wipes",sum:!1,sub:{label:"Death's Door",rows:t,sum:!0}},{key:"key_items",cls:"item",label:"Key Items",sum:!1}].filter(l=>l.key!=="conquests"||!_tfContentFilterOn()),a=l=>(l||[]).reduce((d,u)=>d+(parseInt(u.count)||0),0),s=e.coin||{},i=["pp","gp","ep","sp","cp"].filter(l=>parseInt(s[l])>
10760).map(l=>`${s[l]} ${l}`);if(!(n.reduce((l,d)=>l+(d.rows||(Array.isArray(e[d.key])?e[d.key]:[])).length+(d.sub&&d.sub.rows||[]).length,0)+i.length))return"";const r=l=>l.map(d=>{const u=parseInt(d.count)||0,p=u>1?`\xD7${u}`:"",m=(d.note||"").trim();return`<li><span class="x">${p}</span><span><span class="nm">${escHtml(d.name||"")}</span>${m?`<span class="nt">${escHtml(m)}</span>`:""}</span></li>`}).join("");return`<div class="spoils">
1077    <div class="spoils-hdr"><span class="t">The Spoils</span><span class="sub">This chapter's reckoning</span></div>
1078    <div class="spoils-grid">${n.map(l=>{const d=l.rows||(Array.isArray(e[l.key])?e[l.key]:[]),u=l.sum?a(d):d.length,p=l.sub&&l.sub.rows||[],m=p.length?`<div class="spoils-sub"><h6><span class="n">${l.sub.sum?a(p):p.length}</span> ${l.sub.label}</h6><ul class="spoils-list">${r(p)}</ul></div>`:"";return`<div class="spoils-col ${l.cls}">
1079      <h5><span class="n">${u}</span> ${l.label}</h5>
1080      <ul class="spoils-list">${r(d)}</ul>${m}
1081    </div>`}).join("")}</div>
1082    ${i.length?`<div class="spoils-total"><span class="spoils-total-lbl">Plunder</span><span class="spoils-total-amt">${i.join(" \xB7 ")}</span></div>`:""}  </div>`}async function _chr2RenderSpoils(e,t){if(!t||!t.summary)return;const n=currentCampaign&&currentCampaign.id;if(!n)return;const a=e.querySelector(".chapter-body");if(!a||a.querySelector(".spoils"))return;let s=sessionTallyCache[t.id];if(s==="loading")return;if(s==null)try{s=await api("GET",`/api/campaigns/${n}/sessions/${t.id}/tally`),sessionTallyCache[t.id]=s}catch{return}const i=_chr2SpoilsHtml(s);i&&(!document.body.contains(a)||a.querySelector(".spoils")||a.insertAdjacentHTML("beforeend",i))}function _chr2SortTallyEntries(e){return(e||[]).filter(t=>t&&(t.name||"").trim()).slice().sort((t,n)=>(parseInt(n.count)||0)-(parseInt(t.count)||0)||(t.name||"").toLowerCase().localeCompare((n.name||"").toLowerCase()))}function _chr2EntryNote(e){return e?(e.note||"").trim()?e.note.trim():Array.isArray(e.notes)?e.notes.map(t=>(t||"").trim()).filter(Boolean).join(" \xB7 "):"":""}function _chr2EntrySess(e){const t=e&&e.session_num!=null?e.session_num:e&&e.last_session_num;return t??null}function _chr2PartitionKills(e){const t=[],n=[];return(e||[]).forEach(a=>{!a||!(a.name||"").trim()||(_classifyTallyRow(a)==="factions"?n:t).push(a)}),{bbegs:_chr2SortTallyEntries(t),minions:_chr2SortTallyEntries(n)}}function _chr2PopLi(e,t,n){const a=parseInt(e.count)||0,s=t&&a>=1?`<span class="x">\xD7${a}</span>`:'<span class="x"></span>',i=n?_chr2EntryNote(e):"",o=i?`<span class="pop-note">${escHtml(i)}</span>`:"";return`<li${o?' class="has-note"':""}>${s}<span class="nm">${escHtml(e.name)}</span>${o}</li>`}function _chr2PopSection(e,t,n,a,s){if(!t.length)return"";const i=a?t.slice(0,a):t,o=t.length-i.length,r=o>0?`<li class="more">\u2026and ${o} more</li>`:"";return`${e?`<span class="chr2-stat-pop-sub">${escHtml(e)}</span>`:""}<ul>
1082${i.map(l=>_chr2PopLi(l,n,s)).join("")}${r}</ul>`}function _chr2TallyPopInner(e,t,n,a){const s=`<span class="chr2-stat-pop-hdr">${escHtml(e)}</span>`;if(a==="kills"){const{bbegs:d,minions:u}=_chr2PartitionKills(t);return!d.length&&!u.length?"":s+_chr2PopSection("BBEGs",d,n,6,!0)+_chr2PopSection("Minions",u,n,6,!1)+'<div class="chr2-stat-pop-foot">click for full details</div>'}const i=_chr2SortTallyEntries(t);if(!i.length)return"";const r=i.slice(0,12),c=i.length-r.length,l=c>0?`<li class="more">\u2026and ${c} more \xB7 click for all</li>`:'<li class="more">click for full details</li>';return s+`<ul>${r.map(d=>_chr2PopLi(d,n)).join("")}${l}</ul>`}function _chr2ShowStatPop(e,t,n,a,s){const i=_chr2TallyPopInner(t,n,a,s);if(!i)return;let o=document.getElementById("chr2-stat-pop");o||(o=document.createElement("div"),o.id="chr2-stat-pop",o.className="chr2-stat-pop",document.body.appendChild(o)),o.innerHTML=i;const r=280,c=10;let l=e.clientX;l+r+c>window.innerWidth&&(l=Math.max(c,window.innerWidth-r-c)),o.style.left=l+"px",o.style.top=e.clientY+6+"px",o.classList.add("show")}function _chr2HideStatPop(){const e=document.getElementById("chr2-stat-pop");e&&e.classList.remove("show")}function _chr2StatModalText(e,t,n,a){const s=(o,r=!0)=>{const c=parseInt(o.count)||0,l=n&&c>=1?`x${c} `:"",d=r?_chr2EntryNote(o):"",u=_chr2EntrySess(o),p=u!=null?` (S${u})`:"";return`- ${l}${o.name}${d?` \xB7 ${d}`:""}${p}`};if(a==="kills"){const{bbegs:o,minions:r}=_chr2PartitionKills(t),c=(l,d,u)=>d.length?`
1083${l}:
1084`+d.map(p=>s(p,u)).join(`
1085`):"";return`${e} (${o.length+r.length})`+c("BBEGs",o,!0)+c("Minions",r,!1)}const i=_chr2SortTallyEntries(t).map(o=>s(o,!0));return`${e} (${i.length})
1086`+i.join(`
1087`)}function _chr2OpenStatModal(e,t,n,a){_chr2HideStatPop();let s=document.getElementById("chr2-stat-modal");if(!s){s=document.createElement("div"),s.id="chr2-stat-modal",s.className="modal-overlay chr2-stat-modal hidden",s.innerHTML=`<div class="modal-box modal-box--sm chr2-stat-modal-box">
1088      <div class="csm-hdr">
1089        <span class="csm-title"></span>
1090        <button type="button" class="btn-ghost btn-xs csm-copy">Copy</button>
1091        <button type="button" class="csm-close" aria-label="Close">&times;</button>
1092      </div>
1093      <ul class="csm-list"></ul>
1094    </div>`,document.body.appendChild(s);const c=()=>s.classList.add("hidden");s.querySelector(".csm-close").addEventListener("click",c),s.addEventListener("click",l=>{l.target===s&&c()}),document.addEventListener("keydown",l=>{_tfModalEsc(l,s)&&c()})}s.querySelector(".csm-title").textContent=e;const i=(c,l=!0)=>{const d=parseInt(c.count)||0,u=n&&d>=1?`<span class="csm-x">\xD7${d}</span>`:'<span class="csm-x"></span>',p=l?_chr2EntryNote(c):"",m=_chr2EntrySess(c),f=m!=null?`S${m}`:"";return`<li>${u}<span class="csm-name">${escHtml(c.name)}</span>${f?`<span class="csm-sess">${escHtml(f)}</span>`:""}${p?`<span class="csm-note">${escHtml(p)}</span>`:""}</li>`};let o;if(a==="kills"){const{bbegs:c,minions:l}=_chr2PartitionKills(t),d=(u,p,m)=>p.length?`<li class="csm-sub">${u}</li>`+p.map(f=>i(f,m)).join(""):"";o=d("BBEGs",c,!0)+d("Minions",l,!1)}else o=_chr2SortTallyEntries(t).map(c=>i(c,!0)).join("");s.querySelector(".csm-list").innerHTML=o||'<li class="csm-empty">No entries.</li>';const r=s.querySelector(".csm-copy");r.textContent="Copy",r.onclick=()=>{const c=_chr2StatModalText(e,t,n,a),l=()=>{r.textContent="\u2713 Copied",setTimeout(()=>{r.textContent="Copy"},1500)};if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(c).then(l).catch(()=>{});else{const d=document.createElement("textarea");d.value=c,document.body.appendChild(d),d.select();try{document.execCommand("copy"),l()}catch{}d.remove()}},s.classList.remove("hidden")}async function _chr2RenderIndexTallyStats(e){const t=e.querySelector(".index-stats");if(!t||t.querySelector(".chr2-stat"))return;if(campaignTally==null)try{await _refreshCampaignTally()}catch{return}const n=campaignTally;if(!n||!document.body.contains(t))return;const s=[{label:"Kills",n:n.body_count!=null?n.body_count:0,word:()=>
1094"slain",entries:n.kills,showCount:!0,partition:"kills"},{label:"Collateral",n:n.collateral_total!=null?n.collateral_total:0,word:()=>"collateral",entries:n.collateral,showCount:!0},{label:"Conquests",n:Array.isArray(n.conquests)?n.conquests.length:0,word:o=>o===1?"conquest":"conquests",entries:n.conquests,showCount:!1},{label:"Near-Wipes",n:n.near_wipe_count!=null?n.near_wipe_count:Array.isArray(n.near_wipes)?n.near_wipes.length:0,word:o=>o===1?"near-wipe":"near-wipes",entries:n.near_wipes,showCount:!1},{label:"Key Items",n:n.key_item_count!=null?n.key_item_count:Array.isArray(n.key_items)?n.key_items.length:0,word:o=>o===1?"key item":"key items",entries:n.key_items,showCount:!1}].filter(o=>o.n>0&&(o.label!=="Conquests"||!_tfContentFilterOn()));if(!s.length)return;const i=s.map((o,r)=>`<span class="dot"></span><span class="chr2-stat" tabindex="0" role="button" data-stat-i="${r}"><b>${o.n}</b> ${o.word(o.n)}</span>`).join("");t.insertAdjacentHTML("beforeend",i),t.querySelectorAll(".chr2-stat[data-stat-i]").forEach(o=>{const r=s[parseInt(o.dataset.statI)];r&&(o.addEventListener("mouseenter",c=>_chr2ShowStatPop(c,r.label,r.entries,r.showCount,r.partition)),o.addEventListener("mouseleave",_chr2HideStatPop),o.addEventListener("click",()=>_chr2OpenStatModal(r.label,r.entries,r.showCount,r.partition)),o.addEventListener("keydown",c=>{(c.key==="Enter"||c.key===" ")&&(c.preventDefault(),_chr2OpenStatModal(r.label,r.entries,r.showCount,r.partition))}))})}let _chr2OpenSid=null;function renderChronicleRecap(e,t){t=t||{};const n=currentCampaign&&currentCampaign.id;if(!n)return;const a=_chr2Panel();if(t.background&&a.classList.contains("hidden"))return;const s=e===_chr2OpenSid&&!t.toTop,i=t.preserveScroll||t.background||s?(a.querySelector(".chron-right")||{}).scrollTop:null;_chr2OpenSid=e,a._chrInsetHeros&&(a._chrInsetHeros.forEach(M=>{try{M.dispose()}catch{}}),a._chrInsetHeros=null),t.background||_chr2Show(a,`#chronicle/${n}/${e}`);const o=escHtml(currentCampaign&&currentCampaign.name||"Campaign"),r=(allSessions||[]).find(M=>M.id===e);if(!r){a.innerHTML='<div class="chron-shell no-rail"><div class="chron-right"><div class="chron-page"><div class="chr-empty">Session not found.</div></div></div></div>';return}const c=(allSessions||[]).filter(M=>!M.is_session_zero).map(M=>({x:M,n:_chr2SessionNum(M)})).sort((M,D)=>M.n-D.n),l=[..._chr2Zeros().map(M=>({x:M.s,n:0})),...c],d=_chr2SessionNum(r),u=_chr2Roman(d),p=l.findIndex(M=>M.x.id===e),m=p>0?l[p-1]:null,f=p>=0&&p<l.length-1?l[p+1]:null,g=_chr2DateLong(r),h=_chr2Beats(r.summary),v=_chr2PickLayout(h.length||0);let b;if(r.summary)b=renderSessionSummary(r.summary,allEntities,r.session_quotes,r.session_blurb,r.qotn);else if(r.is_prep_session&&r.is_session_zero){const M=r.prep_entities_built,D=M==null?"":M>0?`${M} codex ${M===1?"entry was":"entries were"} added or updated from it.`:"Nothing new for the codex from this one.";b=`<div class="chr-unwritten chr-unwritten--prep">
1095      <div class="chr-unwritten-mark" aria-hidden="true">\u2726</div>
1096      <p class="chr-unwritten-lead">This was prep, not play.</p>
1097      <p class="chr-unwritten-sub">No chapter was written. ${escHtml(D)} No session credit was used.</p>
1098      ${r.prep_reason?`<p class="chr-prep-reason">${escHtml(r.prep_reason)}</p>`:""}
1099      ${_isGm()?`<button type="button" class="btn-ghost btn-xs chr-prep-override" data-chr-force="${r.id}">Write it anyway</button>
1100      <span class="chr-generate-status" id="chr-gen-status-${r.id}"></span>
1101      <div class="chr-generate-result" id="chr-gen-result-${r.id}"></div>`:""}
1102    </div>`}else if(_isRecapInProgress(r))_maybeStartRecapPoll(r.id),b=`<div class="chr-unwritten">
1103      <p class="chr-unwritten-lead">The chapter is being written\u2026</p>
1104      <p class="chr-unwritten-sub">This takes a minute or two. Safe to step away; it appears here the moment it's ready.</p>
1105    </div>`;else if(_isGm()){const M=_recapGate(r);if(M.hide)b=`<p class="chr-empty">This chapter hasn't been written yet.</p>`;else{const D=M.disabled?`<button type="button" class="chr-generate-btn chr-generate-btn--disabled" disabled aria-disabled="true" title="${esc
1105Html(M.tip)}">\u2726 Generate Recap</button>`:`<button type="button" class="chr-generate-btn" data-chr-generate="${r.id}">\u2726 Generate Recap</button>`;b=`<div class="chr-unwritten chr-unwritten--gm">
1106      <div class="chr-unwritten-mark" aria-hidden="true">\u2726</div>
1107      <p class="chr-unwritten-lead">This chapter is unwritten.</p>
1108      <p class="chr-unwritten-sub">${M.hasTx?"The session is recorded: generate the recap to bring this chapter to life.":"Record this session at Live at the Table, or upload a transcript in Session Tools, to write this chapter."}</p>
1109      ${D}
1110      <button type="button" class="btn-new btn-xs chr-write-own" data-body-edit>Write it myself</button>
1111      <span class="chr-generate-status" id="chr-gen-status-${r.id}"></span>
1112      <div class="chr-generate-result" id="chr-gen-result-${r.id}"></div>
1113    </div>`}}else b=`<p class="chr-empty">This chapter hasn't been written yet.</p>`;const _=(r.chronicle_bardic||"").trim()||r.session_blurb||_chr2OneLine(r)||"",w=_isGm()?`<button type="button" class="chapter-bardic-edit" data-bardic-edit title="Edit bardic standfirst" aria-label="Edit bardic standfirst">${_CHR2_PENCIL_SVG}</button>`:"",$=r.summary?_chr2BriefText(r):"",E=_isGm()?`<div class="chapter-brief-actions"><button type="button" class="chapter-brief-edit" data-brief-edit title="Edit the recap TL;DR" aria-label="Edit the recap TL;DR">${_CHR2_PENCIL_SVG}</button></div>`:"",k=$?`<div class="chapter-brief" id="cr-brief">
1114    <button type="button" class="chapter-brief-bar" data-brief-toggle aria-expanded="false" aria-controls="cr-brief-body" title="Show the whole recap TL;DR">
1115      <span class="chapter-brief-lbl">Recap TL;DR</span>
1116      <span class="chapter-brief-snip">${escHtml($)}</span>
1117      <span class="chapter-brief-go" aria-hidden="true"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></span>
1118    </button>
1119    <div class="chapter-brief-body" id="cr-brief-body" hidden><p class="chapter-brief-text">${linkifyText(escHtml($),allEntities,null)}</p>${E}</div>
1120  </div>`:"",C=_.trim()?`<div class="chapter-bardic" id="cr-bardic">
1121    ${_CHR2_QUILL_SVG}
1122    <p class="chapter-bardic-text">${_bardicFlavorHtml(_)}</p>
1123    <div class="attr">${escHtml(_bardicByline("Bardic Companion"))}</div>
1124    ${w}
1125  </div>`:"",S=_chr2InThisSession(r),q=S.length?S.slice().sort((M,D)=>sortKey(M.name).localeCompare(sortKey(D.name))).map(M=>_codex2RailItemHtml(M,null)).join(""):'<em style="color:var(--ink-4);font-size:13px;font-family:var(--t-disp)">No linked entities</em>';let I="";if(_isGm()&&r.summary){const M=typeof r.raw_transcript=="string"?r.raw_transcript.split(/\s+/).filter(Boolean).length>1500:(r.transcript_chars||0)>9e3,D=r.is_live_transcript&&M?"post-live":"post-recap";I=`<div class="referral-promo-slot referral-promo-slot--${D}" data-session-id="${r.id}" data-surface="${D==="post-live"?"post_live":"post_recap"}"></div>`}const T=(()=>{const M=N=>/^\d+$/.test(String(N))?+N:_tfRomanToNum(String(N||"").replace(/^\s*ch\.?\s*/i,""))||null,D=(currentCampaign&&currentCampaign.company_deeds||[]).filter(N=>N&&N.title&&d>0&&M(N.chapter)===d);return D.length?`<div class="chapter-honors">
1126      <div class="cast-hdr">Honored This Chapter</div>
1127      <ul class="chapter-honor-list">${D.map(N=>{const G=N.party?"the whole party":(N.honorees||[]).map(W=>((allEntities||[]).find(z=>z.id===W)||{}).name).filter(Boolean).join(", "),F=N.honorific?`<span class="chapter-honor-bestow">Title bestowed: <b>${escHtml(N.honorific)}</b>${G?" to "+escHtml(G):""}</span>`:"";return`<li class="chapter-honor"><span class="chapter-honor-t">${escHtml(N.title)}</span>${F}</li>`}).join("")}</ul>
1128      <button type="button" class="chapter-honors-link" data-chr-honors>See the Honors Roll &rsaquo;</button>
1129    </div>`:""})(),P=r.legend&&r.legend.spec?`<div class="chapter-legend">
1130      <div class="cast-hdr">Legend in the Making</div>
1131      <div class="chapter-legend-card">${_qcRenderInline(r.legend.spec,.5)}</div>
1132      ${_tfCanShareLegend(r.legend)?'<button type="button" class="btn-new btn-sm chapter-legend-share" data-chr-legend-share>Share the card</button>':""}
1133    </div>`:"",B=m?`<a class="prev" data-open="${m.x.id}" role="button" tabindex="0"><span class="lab">&lsaquo; ${_chr2ChapLabel(m.n)}</span><span class="ti">${escHtml(m.x.title||"")}</span></a>`:"<span></span>",H=f?`<a class="next" data-open="${f.x.id}" role="button" tabindex="0"><span class="lab">${_chr2ChapLabel(f.n)} &rsaquo;</span><span class="ti">${escHtml(f.x.title||"")}</span></a>`:"<span></span>";a.innerHTML=`<div class="chron-shell">
1134    <aside class="chron-rail">
1135      <div class="chron-rail-head">
1136        <div class="chron-rail-top">
1137          <span class="chron-rail-eyebrow">${escHtml(_chr2TitleOrName())}</span>
1138          <button class="chron-indexlink back" data-chron-index>&lsaquo; Index</button>
1139        </div>
1140        <input class="chron-rail-search" data-clearable placeholder="Search chapters\u2026" aria-label="Search chapters">
1141        <button class="chron-rail-sort" data-chron-sort type="button"></button>
1142      </div>
1143      <div class="chron-list" id="chron-list"></div>
1144    </aside>
1145    <div class="chron-right">
1146      <div class="chron-page" id="chron-read">
1147        <article class="chapter" data-layout="${v}">
1148          <div class="chapter-crumb">
1149            <button class="back" data-chron-index>&lsaquo; Chronicle</button>
1150            <span>\xB7</span><span class="cur">${escHtml(_chr2TitleOrName())}</span>
1151            ${r.summary?'<div class="actions"><button type="button" class="chapter-share" data-chr-share title="Share this chapter" aria-label="Share this chapter"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m8 7 4-4 4 4"/><path d="M5 13v6a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6"/></svg><span>Share</span></button></div>':""}
1152          </div>
1153          <header class="chapter-head">
1154            <span class="chapter-num" id="cr-num">${_chr2ChapLabel(d)}</span>
1155            <div class="chapter-title-row">
1156              <h1 class="chapter-title" id="cr-title">${escHtml(r.title||"Untitled session")}</h1>
1157              ${_isGm()?_tfTitleAltsHintDue(r)?`<button type="button" class="chapter-title-edit chapter-title-edit--hinted" data-title-edit title="The bard wrote other titles for this session" aria-label="Rename chapter, or pick one of the other titles the bard wrote">${_CHR2_PENCIL_SVG}<span class="chapter-title-edit-hint">Try another title</span></button>`:`<button type="button" class="chapter-title-edit" data-title-edit title="Rename chapter" aria-label="Rename chapter">${_CHR2_PENCIL_SVG}</button>`:""}
1158            </div>
1159            <div class="chapter-dateline" id="cr-dateline">
1160              <span>Session ${d}</span><span class="dot"></span>
1161              ${g?`<span>${escHtml(g)}</span>`:""}
1162              ${_isGm()?`<button type="button" class="chapter-date-edit" data-date-edit title="${g?"Edit session date":"Set session date"}" aria-label="Edit session date">${_CHR2_PENCIL_SVG}</button>`:""}
1163            </div>
1164            ${k}
1165            ${C}
1166            <div class="chapter-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div>
1167          </header>
1168          ${_chr2HeaderArt(r)}
1169          ${_chapterBuildBlockHtml(r,_isGm(),_isRecapInProgress(r))}
1170          <div class="chapter-body" id="cr-body">${_isGm()&&r.summary?`<button type="button" class="chapter-body-edit" data-body-edit title="Edit chapter prose" aria-label="Edit chapter prose">${_CHR2_PENCIL_SVG}</button>`:""}${b}</div>
1171          ${I}
1172          <div class="chapter-foot">
1173            <div class="chapter-debuts" id="cr-debuts" hidden></div>
1174            <div class="cast">
1175              <div class="cast-hdr">In This Chapter</div>
1176              <div class="cast-row">${q}</div>
1177            </div>
1178            ${P}
1179            ${T}
1180            <nav class="chapter-nav">${B}${H}</nav>
1181            <div class="colophon"><span class="mark">${_CHR2_MARK_SVG} Threadfall Chronicle</span><span>${escHtml(o)}</span></div>
1182          </div>
1183        </article>
1184      </div>
1185    </div>
1186  </div>`,a.querySelectorAll("[data-chron-index]").forEach(M=>M.addEventListener("click",()=>renderChronicleTimeline())),a._chrSid=e,a._chrEditDelegated||(a._chrEditDelegated=!0,a.addEventListener("click",M=>{const D=a._chrSid,O=M.target.closest("[data-chr-share]");if(O){M.stopPropagation(),_chrShareMenu(D,O);return}const N=M.target.closest("[data-chr-legend-share]");if(N){M.stopPropagation();const G=(allSessions||[]).find(W=>W.id===D),F=G&&G.legend;F&&_tfLegendShareMenu({id:F.card_id,status:F.status,png_url:F.png_url,share_token:F.share_token,spec:F.spec},N,_tfCanShareLegend(F));return}if(M.target.closest("[data-chr-honors]")){const G=currentCampaign&&currentCampaign.id;G&&_partyHandleHash(`#party/${G}/annals`);return}if(M.target.closest("[data-bardic-edit]"))_chr2OpenBardicEditor(a,D);else if(M.target.closest("[data-cap-edit]"))_chr2OpenCaptionEditor(a,D);else if(M.target.closest("[data-title-edit]"))_chr2OpenChapterTitleEditor(a,D);else if(M.target.closest("[data-date-edit]"))_chr2OpenDatelineEditor(a,D);else if(M.target.closest("[data-body-edit]"))_chr2OpenChapterBodyEditor(a,D);else if(M.target.closest("[data-brief-edit]"))_chr2OpenBriefEditor(a,D);else if(M.target.closest("[data-brief-toggle]")){const G=M.target.closest("[data-brief-toggle]"),F=G.closest(".chapter-brief"),W=F&&F.querySelector(".chapter-brief-body"),z=G.getAttribute("aria-expanded")!=="true";G.setAttribute("aria-expanded",z?"true":"false"),G.title=z?"Fold the TL;DR":"Show the whole TL;DR",F&&F.classList.toggle("is-open",z),W&&(W.hidden=!z)}})),a.querySelector("[data-chr-generate]")?.addEventListener("click",()=>{_fireRecapForSession(e,{btnEl:a.querySelector(".chr-generate-btn"),statusEl:a.querySelector(`#chr-gen-status-${e}`),resultEl:a.querySelector(`#chr-gen-result-${e}`),onComplete:()=>renderChronicleRecap(e,{background:!0})})}),a.querySelector("[data-chr-build]")&&_isRecapInProgress(r)&&_maybeStartRecapPoll(e),a.querySelector("[data-chr-build]")?.addEventListener("click",()=>{_fireRecapForSession(e,{from_chapter:!0,btnEl:a.querySelector(".chr-build-codex-btn"),statusEl:a.querySelector(`#chr-gen-status-${e}`),resultEl:a.querySelector(`#chr-gen-result-${e}`),onComplete:()=>renderChronicleRecap(e,{background:!0})})}),a.querySelector("[data-chr-force]")?.addEventListener("click",async()=>{(typeof confirmDialog!="function"||await confirmDialog({title:"Write this chapter anyway?",body:"We read this session as table setup rather than play. Writing it will use a session credit.",confirmText:"Write it"}))&&_fireRecapForSession(e,{force_story:!0,btnEl:a.querySelector(".chr-prep-override"),statusEl:a.querySelector(`#chr-gen-status-${e}`),resultEl:a.querySelector(`#chr-gen-result-${e}`),onComplete:()=>renderChronicleRecap(e,{background:!0})})}),_p2HydrateReactions(a,currentCampaign&&currentCampaign.id),a._chrRailSort||(a._chrRailSort=_chr2GetRailSort()),_chr2RenderRail(a,e);const A=a.querySelector(".chron-rail-search");if(A&&(A.addEventListener("input",()=>
1186_chr2ApplyRailFilter(a)),A.addEventListener("keydown",M=>{M.key==="Escape"&&(M.preventDefault(),A.value="",_chr2ApplyRailFilter(a))})),a.querySelector("[data-chron-sort]")?.addEventListener("click",()=>{const M=["act","oldest","newest"];a._chrRailSort=M[(M.indexOf(a._chrRailSort)+1)%M.length];try{localStorage.setItem("tf_chr_railsort",a._chrRailSort)}catch{}_chr2RenderRail(a,e)}),typeof _ensureSearchClears=="function"&&_ensureSearchClears(),a.querySelectorAll(".chapter-nav [data-open]").forEach(M=>{M.addEventListener("click",()=>renderChronicleRecap(+M.dataset.open)),M.addEventListener("keydown",D=>{(D.key==="Enter"||D.key===" ")&&(D.preventDefault(),M.click())})}),a.querySelectorAll(".cast .cx2-rail-item[data-eid]").forEach(M=>{_chr2Activate(M,()=>{const D=+M.dataset.eid;D&&typeof renderCodexEntry=="function"&&renderCodexEntry(D)})}),_chr2RenderDebutBoxes(a,r),_chr2RenderInlineImages(a,r),_chr2RenderInlineLibraryImages(a,r),_chr2RenderSpoils(a,r),typeof _mountReferralPromoSlots=="function"&&_mountReferralPromoSlots(),_chr2WireCoverDrag(a,r),_isGm()){const M=a.querySelector(".scene-banner .art--scene .art-drop"),D=M&&M.closest(".art--scene");D&&(D.classList.add("art-drop-zone"),D.addEventListener("click",()=>_chr2OpenHeaderArtModal(r)),D.addEventListener("dragover",O=>{O.preventDefault(),D.classList.add("drag-over")}),D.addEventListener("dragleave",()=>D.classList.remove("drag-over")),D.addEventListener("drop",O=>{O.preventDefault(),D.classList.remove("drag-over");const N=O.dataTransfer&&O.dataTransfer.files&&O.dataTransfer.files[0];N&&_chr2UploadHeaderArt(r,N)}))}if(_isGm()){const M=document.createElement("button");M.type="button",M.className="chr2-gm-trigger",M.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h11l3 3v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/><path d="M9 7h6M9 11h6M9 15h4"/></svg><span class="chr2-gm-trigger-lbl">Session Tools</span>',M.addEventListener("click",()=>_chr2OpenGmDrawer(e)),a.appendChild(M)}const x=a.querySelector(".chron-right");if(x){const M=i??0;x.scrollTop=M,i!=null&&requestAnimationFrame(()=>{const D=a.querySelector(".chron-right");D&&(D.scrollTop=M)})}}async function _chronicleHandleHash(){const e=(location.hash||"").match(/^#chronicle\/(\d+)(?:\/(\d+))?$/);if(!e)return!1;const t=+e[1],n=e[2]?+e[2]:null;if(typeof getStoredToken!="function"||!getStoredToken())return!1;const a=++_tfNavSeq;try{return(!currentCampaign||currentCampaign.id!==t)&&await bootWithCampaignId(t),_tfNavStale(a)||(n?renderChronicleRecap(n):renderChronicleTimeline()),!0}catch(s){return console.warn("[chronicle] deep-link failed",s),!1}}window.addEventListener("hashchange",_chronicleHandleHash),window.openChronicle=renderChronicleTimeline;function _entityLinkActivate(e){const t=document.getElementById("live-session-screen");if(t&&!t.classList.contains("hidden")){_showEntityPreview(e);return}typeof navigateToEntity=="function"?navigateToEntity(e):_showEntityPreview(e)}document.addEventListener("click",e=>{const t=e.target.closest(".entity-link");t&&_entityLinkActivate(parseInt(t.dataset.id))});async function _p2DoReact(e,t){if(_isReadOnlyRole())return;const n=currentCampaign&&currentCampaign.id,a=e&&e.dataset.quoteId;if(!(!e||!n||!a||!t))try{const s=await api("POST",`/api/campaigns/${n}/quotes/${a}/react`,{reaction:t});_p2ReactBarSet(e,{id:a,reactions:s.counts,reactors:s.reactors,mine:s.mine});const i=e.closest&&e.closest(".p2-quote");i&&i._p2q&&String(i._p2q.id)===String(a)&&(i._p2q.reactions=s.counts,i._p2q.reactors=s.reactors,i._p2q.mine=s.mine)}catch(s){typeof showToast=="function"&&showToast("Reaction failed: "+(s.message||s),3e3)}}let _p2PickerBar=null;function _p2CloseReactPicker(){document.getElementById("p2-react-pop")?.remove(),_p2PickerBar=null,document.removeEventListener("click",_p2PickerOutside,!0),document.removeEventListener("keydown",_p2PickerEsc,!0)}function _p2PickerOutside(e){const t=document.getElementById("p2-react-pop");t&&!t.contains(e.target)&&!e.target.closest("[data-react-more]")&&_p2CloseReactPicker()}function _p2PickerEsc(e){e.key==="Escape"&&_p2CloseReactPicker()}function _p2OpenReactPicker(e){if(_isReadOnlyRole())return;if(document.getElementById("p2-react-pop")){_p2CloseReactPicker();return}const t=e.closest(".p2-react");if(!t)return;_p2PickerBar=t;const n=document.createElement("div");n.id="p2-react-pop",n.className="p2-react-pop",n.setAttribute("role","menu"),n.innerHTML=_P2_REACTIONS.filter(([,,,o])=>!o).map(([o,r,c])=>`<button class="p2-react-pick" type="button" data-react-pick="${o}
1186" title="${escHtml(c)}" aria-label="${o}">${r}</button>`).join(""),document.body.appendChild(n);const a=e.getBoundingClientRect(),s=Math.min(Math.max(8,a.left),window.innerWidth-n.offsetWidth-8);let i=a.top-n.offsetHeight-6;i<8&&(i=a.bottom+6),n.style.left=s+"px",n.style.top=i+"px",setTimeout(()=>{document.addEventListener("click",_p2PickerOutside,!0),document.addEventListener("keydown",_p2PickerEsc,!0)},0)}document.addEventListener("click",async e=>{const t=e.target.closest(".p2-react-more");if(t){e.preventDefault(),_p2OpenReactPicker(t);return}const n=e.target.closest(".p2-react-pick");if(n){e.preventDefault();const s=_p2PickerBar;_p2CloseReactPicker(),await _p2DoReact(s,n.dataset.reactPick);return}const a=e.target.closest(".p2-react-btn");a&&await _p2DoReact(a.closest(".p2-react"),a.dataset.react)});async function _p2HydrateReactions(e,t){if(t=t||currentCampaign&&currentCampaign.id,!e||!t)return;const n=[...e.querySelectorAll(".p2-react[data-quote-id]")].filter(s=>s.dataset.quoteId);if(!n.length)return;const a=[...new Set(n.map(s=>s.dataset.quoteId))];try{const s=await api("GET",`/api/campaigns/${t}/quotes/reactions?ids=${a.join(",")}`);n.forEach(i=>{const o=s[i.dataset.quoteId];o&&_p2ReactBarSet(i,{id:i.dataset.quoteId,reactions:o.counts,reactors:o.reactors,mine:o.mine})})}catch{}}document.addEventListener("keydown",e=>{if(e.key!=="Enter"&&e.key!==" ")return;const t=e.target.closest?.(".entity-link");t&&(e.preventDefault(),_entityLinkActivate(parseInt(t.dataset.id)))});const _EPK_HOVER_MS=500,_EPK_WARM_MS=100,_EPK_FOCUS_MS=200,_EPK_HIDE_MS=150,_EPK_WARM_WINDOW=400;let _epkShowT=null,_epkHideT=null,_epkWarmUntil=0,_epkAnchor=null,_epkLastPT="mouse";document.addEventListener("pointerdown",e=>{e.pointerType&&(_epkLastPT=e.pointerType)},{capture:!0,passive:!0});function _epkPointerOk(e){return e!=="touch"}function _epkEl(){let e=document.getElementById("epk-card");return e||(e=document.createElement("div"),e.id="epk-card",e.className="epk-card cool-paper-surface",e.setAttribute("role","tooltip"),document.body.appendChild(e)),e}function _epkDescFor(e){let t="";if(e.type==="spell"){let a={};try{a=(typeof e.dnd_beyond_data=="string"?JSON.parse(e.dnd_beyond_data):e.dnd_beyond_data)||{}}catch{}const s=a.spell_level!=null?a.spell_level===0?"Cantrip":`Level ${a.spell_level}`:"",i=(a.spell_school||"").replace(/^\w/,o=>o.toUpperCase());t=[s,i].filter(Boolean).join(" \xB7 ")}else if(e.type==="item"){const a=typeof _itemRarityFor=="function"?_itemRarityFor(e):null;a&&(t=a.label)}let n=(e.snippet||"").trim();if(!n&&e.description){const a=e.description.replace(/^#{1,6}\s+.+\n?/gm,"").trim().split(/\s+/);n=a.slice(0,30).join(" ")+(a.length>30?"\u2026":"")}if((e.type==="pc"||e.type==="companion")&&(e.player_name||"").trim()){const a=`played by ${e.player_name.trim()}`;n=n?`${n} \xB7 ${a}`:a.replace(/^p/,"P")}return[t,n].filter(Boolean).join(" \xB7 ")}function _epkShow(e,t){if(!document.contains(e))return;const n=(allEntities||[]).find(v=>v.id===t);if(!n||_hiddenFromViewer(n))return;const a=ENTITY_CFG[n.type]||ENTITY_CFG.npc,s=_CX2_TC[n.type]||"npc",i=_CX2_GL[n.type]||"npc",o=typeof _entityImageUrl=="function"?_entityImageUrl(n):n.image_url,r=n.is_dead?" is-dead":n.is_inactive?" is-departed":"",c=n.is_dead?`<span class="epk-life">${_CX2_ACT_ICO.skull}Fallen</span>`:n.is_inactive?'<span class="epk-life">Departed</span>':"",l=_epkDescFor(n),d=_epkEl();d.dataset.eid=String(t),d.innerHTML=`<div class="epk-hdr"><span class="epk-thumb ${s}${o?" has-img":""}${r}">`+(o?`<img src="${escHtml(thumbUrl(o)||o)}" alt="" loading="lazy">`:`<svg aria-hidden="true"><use href="#tf-glyph-${i}"/></svg>`)+`</span><span class="epk-titlebox"><span class="epk-name">${escHtml(n.name||"")}</span><span class="epk-subline"><span class="epk-badge">${escHtml(a.label||n.type)}</span>${c}</span></span></div>`+(l?`<p class="epk-desc">${escHtml(l)}</p>`:"");const u=e.getBoundingClientRect();d.classList.remove("is-closing"),d.style.visibility="hidden",d.classList.add("is-open");const p=d.offsetHeight,m=d.offsetWidth,f=8;let g=u.top-p-f;g<64&&(g=u.bottom+f);let h=Math.min(Math.max(f,u.left),window.innerWidth-m-f);d.style.top=Math.round(g)+"px",d.style.left=Math.round(h)+"px",d.style.visibility="",_epkAnchor=e,e.setAttribute("aria-describedby","epk-card")}function _epkClose(){clearTimeout(_epkShowT),clearTimeout(_epkHideT);const e=document.getElementById("epk-card");if(e&&e.classList.contains("is-open")&&(e.classList.remove("is-open"),e.classList.add("is-closing"),_epkWarmUntil=Date.now()+_EPK_WARM_WINDOW),_epkAnchor){try{_epkAnchor.removeAttribute("aria-describedby")}catch{}_epkAnchor=null}}function _epkScheduleShow(e,t){clearTimeout(_epkShowT),clearTimeout(_epkHideT);const n=parseInt(e.dataset.id);if(!n)return;const a=document.querySelector(".epk-card.is-open");if(a&&a.dataset.eid===String(n)&&_epkAnchor===e)return;const s=!!a||Date.now()<_epkWarmUntil;_epkShowT=setTimeout(()=>_epkShow(e,n),t??(s?_EPK_WARM_MS:_EPK_HOVER_MS))}
1186function _epkScheduleHide(){if(clearTimeout(_epkShowT),clearTimeout(_epkHideT),_epkAnchor&&!document.contains(_epkAnchor)){_epkClose();return}_epkHideT=setTimeout(_epkClose,_EPK_HIDE_MS)}document.addEventListener("pointerover",e=>{if(!_epkPointerOk(e.pointerType))return;if(e.target.closest?.("#epk-card")){clearTimeout(_epkHideT);return}const t=e.target.closest?.(".entity-link");if(!t||t.closest("#live-session-screen"))return;const n=document.activeElement;if(n&&(n.tagName==="TEXTAREA"||n.tagName==="INPUT"&&n.type==="text"||n.isContentEditable))return;const a=document.getSelection&&document.getSelection();a&&!a.isCollapsed||_epkScheduleShow(t)}),document.addEventListener("pointerout",e=>{const t=e.target.closest?.(".entity-link, #epk-card");if(!t)return;const n=e.relatedTarget;n&&(n.closest?.("#epk-card")||n.closest?.(".entity-link")===t)||_epkScheduleHide()}),document.addEventListener("focusin",e=>{if(!_epkPointerOk(_epkLastPT))return;const t=e.target.closest?.(".entity-link");!t||t.closest("#live-session-screen")||_epkScheduleShow(t,_EPK_FOCUS_MS)}),document.addEventListener("focusout",e=>{e.target.closest?.(".entity-link")&&_epkScheduleHide()}),document.addEventListener("keydown",e=>{e.key==="Escape"&&_epkClose()}),document.addEventListener("scroll",()=>{_epkAnchor&&_epkClose()},{capture:!0,passive:!0}),document.addEventListener("click",e=>{_epkAnchor&&!e.target.closest?.("#epk-card")&&_epkClose()});let allSessions=[];function _tfSortSessionsNewestFirst(e){return(e||[]).slice().sort((t,n)=>{const a=t&&t.date?String(t.date):"",s=n&&n.date?String(n.date):"";return a!==s?a?s&&s<a?-1:1:-1:(n&&n.id||0)-(t&&t.id||0)})}function _tfResortSessions(){allSessions=_tfSortSessionsNewestFirst(allSessions)}let expandedSessionId=null,sessionEntries={},liveEntriesCache={},sessionTallyCache={},_tallyDraft={};function _ensureTallyDraft(e){if(_tallyDraft[e])return _tallyDraft[e];const t=sessionTallyCache[e],n=t&&t!=="loading"?t:{kills:[],collateral:[],conquests:[],near_wipes:[],key_items:[],downs:[],legends:[],coin:null};return _tallyDraft[e]={kills:JSON.parse(JSON.stringify(n.kills||[])),collateral:JSON.parse(JSON.stringify(n.collateral||[])),conquests:JSON.parse(JSON.stringify(n.conquests||[])),near_wipes:JSON.parse(JSON.stringify(n.near_wipes||[])),key_items:JSON.parse(JSON.stringify(n.key_items||[])),downs:JSON.parse(JSON.stringify(n.downs||[])),coin:n.coin?JSON.parse(JSON.stringify(n.coin)):{}},_tallyDraft[e]}function _emptyTallyShell(e){return`<div class="tally-panel">
1187    <div class="tally-subsection"><div class="tally-subhdr">\u{1F5E1} Kills <span class="tally-subcount">(0)</span></div>
1188      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1189      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="kills">+ Add Kills</button>
1190    </div>
1191    <div class="tally-subsection"><div class="tally-subhdr">\u2620 Collateral Damage <span class="tally-subcount">(0)</span></div>
1192      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1193      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="collateral">+ Add Collateral Damage</button>
1194    </div>
1195    <div class="tally-subsection"><div class="tally-subhdr">\u2665 Conquests <span class="tally-subcount">(0)</span></div>
1196      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1197      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="conquests">+ Add Conquests</button>
1198    </div>
1199    <div class="tally-subsection"><div class="tally-subhdr">\u{1F480} Near-Wipes <span class="tally-subcount">(0)</span></div>
1200      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1201      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="near_wipes">+ Add Near-Wipe</button>
1202    </div>
1203    <div class="tally-subsection"><div class="tally-subhdr">\u{1F48E} Key Items <span class="tally-subcount">(0)</span></div>
1204      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1205      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="key_items">+ Add Key Item</button>
1206    </div>
1207    <div class="tally-subsection"><div class="tally-subhdr">\u{1FA78} Downs <span class="tally-subcount">(0)</span></div>
1208      <div class="tally-rows"><div class="tally-empty">No entries yet.</div></div>
1209      <button class="tally-add-row btn-ghost btn-xs" data-sid="${e}" data-sub="downs">+ Add Down</button>
1210    </div>
1211  </div>`}async function loadSessionEntries(e){const t=currentCampaign?.id;try{const n=await api("GET",`/api/campaigns/${t}/sessions/${e}/entries`);
1211if(_campaignStale(t))return;sessionEntries[e]=n}catch{_campaignStale(t)||(sessionEntries[e]=[])}}document.querySelector('[data-tab="sessions"]').addEventListener("click",loadSessions);async function loadSessions(e){if(!currentCampaign)return;const t=currentCampaign.id;try{let n;if(e&&Array.isArray(e.items)){n=e.items;let a=e.next_cursor||null;for(;a;){const s=await api("GET",`/api/campaigns/${t}/sessions?cursor=${a}`);if(!currentCampaign||currentCampaign.id!==t)return;n=n.concat(s.items||[]),a=s.next_cursor||null}}else n=await _apiAllPages(`/api/campaigns/${t}/sessions`);if(!currentCampaign||currentCampaign.id!==t)return;allSessions=_tfSortSessionsNewestFirst(n),_chr2IsShown()&&typeof renderChronicleTimeline=="function"&&renderChronicleTimeline()}catch(n){if(!currentCampaign||currentCampaign.id!==t)return;console.error("Sessions refresh failed:",n.message)}}function _chr2IsShown(){const e=document.getElementById("tab-chronicle2");return!!e&&!e.classList.contains("hidden")}const _postSessionDismissedQuotes={};function _extractSpeakerSamples(e,t){const n=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),a=[];for(const r of e.matchAll(new RegExp(`^\\[\\d+:\\d+\\] ${n}:\\s*(.+)$`,"gm"))){const c=r[1].trim();c&&a.push(c)}if(!a.length)for(const r of e.matchAll(new RegExp(`^${n}:\\s*(.+)$`,"gm"))){const c=r[1].trim();c&&a.push(c)}const s=a.filter(r=>r.split(/\s+/).filter(Boolean).length>=_SPK_MIN_WORDS).sort((r,c)=>c.split(/\s+/).length-r.split(/\s+/).length),i=new Set,o=[];for(const r of s){const c=r.slice(0,40).toLowerCase();if(!i.has(c)&&(i.add(c),o.push(r),o.length>=_SPK_MAX_CANDIDATES))break}return o}function _buildSpeakerBar(e,t){const n=new Map;for(const r of t.matchAll(/^\[\d+:\d+\] ([^:\n]+):/gm)){const c=r[1].trim();n.has(c)||n.set(c,c)}if(!n.size){const r=new Map;for(const c of t.matchAll(/^([^:\n]{1,40}):\s+\S/gm)){const l=c[1].trim();l&&r.set(l,(r.get(l)||0)+1)}for(const[c,l]of r)(/^[A-Z]{1,2}$/.test(c)||l>=3)&&n.set(c,c)}if(!n.size)return"";const a=allEntities.filter(r=>r.type==="pc"&&!r.is_dead&&!r.is_inactive);function s(r){const c=a.find(l=>l.name===r);return c?_avatarHtml(c.image_url||null,c.name,24):r==="GM"?_avatarHtml(null,"GM",24):_avatarHtml(null,r,24)}const i=_postSessionDismissedQuotes[e]||{};return`<div class="spk-bar"><span class="spk-bar-hdr">\u{1F399} Speakers:</span>${[...n.keys()].map(r=>{const c=r==="GM",l=r.length<=2&&/^[A-Z]+$/.test(r),d=r,u=a.map(E=>{const k=E.name===d?"selected":"";return`<option value="${escHtml(E.name)}" ${k}>${escHtml(E.name)}</option>`}).join(""),p=c?"selected":"",m=l?"selected":"",f=s(c?"GM":a.find(E=>E.name===d)?.name||d),g=i[r]||new Set,h=_extractSpeakerSamples(t,r),v=h.filter(E=>!g.has(E)),b=v[0]||"",y=b?`"${escHtml(b.length>_SPK_DISPLAY_CAP?b.slice(0,_SPK_DISPLAY_CAP)+"\u2026":b)}"`:h.length?"<em>No clean samples left. Review the transcript to assign.</em>":"<em>No long-form sample found, only short utterances.</em>",_=Math.max(0,v.length-1),w=_>0?`<span class="spk-quote-more">+${_}</span>`:"",$=b?`<button class="spk-quote-dismiss" data-sid="${e}" data-tok="${escHtml(r)}" data-sample="${escHtml(b)}" title="Not a clean single-voice sample, show the next candidate">\u2715</button>`:"";return`<div class="spk-row" data-sid="${e}" data-label="${escHtml(r)}">
1212      <div class="spk-row-top">
1213        <span class="spk-token-chip">${escHtml(r)}</span>
1214        <span class="spk-arrow">\u2192</span>
1215        <span class="spk-avatar-preview" data-sid="${e}" data-tok="${escHtml(r)}">${f}</span>
1216        <select class="spk-select" data-sid="${e}" data-label="${escHtml(r)}">
1217          <option value="" ${m}>(Unlabeled)</option>
1218          ${u}
1219          <option value="GM" ${p}>GM</option>
1220        </select>
1221      </div>
1222      <div class="spk-row-sample">
1223        <span class="spk-quote-text">${y}${w}</span>
1224        ${$}
1225      </div>
1226    </div>`}).join("")}</div>`}function _spkDismissHandler(e){const t=e.target.closest(".spk-quote-dismiss");if(!t)return;const n=parseInt(t.dataset.sid),a=t.dataset.tok,s=t.dataset.sample;if(!n||!a||!s)return;const i=_postSessionDismissedQuotes[n]||(_postSessionDismissedQuotes[n]={});(i[a]||(i[a]=new Set)).add(s);const r=allSessions.find(u=>u.id===n);if(!r?.raw_transcript)return;const c=t.closest(".spk-row");if(!c)return;const l=_buildSpeakerBar(n,r.raw_transcript),d=c.closest(".spk-bar");if(d){const u=document.createElement("div");u.innerHTML=l,d.outerHTML=u.innerHTML}}async function _spkRelabelHandler(e){const t=e.target.closest(".spk-select");if(!t)return;const n=parseInt(t.dataset.sid),a=t.dataset.label,s=t.value;if(!s)return;const i=allSessions.find(u=>u.id===n);if(i&&typeof i.raw_transcript!="string"&&await _tfSessionTranscript(n),!i?.raw_transcript)return;const o=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=(i.raw_transcript.match(new RegExp(`^(\\[\\d+:\\d+\\] ${o}:|${o}: )`,"gm"))||[]).length,l=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let d=i.raw_transcript.replace(new RegExp(`^(\\[\\d+:\\d+\\]) ${l}:`,"gm"),`$1 ${s}:`).replace(new RegExp(`^${l}:( )`,"gm"),`${s}:$1`);
1226if(d!==i.raw_transcript)try{await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${n}`,{raw_transcript:d}),allSessions=allSessions.map(h=>h.id===n?{...h,raw_transcript:d}:h);const u=document.querySelector(`.session-section-pre[data-sid="${n}"]`);u&&(u.textContent=d),t.dataset.label=s;const p=document.querySelector(`.spk-avatar-preview[data-sid="${n}"][data-tok="${CSS.escape(a)}"]`);if(p){const h=allEntities.find(v=>v.type==="pc"&&v.name===s);p.innerHTML=_avatarHtml(h?.image_url||null,s,24),p.dataset.tok=s}const m=t.closest(".spk-row");if(m){m.dataset.label=s;const h=m.querySelector(".spk-token-chip");h&&(h.textContent=s),t.dataset.label=s}const f=a.toUpperCase()==="GM"||allEntities.some(h=>h.type==="pc"&&h.name.toLowerCase()===a.toLowerCase());f&&api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${n}/relabel-speaker`,{from_label:a,to_label:s,record_only:!0}).catch(()=>{});const g=f&&i.summary?" The recap was written before this fix \u2014 Regenerate to apply it.":"";c>=20?showToast(`${c} lines \u2192 ${s}. If the sample looked wrong, reassign or X the next sample.${g}`,g?8e3:5e3):showToast(`All "${a}" lines \u2192 ${s} (${c})${g}`,g?7e3:2500)}catch(u){showToast("Failed to save: "+u.message)}}const TF_AUDIO_MULTIPART_MAX_BYTES=4*1024*1024,TF_AUDIO_DIRECT_MAX_BYTES=100*1024*1024,TF_RECORDER_AUDIO_BPS=48e3;async function _tfUploadLiveAudioDirect(e,t,n,a){if(e.size>TF_AUDIO_DIRECT_MAX_BYTES){const l=new Error(`Recording is ${Math.round(e.size/1048576)} MB; the upload limit is ${TF_AUDIO_DIRECT_MAX_BYTES/1048576} MB.`);throw l.status=413,l}const s=await fetch(`/api/campaigns/${t}/sessions/${n}/audio-upload-params`,{method:"POST",credentials:"include",headers:a?{Authorization:"Bearer "+a}:{}});if(!s.ok){const l=new Error(await _tfResErr(s));throw l.status=s.status,l}const i=await s.json(),o=new FormData;o.append("file",e,"session.webm"),o.append("api_key",i.api_key),o.append("timestamp",String(i.timestamp)),o.append("signature",i.signature),o.append("folder",i.folder);const r=await fetch(i.upload_url,{method:"POST",body:o});if(!r.ok){const l=new Error("The recording could not be uploaded.");throw r.status===413&&(l.status=413),l}const c=(await r.json()).public_id;if(!c)throw new Error("The upload returned no reference.");return c}async function _tfSendRecordingForTranscript(e,t,n,a,s){const i=new FormData;a&&a.length>0&&i.append("live_segments",JSON.stringify(a)),e.size>TF_AUDIO_MULTIPART_MAX_BYTES?(showToast("Uploading the recording\u2026"),i.append("audio_public_id",await _tfUploadLiveAudioDirect(e,t,n,s)),showToast("Transcribing audio\u2026 this may take a minute.")):i.append("audio",e,"session.webm");const o=await fetch(`/api/campaigns/${t}/sessions/${n}/transcribe`,{method:"POST",credentials:"include",headers:s?{Authorization:"Bearer "+s}:{},body:i});if(!o.ok){let r=o.statusText;try{r=(await o.json()).detail||r}catch{}const c=new Error(r);throw c.status=o.status,c}return await o.json()}let _tfHeldRecording=null;const _TF_PERMANENT_UPLOAD_STATUSES=new Set([400,403,404,409,413,415,422]);function _tfFailureIsRetryable(e){return!_TF_PERMANENT_UPLOAD_STATUSES.has(Number(e&&e.status))}function _tfHoldFailedRecording(e,t,n,a,s,i){_tfHeldRecording={blob:e,campaignId:t,sessionId:n.id,title:n.title||"",liveSegments:Array.isArray(a)?a.slice():[],priorChars:Number(s)||0,heldAt:Date.now(),retryable:_tfFailureIsRetryable(i),reason:String(i&&i.message?i.message:i||""),ownerSub:_tfJwtSub(getStoredToken())},_liveTel("held_recording",{bytes:e.size,sessionId:n.id,retryable:_tfHeldRecording.retryable}),_tfShowHeldRecordingBanner()}function _tfShowHeldRecordingBanner(e){const t=_tfHeldRecording;if(!t)return;const n=[{label:"Download Recording",cls:"btn-ghost btn-sm",onClick:_tfDownloadHeldRecording}];t.retryable&&n.unshift({label:"Retry Upload",cls:"btn-new btn-sm",onClick:_tfRetryHeldRecording});const a=t.title?`Session "${t.title}". `:"";_showCaptureAlert("Upload failed. Your recording is still in this browser.","error",a+(e||(t.retryable?"It has not reached our servers and will not survive closing this tab.":t.reason||"It has not reached our servers and will not survive closing this tab.")),n)}async function _tfRetryHeldRecording(e){const t=_tfHeldRecording;if(!(!t||t.busy)){t.busy=!0,e&&(e.disabled=!0);try{let n=null;try{n=await api("GET",`/api/campaigns/${t.campaignId}/sessions/${t.sessionId}`)}catch{}const a=n?Number(n.transcript_chars)||0:null;if(a!==null&&a>t.priorChars+500){_tfShowHeldRecordingBanner("The server already has more transcript than it did before the failed save. Check the session before retrying.");return}const s=await _tfSendRecordingForTranscript(t.blob,t.campaignId,t.sessionId,t.liveSegments,getStoredToken());_tfHeldRecording=null,currentCampaign?.id===t.campaignId&&s?.session&&(allSessions=allSessions.map(i=>i.id===s.session.id?s.session:i));try{localStorage.removeItem(_transcriptBackupKey(t.campaignId,t.sessionId))}catch{}_hideCaptureAlert(),showToast(`Transcript saved for "${t.title}", review and run recap.`),_liveTel("held_recording_retry_ok",{bytes:t.blob.size,sessionId:t.sessionId})}catch(n){console.error("[transcribe-retry]",n),_liveTel("held_recording_retry_failed",{msg:String(n&&n.message||n).slice(0,200)}),_tfShowHeldRecordingBanner("Retry failed: "+(n&&n.message?n.message:n))}finally{_tfHeldRecording&&(_tfHeldRecording.busy=!1),e&&(e.disabled=!1)}}}function _tfDownloadHeldRecording(){const e=_tfHeldRecording;if(!e)return;const t=(e.title||"session").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"session",n=new Date(e.heldAt).toISOString().slice(0,10),a=/webm/.test(e.blob.type||"")?"webm":"audio",s=document.createElement("a");s.href=URL.createObjectURL(e.blob),s.download=`threadfall-${t}-${n}.${a}`,document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>{try{URL.revokeObjectURL(s.href)}catch{}},6e4),_liveTel("held_recording_download",{bytes:e.blob.size,sessionId:e.sessionId})}
1226async function _reTranscribeAudio(e,t,n){if(e.size>100*1024*1024)return showToast("File too large, maximum 100 MB."),!1;if(!await confirmDialog({title:"Replace Transcript",message:`Replace the entire transcript for this session with the transcription of "${e.name}"? This cannot be undone.`,confirmLabel:"Replace"}))return!1;const s=n?n.innerHTML:"";n&&(n.disabled=!0);try{n&&(n.innerHTML="\u23F3 Uploading\u2026");const i=await api("GET","/api/admin/audio-upload-params"),o=new FormData;o.append("file",e,e.name),o.append("api_key",i.api_key),o.append("timestamp",String(i.timestamp)),o.append("signature",i.signature),o.append("folder",i.folder);const r=await fetch(i.upload_url,{method:"POST",body:o});if(!r.ok){const f=await r.text().catch(()=>"");throw new Error(`Upload failed: ${f}`)}const c=(await r.json()).secure_url;if(!c)throw new Error("Cloudinary upload returned no URL");n&&(n.innerHTML="\u23F3 Transcribing\u2026");const l=getStoredToken(),d={"Content-Type":"application/json"};l&&(d.Authorization="Bearer "+l);const u=(()=>{try{return localStorage.getItem("tf_admin_token")}catch{return null}})();u&&(d["X-Admin-Token"]=u);const p=await fetch(`/api/campaigns/${currentCampaign.id}/sessions/${t}/transcribe-replace`,{method:"POST",credentials:"include",headers:d,body:JSON.stringify({url:c})});if(!p.ok){const f=await p.text().catch(()=>"");let g="";try{g=JSON.parse(f).detail}catch{g=f}throw new Error(g||`HTTP ${p.status}`)}const m=await p.json();return allSessions=allSessions.map(f=>f.id===t?m.session:f),showToast("Transcript replaced successfully."),!0}catch(i){return showToast("Transcription failed: "+(i.message||i)),n&&(n.innerHTML=s,n.disabled=!1),!1}}const _recapUpToDateLocal={},_recapPollTimers={},_RECAP_LOCK_STALE_MS=7*60*1e3;function _recapGate(e){if(!!(e&&e.transcript_chars>0))return{hasTx:!0,hide:!1,disabled:!1,tip:""};const n="No transcript yet: record this session at Live at the Table, or upload a transcript in Session Tools.";return e&&e.is_session_zero?{hasTx:!1,hide:!0,disabled:!1,tip:n}:{hasTx:!1,hide:!1,disabled:!0,tip:n}}const _CHAPTER_BUILD_LABEL="Build Codex from Chapter";function _tfCreditCount(e,t){const n=Math.max(0,Math.floor(Number(t)||0)),a=Math.max(0,Math.floor(Number(e)||0))+Math.floor(n/2);return n%2===0?String(a):a?`${a} \xBD`:"\xBD"}function _tfHalfCredits(e,t){const n=e&&e.half_credits||{};return Math.max(0,Math.floor(Number(n[t])||0))}function _chapterBuildCostLine(){const e=typeof _tfBillingStatus<"u"&&_tfBillingStatus||null,t=typeof currentCampaign<"u"&&currentCampaign||null,n=e&&e.usage||{},a=t&&t.primary_gm_changed_at?n.chapter_build_cost_handed_off:n.chapter_build_cost;if(!a)return"";const s=typeof currentUser<"u"&&currentUser?currentUser.id:null,i=t?t.primary_gm_id:null;return s==null||i==null||String(s)!==String(i)?"":a==="covered"?"Covered by the half credit you have banked.":"Uses half a credit."}const _CHAPTER_BUILD_STEPS=["Finding NPCs, places & lore","Updating your Codex","Almost there"];function _chapterBuildState(e,t){if(!t||!e)return null;const n=e.chapter_build;return n==="ready"||n==="built"||n==="stale"?n:null}function _chapterBuildBlockHtml(e,t,n){return _chapterBuildState(e,t)!=="ready"?"":`<div class="chr-build-codex">
1227      <button type="button" class="btn-new btn-xs chr-build-codex-btn" data-chr-build="${e.id}"${n?" disabled":""}>${_CHAPTER_BUILD_LABEL}</button>
1228      <span class="form-helper-text chr-build-codex-cost">${escHtml(_chapterBuildCostLine())}</span>
1229      <span class="chr-generate-status" id="chr-gen-status-${e.id}"></span>
1230      <div class="chr-generate-result" id="chr-gen-result-${e.id}"></div>
1231    </div>`}function _isRecapInProgress(e){const t=e&&e.recap_in_progress_since;if(!t)return!1;const n=Date.now()-new Date(t).getTime();return n>=0&&n<_RECAP_LOCK_STALE_MS}async function _tfRenderCorrectionChips(e,t){const n=currentCampaign?.id;if(!n||!t||!(_isGm()||currentUser&&currentUser.is_admin))return;let a;try{a=await api("GET",`/api/campaigns/${n}/sessions/${e}/correction-candidates`)}catch{return}if(_campaignStale(n))return;const s=a&&a.candidates||[];if(!s.length)return;const i=document.createElement("div");
1231i.className="correction-chip-strip",i.innerHTML='<span class="form-section-label">Name check <button type="button" class="btn-ghost btn-xs correction-manage-link" data-corr-manage="1" title="See and prune every fix this campaign has learned">Manage fixes \u2192</button></span>'+s.map((r,c)=>{const l=r.candidates.map(u=>`<button type="button" class="btn-new btn-xs" data-corr-accept="${c}" data-corr-name="${escHtml(u)}">${escHtml(u)}</button>`).join(" "),d=r.snippet?`<div class="form-helper-text correction-chip-snippet">\u201C${escHtml(r.snippet)}\u201D</div>`:"";return`<div class="correction-chip" data-corr-garble="${escHtml(r.garble)}" data-corr-multi="${r.candidates.length>1?1:0}"><span class="form-helper-text">Heard \u201C${escHtml(r.garble)}\u201D ${r.count}\xD7 \u2014 did you mean:</span> ${l} <button type="button" class="btn-ghost btn-xs" data-corr-dismiss="${c}">Not a name (won\u2019t ask again)</button>${d}</div>`}).join(""),t.appendChild(i);const o=async(r,c,l)=>{const d=await api("POST",`/api/campaigns/${n}/corrections`,{garble:c,canonical:l,action:"accept",session_id:e});showToast(`\u201C${c}\u201D \u2192 ${l} saved. Every future recap inherits it${d.applied_rows?"; this session\u2019s text was repaired":""}.`,6e3),r.remove()};i.addEventListener("click",async r=>{if(r.target.closest("[data-corr-manage]")){document.getElementById("nav-manage-btn")?.click(),setTimeout(()=>document.getElementById("manage-corrections-section")?.scrollIntoView({behavior:"smooth",block:"start"}),350);return}const l=r.target.closest("[data-corr-accept]"),d=r.target.closest("[data-corr-dismiss]"),u=r.target.closest("[data-corr-confirm]"),p=r.target.closest("[data-corr-cancel]"),m=r.target.closest(".correction-chip");if(!m)return;const f=m.dataset.corrGarble;try{if(p){delete m.dataset.corrPendingName,m.querySelector(".correction-chip-confirm")?.remove(),m.querySelectorAll("button").forEach(h=>h.disabled=!1);return}if(u){const h=m.dataset.corrPendingName;h&&(u.disabled=!0,await o(m,f,h));return}if(!l&&!d)return;if(m.querySelectorAll("button").forEach(h=>h.disabled=!0),d){await api("POST",`/api/campaigns/${n}/corrections`,{garble:f,action:"dismiss"}),m.remove();return}const g=l.dataset.corrName;if(m.dataset.corrMulti==="1"){const h=await api("POST",`/api/campaigns/${n}/corrections`,{garble:f,action:"count"});m.dataset.corrPendingName=g,m.querySelector(".correction-chip-confirm")?.remove(),m.insertAdjacentHTML("beforeend",`<div class="correction-chip-confirm"><span class="form-helper-text form-helper-text--warn">Rewrite \u201C${escHtml(f)}\u201D \u2192 ${escHtml(g)} across ${h.sessions} session(s) and ${h.entities} codex entr${h.entities===1?"y":"ies"}?</span> <button type="button" class="btn-new btn-xs" data-corr-confirm="1">Confirm</button> <button type="button" class="btn-ghost btn-xs" data-corr-cancel="1">Cancel</button></div>`),m.querySelector("[data-corr-confirm]").disabled=!1,m.querySelector("[data-corr-cancel]").disabled=!1}else await o(m,f,g)}catch(g){showToast("Correction failed: "+g.message,5e3),m.querySelectorAll("button").forEach(h=>h.disabled=!1)}})}function _maybeStartRecapPoll(e){if(_recapPollTimers[e]||!currentCampaign)return;const t=currentCampaign;_recapPollTimers[e]=setInterval(async()=>{try{const n=await api("GET",`/api/campaigns/${t.id}/sessions/${e}`),a=_isRecapInProgress((allSessions||[]).find(i=>i.id===e)||{}),s=_isRecapInProgress(n);if(allSessions=allSessions.map(i=>i.id===e?n:i),!s&&(clearInterval(_recapPollTimers[e]),delete _recapPollTimers[e],a)){showToast("\u2726 Recap finished generating.",4e3);const i=(location.hash||"").match(/^#chronicle\/\d+\/(\d+)$/);i&&parseInt(i[1])===e&&!document.querySelector(".chr2-field-editor")&&typeof renderChronicleRecap=="function"&&renderChronicleRecap(e)}}catch{clearInterval(_recapPollTimers[e]),delete _recapPollTimers[e]}},6e3)}async function _fireGapFillForSession(e,t,{onComplete:n}={}){const a=currentCampaign?.id,s=t?t.textContent:"";t&&(t.disabled=!0);let i=0,o=null,r=!1;try{for(let c=0;c<5;c++){if(t&&(t.textContent=c===0?"\u23F3 Filling gaps\u2026":`\u23F3 Filling gaps\u2026 (${i} updated)`),o=await api("POST",`/api/campaigns/${a}/sessions/${e}/recap-gap-fill`),_campaignStale(a))return;
1231if((o.enriched_entities||[]).forEach(l=>{allEntities=allEntities.map(d=>d.id===l.id?l:d)}),i+=(o.enriched_entities||[]).length,r=r||!!o.quest_sync_missing,!o||!(o.remaining>0))break}r&&fireQuestSync(e),i>0?(renderCompendium(),showToast(`Filled the gaps: updated ${i} codex entr${i===1?"y":"ies"}${r?"; quest sync running":""}.`,6e3)):o&&o.nothing_missing&&!r?showToast("Nothing missing: this recap already ran end to end.",5e3):showToast(`Gap check complete: no codex entries needed changes${r?"; quest sync running":""}.`,5e3),n&&n(o)}catch(c){c.status===409?showToast(c.message||"This session's recap is generating. Progress will appear as it finishes.",6e3):c.status===402?_showUpgradeModal({scenario:"cap_exhausted"}):showToast("Gap fill failed: "+c.message,7e3)}finally{t&&(t.disabled=!1,t.textContent=s)}}const _TF_RS_FAST_MS=6e3,_TF_RS_IDLE_MS=45e3;let _tfRsTimer=null,_tfRsDelay=_TF_RS_IDLE_MS;const _TF_RS_DISMISS_KEY="tf_rs_dismissed",_TF_RS_DISMISS_MAX=40;function _tfRsLoadDismissed(){try{const e=JSON.parse(localStorage.getItem(_TF_RS_DISMISS_KEY)||"[]");return new Set(Array.isArray(e)?e.filter(t=>typeof t=="string"):[])}catch{return new Set}}function _tfRsSaveDismissed(){try{localStorage.setItem(_TF_RS_DISMISS_KEY,JSON.stringify([..._tfRsDismissed].slice(-_TF_RS_DISMISS_MAX)))}catch{}}let _tfRsDismissed=_tfRsLoadDismissed(),_tfRsShownSig=null,_tfRsNotAGm=!1,_tfRsLast=null;function _tfRsSignature(e){return`${e.session_id}:${e.state}:${e.recovery&&e.recovery.kind||""}`}function _tfRsWhen(e,t){if(!e)return"";const n=new Date(e).getTime(),a=new Date(t||Date.now()).getTime(),s=Math.max(0,Math.round((n-a)/6e4)),i=new Date(e).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"});return s<=0?`at ${i}, any moment now`:s===1?`at ${i}, in about a minute`:`at ${i}, in ${s} min`}function _tfRsAgo(e){if(!e)return"";const t=Math.max(0,Math.round((Date.now()-new Date(e).getTime())/6e4));return t<1?"just now":`${t} min ago`}function _tfRsMeta(e){const t=[];if(e.campaign_name&&t.push(escHtml(e.campaign_name)),e.is_session_zero?t.push("Session 0"):e.session_number&&t.push(`Session ${e.session_number}`),e.session_date){const n=String(e.session_date).split("-").map(Number);n.length===3&&t.push(new Date(n[0],n[1]-1,n[2],12).toLocaleDateString([],{month:"short",day:"numeric"}))}return t.join(" \xB7 ")}const _TF_RS_TITLE_MAX=38;function _tfRsChapter(e){const t=(e.title||"").trim();return t&&t.length<=_TF_RS_TITLE_MAX?`\u201C${escHtml(t)}\u201D`:"this chapter"}function _tfRsCopy(e,t){const n=_tfRsChapter(e),a=e.recovery||{},s=_tfRsWhen(a.at,t),i=e.has_chapter?[{act:"open",label:"Open chapter"}]:[];return e.state==="running"?{lead:`Writing ${n}`,sub:`Started ${_tfRsAgo(e.started_at)}. Safe to step away. It keeps going without you.`,actions:[...i,{act:"hide",label:"Hide"}]}:e.state==="stalled"&&a.kind==="tail"?{lead:"Long session; the chapter is saved.",sub:`${n} is written and safe. The codex and quest updates finish on their own ${s}.`,actions:[...i,{act:"gaps",label:"Finish it now"},{act:"hide",label:"Hide"}]}:e.state==="stalled"&&a.kind==="refire"?{lead:"Long session; the chapter is still coming.",sub:`It restarts on its own ${s}. Attempt ${a.attempt} of ${a.max}.`,actions:[{act:"retry",label:"Retry now"},{act:"hide",label:"Hide"}]}:e.state==="stalled"?{lead:"This recap needs you.",sub:"The automatic retries are used up. Generate it again when you are ready.",actions:[{act:"retry",label:"Generate now"},{act:"hide",label:"Hide"}]}:{lead:"Your Codex is still catching up.",sub:`The chapter itself is done and safe to read. It repairs itself each hour; the next pass is ${s}.`,actions:[{act:"gaps",label:"Fill gaps now"},...i,{act:"hide",label:"Hide"}]}}const _TF_RS_GHOST_ICON='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 7C10.4 5.6 8.5 4.9 6 4.9H3.4v12.6H6c2.5 0 4.4.7 6 2.1"/><path d="M12 7c1.6-1.4 3.5-2.1 6-2.1h2.6v12.6H18c-2.5 0-4.4.7-6 2.1"/><path d="M12 7v12.6"/></svg>';function _tfRsRenderGhost(e,t,n){const a=_tfRsCopy(t,n&&n.server_time),s=_tfRsMeta(t),i=`${s?s+" \xB7 ":""}${a.lead} \xB7 Click to reopen.`;e.className=`recap-status-card recap-status-card--ghost recap-status-card--${t.state}`,e.innerHTML=`<button type="button" class="rs-ghost" data-rs-act="reopen" title="${i}" aria-label="${i}">${_TF_RS_GHOST_ICON}</button>`,_tfRsShownSig=null}function _tfRsRender(e){const t=document.getElementById("recap-status-card");if(!t)return;_tfRsLast=e||null;const n=e&&e.sessions||[],a=n.find(l=>!_tfRsDismissed.has(_tfRsSignature(l)));if(!a){if(n.length){_tfRsRenderGhost(t,n[0],e);return}t.classList.add("hidden"),t.innerHTML="",_tfRsShownSig=null;return}const s=_tfRsSignature(a),i=_tfRsCopy(a,e.server_time);a.campaign_id&&(!currentCampaign||a.campaign_id!==currentCampaign.id)&&(i.actions=[{act:"open",label:"Go to it"},{act:"hide",label:"Hide"}
1231]);const o=a.done!=null&&a.total!=null?` \xB7 ${a.done} of ${a.total}`:a.enriched&&a.phase==="codex"?` \xB7 ${a.enriched} so far`:"",r=i.actions.map(l=>`<button type="button" class="rs-act" data-rs-act="${l.act}" data-rs-sid="${a.session_id}">${escHtml(l.label)}</button>`).join("");t.className=`recap-status-card recap-status-card--${a.state}`,t.style.setProperty("--rs-progress",`${a.percent}%`);const c=_tfRsMeta(a);t.innerHTML=`
1232    <div class="rs-top">
1233      <span class="rs-phase">${escHtml(a.phase_label)}${escHtml(o)}</span>
1234      <div class="rs-bar"><div class="rs-bar-fill"></div></div>
1235    </div>
1236    <div class="rs-main">
1237      <div class="rs-body">
1238        ${c?`<p class="rs-meta">${c}</p>`:""}
1239        <p class="rs-lead">${i.lead}</p>
1240        <p class="rs-sub">${i.sub}</p>
1241      </div>
1242      <div class="rs-actions">${r}</div>
1243    </div>`,_tfRsShownSig=s}async function _tfRsTick(){const e=document.getElementById("recap-status-card");if(!e||!currentUser||_tfRsNotAGm){e&&(e.classList.add("hidden"),e.innerHTML="");return}let t;try{t=await api("GET","/api/recap-status")}catch{_tfRsDelay=_TF_RS_IDLE_MS;return}if(t&&t.gm===!1){_tfRsNotAGm=!0,e.classList.add("hidden"),e.innerHTML="";return}_tfRsRender(t),_tfRsDelay=(t.sessions||[]).some(a=>a.state==="running")?_TF_RS_FAST_MS:_TF_RS_IDLE_MS}function _tfRsStart(){if(_tfRsTimer)return;const e=()=>{_tfRsTimer=setTimeout(async()=>{_tfRsTimer=null,await _tfRsTick(),e()},_tfRsDelay)};_tfRsTimer=setTimeout(()=>{_tfRsTimer=null,_tfRsTick().then(e)},_TF_POLL_FIRST_TICK_MS)}function _tfRsResetForCampaign(){_tfRsShownSig=null;const e=document.getElementById("recap-status-card");e&&(e.classList.add("hidden"),e.innerHTML=""),_tfRsDelay=_TF_RS_FAST_MS}document.addEventListener("click",e=>{const t=e.target.closest("[data-rs-act]");if(!t)return;const n=t.dataset.rsAct,a=parseInt(t.dataset.rsSid,10);if(n==="hide"){if(_tfRsShownSig&&(_tfRsDismissed.add(_tfRsShownSig),_tfRsSaveDismissed()),_tfRsShownSig=null,_tfRsLast){_tfRsRender(_tfRsLast);return}const o=document.getElementById("recap-status-card");o&&(o.classList.add("hidden"),o.innerHTML="");return}if(n==="reopen"){_tfRsLast&&((_tfRsLast.sessions||[]).forEach(o=>_tfRsDismissed.delete(_tfRsSignature(o))),_tfRsSaveDismissed(),_tfRsRender(_tfRsLast));return}const s=(_tfRsLast&&_tfRsLast.sessions||[]).find(o=>o.session_id===a),i=!!(s&&s.campaign_id&&(!currentCampaign||s.campaign_id!==currentCampaign.id));if(n==="open"){i&&typeof bootWithCampaignId=="function"?Promise.resolve(bootWithCampaignId(s.campaign_id)).then(()=>{typeof renderChronicleRecap=="function"&&renderChronicleRecap(a)}):typeof renderChronicleRecap=="function"&&renderChronicleRecap(a);return}if(!i){if(n==="gaps"&&typeof _fireGapFillForSession=="function"){_fireGapFillForSession(a,t,{onComplete:()=>_tfRsTick()});return}n==="retry"&&typeof _fireRecapForSession=="function"&&_fireRecapForSession(a,{btnEl:t,onComplete:()=>_tfRsTick()})}});const _RECAP_PROGRESS_STEPS=["Generating recap","Reading the transcript","Finding NPCs, places & lore","Writing the chapter","Updating your Codex","Almost there"];async function _flushSessionEdits(e){const t=Array.from(document.querySelectorAll(".session-section-box")).filter(n=>parseInt(n.dataset.id,10)===e);for(const n of t){const a=(allSessions||[]).find(i=>i.id===e),s=n.dataset.key||"raw_transcript";if(!(!a||n.value===(a[s]||"")))try{const i=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${e}`,{[s]:n.value||null});allSessions=(allSessions||[]).map(o=>o.id===e?i:o)}catch(i){showToast("Save failed: "+(i&&i.message||i),4e3)}}}const _RPF_NOT_PLAYER="__not_player__",_RPF_LINE_RE=/^(?:\[\d+:\d+\] )?([^:]{1,40}):\s?(.*)$/,_RPF_NEVER_PLAYER=new Set(["gm","dm","game master","gamemaster","dungeon master","dungeonmaster","narrator","dice","dicebot","dice bot","bot","system","avrae","roll20"]);function _rpfNormLabel(e){return String(e||"").trim().replace(/\s*:+$/,"").replace(/^[<\[(]+\s*/,"").replace(/\s*[>\])]+$/,"").replace(/\s*:+$/,"").replace(/\s+/g," ").trim().toLowerCase()}function _rpfScan(e,t){const n=new Set((t||[]).map(o=>(o.name||"").trim().toLowerCase())),a=e&&e.speaker_review||{},s=new Set([...a.reviewed||[],...a.not_player||[]].map(o=>String(o).trim().toLowerCase())),i=new Map;for(const o of String(e&&e.raw_transcript||"").split(/\r?\n/)){const r=_RPF_LINE_RE.exec(o);if(!r)continue;const c=r[1].trim(),l=_rpfNormLabel(c);if(!l||_RPF_NEVER_PLAYER.has(l)||n.has(l)||n.has(c.toLowerCase()))continue;let d=i.get(l);d||(d={label:c,key:l,count:0,quotes:[]},i.set(l,d)),d.count++;const u=(r[2]||"").trim();u.length>10&&d.quotes.length<15&&d.quotes.push(u)}return[...i.values()].filter(o=>!s.has(o.key)).sort((o,r)=>r.count-o.count).slice(0,30)}const _RPF_PAGE=3;function _rpfQuotes(e,t){if(!e.quotes.length)return`<span class="rpf-speaker-quote"><em>${escHtml(e.label)}, no clear sample line</em></span>`;const n=Math.ceil(e.quotes.length/_RPF_PAGE),a=(t%n+n)%n*_RPF_PAGE;return e.quotes.slice(a,a+_RPF_PAGE).map(s=>`<span class="rpf-speaker-quote">\u201C${escHtml(s.length>_SPK_DISPLAY_CAP?s.slice(0,_SPK_DISPLAY_CAP)+"\u2026":s)}\u201D</span>`).join("")}const _RPF_CHEVRON='<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" focusable="false">
1243<path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';async function _recapPreflight(e,t=null){if(!_isGm())return!0;let n=t||(allSessions||[]).find(s=>s.id===e)||null;const a=currentCampaign?.id;if(a&&(!n||!String(n.raw_transcript||"").trim()))try{const s=await api("GET",`/api/campaigns/${a}/sessions/${e}`);if(_campaignStale(a))return!1;s&&s.id&&(n={...n||{},...s})}catch{return!0}return n?_recapPreflightModal(e,n):!0}function _recapPreflightModal(e,t){return new Promise(n=>{const a=document.getElementById("recap-preflight-modal"),s=document.getElementById("recap-preflight-body"),i=document.getElementById("recap-preflight-foot");if(!a||!s||!i||!t){n(!0);return}const o=currentCampaign?.id,r=()=>(allEntities||[]).filter(v=>v.type==="pc");let c=_rpfScan(t,r());const l=r().length===0;if(!l&&!c.length){n(!0);return}const d=!document.getElementById("live-session-screen")?.classList.contains("hidden");let u=!1;const p=v=>{u||(u=!0,a.classList.add("hidden"),document.removeEventListener("keydown",m),s.innerHTML="",i.innerHTML="",n(v))},m=v=>{v.key==="Escape"&&(v.preventDefault(),p(!1))};document.addEventListener("keydown",m),a.querySelector("[data-rpf-cancel]").onclick=()=>p(!1),a.onclick=v=>{v.target===a&&p(!1)};const f=()=>{const v=c.map(b=>b.label);s.innerHTML=`
1244        <p class="rpf-intro">Threadfall can't tell your players from their characters yet, so Discord names can end up written into the chapter and the Codex. Add your party now, just names, it takes under a minute.</p>
1245        <span class="form-section-label">One character name per line</span>
1246        <textarea class="rpf-names" id="rpf-names" rows="6" spellcheck="false">${escHtml(v.join(`
1247`))}</textarea>
1248        <p class="form-helper-text">Edit or delete any that aren't real characters. Want full sheets? Skip this and use the Party tab.</p>`,i.innerHTML=`
1249        ${d?"":'<button type="button" class="btn-ghost" data-rpf-party>Party tab</button>'}
1250        <button type="button" class="btn-ghost" data-rpf-skip>Run anyway</button>
1251        <button type="button" class="btn-new" data-rpf-add>Add PCs and continue</button>`,document.getElementById("recap-preflight-title").textContent="No player characters in this campaign",i.querySelector("[data-rpf-party]")?.addEventListener("click",()=>{p(!1),document.querySelector('.tab-btn[data-tab="party"]')?.click()}),i.querySelector("[data-rpf-skip]").addEventListener("click",()=>p(!0)),i.querySelector("[data-rpf-add]").addEventListener("click",async b=>{const y=b.currentTarget,_=[...new Set(document.getElementById("rpf-names").value.split(/\r?\n/).map($=>$.trim()).filter($=>$&&$.length<=60))];if(!_.length){showToast("Add at least one character name, or run anyway.",4e3);return}y.disabled=!0,y.textContent="Adding\u2026";const w=[];for(const $ of _)if(!r().some(E=>(E.name||"").toLowerCase()===$.toLowerCase()))try{const E=await api("POST",`/api/campaigns/${o}/entities`,{name:$,type:"pc",description:"",tags:[]});if(_campaignStale(o)){p(!1);return}E&&E.id&&(allEntities.push(E),w.push(E))}catch(E){showToast(`Couldn't add ${$}: ${E&&E.message||E}`,5e3)}for(const $ of c){const E=r().find(k=>(k.name||"").toLowerCase()===$.key);E&&E.name!==$.label&&await g($.label,E.name)}c=_rpfScan(t,r()),w.length&&showToast(`Added ${w.length} character${w.length===1?"":"s"} to the Codex.`,4e3),c.length?h():p(!0)}),setTimeout(()=>document.getElementById("rpf-names")?.focus(),0)},g=async(v,b)=>{try{const y=await api("POST",`/api/campaigns/${o}/sessions/${e}/relabel-speaker`,{from_label:v,to_label:b});return y&&typeof y.raw_transcript=="string"&&(t.raw_transcript=y.raw_transcript,allSessions=(allSessions||[]).map(_=>_.id===e?{..._,raw_transcript:y.raw_transcript}:_)),!0}catch{return!1}},h=()=>{const v={};document.getElementById("recap-preflight-title").textContent="Some speakers aren't attributed";const b=c.length;s.innerHTML=`
1252        <p class="rpf-intro">Threadfall heard ${b} name${b===1?"":"s"} it can't match to a character. Pick who each one is, or mark them not a player.</p>
1253        <div class="rpf-eyebrow-row"><span class="form-section-label">
1253Speakers</span><span class="rpf-badge" id="rpf-badge"></span></div>
1254        <div class="rpf-speaker-rows">${c.map((E,k)=>`
1255          <div class="rpf-speaker-row" data-idx="${k}">
1256            <div class="rpf-speaker-main">
1257              <span class="rpf-speaker-label">${escHtml(E.label)}<span class="rpf-speaker-count">${E.count} line${E.count===1?"":"s"}</span></span>
1258              <div class="rpf-speaker-quotes"><div class="rpf-speaker-quote-list" data-rpf-quote>${_rpfQuotes(E,0)}</div>${E.quotes.length>_RPF_PAGE?`<button type="button" class="rpf-quote-next" data-rpf-next title="Show more of what ${escHtml(E.label)} said" aria-label="Show more lines">${_RPF_CHEVRON}</button>`:""}</div>
1259              <span class="rpf-row-error hidden" data-rpf-err></span>
1260            </div>
1261            <select class="rpf-speaker-select" data-rpf-select aria-label="Who is ${escHtml(E.label)}?">
1262              <option value="">(Who is this?)</option>
1263              ${_liveSpkPcOptions("")}
1264              <option value="${_RPF_NOT_PLAYER}">Not a player</option>
1265            </select>
1266          </div>`).join("")}</div>`,i.innerHTML=`
1267        <button type="button" class="btn-ghost" data-rpf-cancel2>Cancel</button>
1268        <button type="button" class="btn-new" data-rpf-run>Run recap anyway</button>`;const y=document.getElementById("rpf-badge"),_=i.querySelector("[data-rpf-run]"),w=()=>[...s.querySelectorAll("[data-rpf-select]")].map(E=>E.value),$=()=>{const k=w().filter(Boolean).length;y.textContent=`${k} of ${b} attributed`,_.textContent=k===0?"Run recap anyway":k<b?"Run recap with these":"Run recap"};$(),s.querySelectorAll("[data-rpf-select]").forEach(E=>E.addEventListener("change",$)),s.querySelectorAll("[data-rpf-next]").forEach(E=>E.addEventListener("click",()=>{const k=E.closest(".rpf-speaker-row"),C=+k.dataset.idx;v[C]=(v[C]||0)+1,k.querySelector("[data-rpf-quote]").innerHTML=_rpfQuotes(c[C],v[C])})),i.querySelector("[data-rpf-cancel2]").addEventListener("click",()=>p(!1)),_.addEventListener("click",async()=>{_.disabled=!0,_.textContent="Saving\u2026",i.querySelector("[data-rpf-cancel2]").disabled=!0;const E=w(),k=[],C=c.map(S=>S.label);for(let S=0;S<c.length;S++){const q=E[S];if(!q||q===_RPF_NOT_PLAYER){k.push(c[S].label);continue}const I=await g(c[S].label,q);if(_campaignStale(o)){p(!1);return}if(!I){const T=s.querySelector(`.rpf-speaker-row[data-idx="${S}"] [data-rpf-err]`);T&&(T.textContent="Couldn't save this one; the recap will run without it.",T.classList.remove("hidden"))}}try{const S=await api("POST",`/api/campaigns/${o}/sessions/${e}/speaker-review`,{not_player:k,reviewed:C});S&&S.speaker_review&&(t.speaker_review=S.speaker_review,allSessions=(allSessions||[]).map(q=>q.id===e?{...q,speaker_review:S.speaker_review}:q))}catch{}p(!0)}),setTimeout(()=>s.querySelector("[data-rpf-select]")?.focus(),0)};a.classList.remove("hidden"),l?f():h()})}async function _fireRecapForSession(e,t={}){const{btnEl:n=null,statusEl:a=null,resultEl:s=null,onComplete:i=null,force_story:o=!1,skipPreflight:r=!1,from_chapter:c=!1}=t,l=e,d=currentCampaign?.id;if(!r&&!c&&typeof _recapPreflight=="function"){const b=await _recapPreflight(l);if(_campaignStale(d))return;if(!b){n&&(n.disabled=!1);return}}const u=(allSessions||[]).find(b=>b.id===l);if(!c&&u&&(u.summary||"").trim()&&typeof confirmDialog=="function"&&!await confirmDialog({title:"Regenerate this chapter?",message:"This rewrites the recap your players see and uses one recap credit. The current chapter will be replaced.",confirmLabel:"Regenerate",cancelLabel:"Keep current"})){n&&(n.disabled=!1);return}if((!!(u&&(u.summary||"").trim())?"":(u?.test_transcript||"").split(`
1269`).filter(b=>b.trim()!==_TEST_LABEL).join(`
1270`).trim())&&await confirmDialog({title:"Include your test recording?",message:"This session has a test recording your recap won't include. Add it to this recap? Including it makes the take part of the real session; it stops being a free test.",confirmLabel:"Include it",cancelLabel:"Leave it out",danger:!1}))try{const y=await api("POST",`/api/campaigns/${d}/sessions/${l}/test-recording/promote`,{});if(_campaignStale(d)){n&&(n.disabled=!1);return}y&&y.session&&(allSessions=(allSessions||[]).map(_=>_.id===l?{..._,...y.session}:_))}catch(y){showToast("Could not add the test recording: "+y.message,7e3),n&&(n.disabled=!1);return}await _flushSessionEdits(l),n&&(n.disabled=!0),s&&(s.innerHTML="");let f=0;const g=c?_CHAPTER_BUILD_STEPS:_RECAP_PROGRESS_STEPS,h=()=>{a&&(a.textContent=g[f]+"\u2026 \xB7 ~1\u20133 min")};h();const v=a?setInterval(()=>{f=Math.min(f+1,g.length-1),h()},14e3):null;try{let b=await api("POST",`/api/campaigns/${d}/sessions/${l}/generate-recap`,c?{from_chapter:!0}:o?{force_story:!0}:void 0);if(v&&clearInterval(v),_campaignStale(d))return;if(b.status==="no_story"){const E=b.entities_built||0,k=E?`${E} codex ${E===1?"entry was":"entries were"} added or updated from it.`:"Nothing new for the codex from this one.";showToast(`This was prep, not play, so no chapter was written. ${k} No session credit was used.`,8e3),a&&(a.textContent=""),s&&(s.innerHTML="");try{await loadEntities()}catch{}i&&i(b);return}if(b.unchanged){let E=!1;if(!c&&_isRealAdmin()&&typeof confirmDialog=="function"&&await confirmDialog({title:"Nothing has changed",message:"The transcript and notes are identical to the last recap. Force a full regenerate anyway? This rewrites the chapter your players see.",confirmLabel:"Force regenerate",cancelLabel:"Keep current"}
1270)){f=0,h();const C=a?setInterval(()=>{f=Math.min(f+1,_RECAP_PROGRESS_STEPS.length-1),h()},14e3):null;try{b=await api("POST",`/api/campaigns/${d}/sessions/${l}/generate-recap`,{force:!0})}finally{C&&clearInterval(C)}if(_campaignStale(d))return;E=!0}if(!E){let k="";if(b.last_recap_at){const C=Math.max(0,Math.round((Date.now()-new Date(b.last_recap_at).getTime())/6e4));C<1?k=" (just now)":C<60?k=` (${C} min ago)`:C<1440?k=` (${Math.round(C/60)} hr ago)`:k=` (${Math.round(C/1440)} day${C>=2880?"s":""} ago)`}showToast(`Nothing's changed since the last recap${k}.`,4e3),c||(_recapUpToDateLocal[l]=!0),a&&(a.textContent=""),s&&(s.innerHTML=""),i&&i(b);return}}if(b.recap_continued){const E="That was a long one. Your progress is saved and the recap is still finishing. It will appear here when it lands.";showToast(E,8e3),a&&(a.textContent=""),s&&(s.innerHTML=`<p class="placeholder--padded">${E}</p>`),_maybeStartRecapPoll(l),n&&(n.disabled=!1);return}allSessions=allSessions.map(E=>E.id===l?b.session:E);const y={};if((b.enriched_entities||[]).forEach(E=>{y[E.id]=E}),(b.killed_npcs||[]).forEach(E=>{y[E.id]=E}),Object.keys(y).length&&(allEntities=allEntities.map(E=>y[E.id]||E)),b.new_entities.length>0&&(allEntities=await _apiAllPages(`/api/campaigns/${currentCampaign.id}/entities`)),b.tail_dispatched&&_maybeStartRecapPoll(l),typeof _tfLoadBillingTier=="function")try{_tfLoadBillingTier()}catch{}let _=c?"":"Recap generated.";if(b.mentioned.length&&(_+=` Mentioned: ${b.mentioned.join(", ")}.`),b.new_entities.length&&(_+=` Added ${b.new_entities.length} new entry(ies) to Codex.`),b.enriched_entities&&b.enriched_entities.length&&(_+=` Updated ${b.enriched_entities.length} codex entry(ies).`),b.suggested_title&&showToast(`Title saved: "${escHtml(b.suggested_title)}"`,4e3),b.killed_npcs&&b.killed_npcs.length){const E=b.killed_npcs.map(k=>k.name).join(", ");_+=` \u2620\uFE0F Marked dead: ${E}.`,showToast(`\u2620\uFE0F Auto-flagged as dead: ${E}`,4e3)}if(b.resolved_quests&&b.resolved_quests.length){const E={};b.resolved_quests.forEach(S=>{E[S.id]=S}),allQuests=allQuests.map(S=>E[S.id]||S);const k=b.resolved_quests.filter(S=>S.status==="complete").map(S=>S.title),C=b.resolved_quests.filter(S=>S.status==="failed").map(S=>S.title);k.length&&(_+=` \u2705 Quest completed: ${k.join(", ")}.`,showToast(`\u2705 Quest completed: ${k.join(", ")}`,4e3)),C.length&&(_+=` \u274C Quest failed: ${C.join(", ")}.`,showToast(`\u274C Quest failed: ${C.join(", ")}`,4e3)),_questsSurfaceVisible()&&loadQuests()}const w=b.new_entities.length,$=(b.enriched_entities||[]).length;if(w>0||$>0){const E=[];if(w>0){const k=b.new_entities.slice(0,3).map(S=>S.name).join(", "),C=w>3?` +${w-3} more`:"";E.push(`${w} added: ${k}${C}`)}if($>0){const k=b.enriched_entities.slice(0,2).map(S=>S.name).join(", "),C=$>2?` +${$-2} more`:"";E.push(`${$} updated: ${k}${C}`)}showToast(`\u{1F4CB} Codex: ${E.join(" \xB7 ")}`,8e3)}if(s){const E=(S,q)=>`<button type="button" class="recap-cta-btn" data-recap-cta="${S}">${q}</button>`;let k="";(w>0||$>0)&&(k+=E("compendium","\u2192 See your Codex")),b.resolved_quests&&b.resolved_quests.length&&(k+=E("quests","Quests")),_isGm()&&(k+=E("threads","Open GM Prep")),s.innerHTML=`<div class="session-recap-result">${_.trim()}${k?`<div class="recap-cta-row">${k}</div>`:""}</div>`,_tfRenderCorrectionChips(l,s.querySelector(".session-recap-result")),s.querySelectorAll("[data-recap-cta]").forEach(S=>S.addEventListener("click",()=>{if(S.dataset.recapCta==="quests"){_partyOpenQuests(currentCampaign&&currentCampaign.id);return}document.getElementById("tab-btn-"+S.dataset.recapCta)?.click()}));const C=typeof _tfDiscordPitchHtml=="function"?_tfDiscordPitchHtml(b.session):"";C&&(s.insertAdjacentHTML("beforeend",C),_tfWireDiscordPitch(s))}if(a&&(a.textContent=""),c||(_recapUpToDateLocal[l]=!0),(b.enriched_entities&&b.enriched_entities.length||b.new_entities.length||b.killed_npcs&&b.killed_npcs.length)&&renderCompendium(),fireQuestSync(l),currentCampaign){const E=Array.isArray(_csAllCampaigns)?_csAllCampaigns:[],k=E.findIndex(q=>q.id===currentCampaign.id),C=k>
1270=0&&E[k]?.pending_recap?.session_id===l,S=currentCampaign.pending_recap?.session_id===l;if((C||S)&&(k>=0&&(E[k].pending_recap=null),currentCampaign.pending_recap=null,typeof _renderCampaignCards=="function"))try{_renderCampaignCards(E)}catch{}}typeof _tfRefreshNotifications=="function"&&_tfRefreshNotifications(),c||(_recapUpToDateLocal[l]=!0),i&&i(b)}catch(b){if(v&&clearInterval(v),a&&(a.textContent=""),_isEmailUnverifiedError(b)){s&&(s.innerHTML=""),_showVerifyWall(),n&&(n.disabled=!1);return}if(b.status===402)s&&(s.innerHTML=""),_showUpgradeModal({scenario:"cap_exhausted"});else if(b.status===409){showToast(b.message||"This session's recap is generating. Progress will appear as it finishes.",5e3),s&&(s.innerHTML="");try{const y=await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${l}`);allSessions=allSessions.map(_=>_.id===l?y:_),_maybeStartRecapPoll(l)}catch{}}else if(b.status===502&&b.message&&!/^HTTP \d+$/.test(b.message)&&!/time[d]?\s*out|gateway/i.test(b.message))s&&(s.innerHTML=`<p class="error">${b.message}</p>`),showToast(b.message,7e3);else if(b.status===504||b.status===502||/time[d]?\s*out|gateway/i.test(b.message||"")){const y="That was a long one. Your progress is saved. The recap will finish on its own within the hour, or generate again in a few minutes to continue it now.";showToast(y,9e3),s&&(s.innerHTML=`<p class="placeholder--padded">${y}</p>`),_maybeStartRecapPoll(l)}else if(!b.status&&(b instanceof TypeError||b instanceof SyntaxError)){const y="We didn't hear back on that one. Checking where the recap got to.";showToast(y,7e3),s&&(s.innerHTML=`<p class="placeholder--padded">${y}</p>`),_maybeStartRecapPoll(l)}else s&&(s.innerHTML=`<p class="error">${b.message}</p>`);n&&(n.disabled=!1)}}const _CAMPAIGN_AI_LOCK_COLUMNS=["threads_detect_in_progress_since","party_assessment_in_progress_since","summary_regen_in_progress_since"];let _campaignAiPollTimer=null;function _isCampaignAiInProgress(e,t){const n=e&&e[t];if(!n)return!1;const a=Date.now()-new Date(n).getTime();return a>=0&&a<_RECAP_LOCK_STALE_MS}function _anyCampaignAiInProgress(e){return _CAMPAIGN_AI_LOCK_COLUMNS.some(t=>_isCampaignAiInProgress(e,t))}function _maybeStartCampaignAiPoll(){if(_campaignAiPollTimer||!currentCampaign||!_anyCampaignAiInProgress(currentCampaign))return;const e=currentCampaign.id;_campaignAiPollTimer=setInterval(async()=>{try{if(!currentCampaign||currentCampaign.id!==e){clearInterval(_campaignAiPollTimer),_campaignAiPollTimer=null;return}const t=await api("GET",`/api/campaigns/${e}`),n=_anyCampaignAiInProgress(currentCampaign);_CAMPAIGN_AI_LOCK_COLUMNS.forEach(o=>{currentCampaign[o]=t[o]}),t.party_assessment_at&&(currentCampaign.party_assessment_at=t.party_assessment_at);const a=_anyCampaignAiInProgress(currentCampaign),s=document.getElementById("threads-detect-btn"),i=_isCampaignAiInProgress(currentCampaign,"threads_detect_in_progress_since");s&&i?(s.disabled=!0,s.textContent="\u21BB Detecting\u2026"):s&&!i&&s.textContent.includes("Detecting")&&(s.disabled=!1,s.textContent="\u21BA Detect Now"),a||(clearInterval(_campaignAiPollTimer),_campaignAiPollTimer=null,n&&showToast("\u2726 Generation finished.",4e3))}catch{clearInterval(_campaignAiPollTimer),_campaignAiPollTimer=null}},6e3)}function openSessionModal(e){if(e=e||{},document.getElementById("session-modal-title").textContent="New Session",e.isSessionZero)document.getElementById("sf-title").value="Session Zero";else{const n=allSessions.filter(a=>!a.is_session_zero).length+1;document.getElementById("sf-title").value=`Session ${n}`}const t=new Date;document.getElementById("sf-date-m").value=String(t.getMonth()+1),document.getElementById("sf-date-d").value=String(t.getDate()),document.getElementById("sf-date-y").value=String(t.getFullYear()).slice(2),document.getElementById("sf-is-session-zero").checked=!!e.isSessionZero,document.getElementById("sf-transcript").value="",document.getElementById("sf-transcript").closest(".form-row").hidden=!1,document.getElementById("session-form-error").classList.add("hidden"),document.getElementById("session-modal").classList.remove("hidden"),document.querySelector("#session-form .date-part")?.focus()}function closeSessionModal(){document.getElementById("session-modal").classList.add("hidden")}(function(){const e=document.getElementById("sf-transcript");
1270if(!e)return;const t=n=>{n.preventDefault(),n.stopPropagation()};["dragenter","dragover"].forEach(n=>e.addEventListener(n,a=>{t(a),e.classList.add("sf-drop-hot")})),["dragleave","dragend","drop"].forEach(n=>e.addEventListener(n,a=>{t(a),e.classList.remove("sf-drop-hot")})),e.addEventListener("drop",n=>{const a=n.dataTransfer&&n.dataTransfer.files&&n.dataTransfer.files[0];if(!a)return;if(!/\.(txt|md|vtt|srt|log|csv)$/i.test(a.name)&&!/^text\//.test(a.type||"")){showToast("Drop a text transcript (.txt, .md, .vtt, .srt).",4e3);return}if(a.size>5*1024*1024){showToast("That file is over 5 MB, paste the text instead.",5e3);return}const s=new FileReader;s.onload=()=>{const i=String(s.result||"");e.value=e.value.trim()?e.value.replace(/\s+$/,"")+`
1271
1272`+i:i,e.dispatchEvent(new Event("input",{bubbles:!0}));const o=i.trim().split(/\s+/).filter(Boolean).length;showToast(`Loaded ${a.name}: ${o.toLocaleString()} words.`,4e3)},s.onerror=()=>showToast("Could not read that file, paste the text instead.",5e3),s.readAsText(a)})})();function _tfDateOrder(e){try{const n=new Intl.DateTimeFormat(e,{year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(new Date(2e3,10,22)).map(a=>({month:"m",day:"d",year:"y"})[a.type]).filter(Boolean);if(n.length===3&&new Set(n).size===3)return n}catch{}return["m","d","y"]}function _tfDateOrderLabel(){const e={m:"MM",d:"DD",y:"YY"};return _tfDateOrder().map(t=>e[t]).join("/")}function _tfBuildDatePartsHTML(e){e=e||{};const t={m:["MM","Month"],d:["DD","Day"],y:["YY","Year"]};return'<span class="date-parts">'+_tfDateOrder().map(n=>`<input class="date-part" data-dp="${n}" inputmode="numeric" maxlength="2" placeholder="${t[n][0]}" aria-label="${t[n][1]}" value="${escHtml(e[n]||"")}">`).join('<span class="date-sep">/</span>')+"</span>"}function _tfWireDateParts(e,t){const n=()=>Array.from(e.querySelectorAll(".date-part"));n().forEach(a=>{a.addEventListener("keydown",s=>{if(!/^\d$/.test(s.key)&&!["Backspace","Delete","Tab","ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(s.key)&&s.preventDefault(),s.key==="Backspace"&&!a.value){const i=n(),o=i.indexOf(a);o>0&&i[o-1].focus()}}),a.addEventListener("focus",()=>a.select()),a.addEventListener("input",()=>{let s=a.value.replace(/\D/g,"").slice(0,2);const i=a.dataset.dp==="m"?1:a.dataset.dp==="d"?3:9;if(s.length===1&&parseInt(s)>i&&(s="0"+s),a.value=s,s.length===2){const o=n(),r=o.indexOf(a);r<o.length-1?o[r+1].focus():t&&t()}})})}function _tfReadDateParts(e){const t=l=>{const d=e.querySelector(`.date-part[data-dp="${l}"]`);return d?d.value.trim():""},n=t("m"),a=t("d"),s=t("y");if(!n&&!a&&!s)return{blank:!0};if(!n||!a||!s)return{error:`Fill the whole date (${_tfDateOrderLabel()}), or leave all three blank.`};const i=parseInt(n,10),o=parseInt(a,10);if(!(i>=1&&i<=12))return{error:`"${n}" isn't a month (1-12).`};if(!(o>=1&&o<=31))return{error:`"${a}" isn't a day (1-31).`};const r=`20${s.padStart(2,"0")}-${String(i).padStart(2,"0")}-${String(o).padStart(2,"0")}`,c=new Date(r+"T12:00:00");return c.getMonth()+1!==i||c.getDate()!==o?{error:"That date doesn't exist. Check the day against the month."}:{iso:r}}(function(){const e=document.querySelector("#session-form .date-parts");if(!e)return;const t=_tfDateOrder();if(t.join("")!=="mdy"){const n=Array.from(e.querySelectorAll(".date-sep")),a=s=>e.querySelector(`.date-part[data-dp="${s}"]`);e.append(a(t[0]),n[0],a(t[1]),n[1],a(t[2]))}_tfWireDateParts(e,()=>document.getElementById("sf-transcript").focus())})();function _closeFirstSessionChoice(){document.getElementById("first-session-choice-modal").classList.add("hidden")}document.getElementById("first-session-choice-zero").addEventListener("click",()=>{_closeFirstSessionChoice(),openSessionModal({isSessionZero:!0})}),document.getElementById("first-session-choice-one").addEventListener("click",()=>{_closeFirstSessionChoice(),openSessionModal()}),document.getElementById("first-session-choice-close").addEventListener("click",_closeFirstSessionChoice),function(){let e=!1;const t=document.getElementById("first-session-choice-modal");t.addEventListener("mousedown",n=>{e=n.target===t}),t.addEventListener("click",n=>{n.target===t&&e&&_closeFirstSessionChoice()})}(),document.getElementById("session-modal-close").addEventListener("click",closeSessionModal),document.getElementById("session-form-cancel").addEventListener("click",closeSessionModal);let _sessionModalMousedownOnBackdrop=!1;document.getElementById("session-modal").addEventListener("mousedown",e=>{_sessionModalMousedownOnBackdrop=e.target===document.getElementById("session-modal")}),document.getElementById("session-modal").addEventListener("click",e=>{e.target===document.getElementById("session-modal")&&_sessionModalMousedownOnBackdrop&&closeSessionModal()}),document.getElementById("session-form").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("session-form-error");t.classList.add("hidden");const n=e.target.querySelector("[type=submit]");n.disabled=!0;const a=_tfReadDateParts(document.querySelector("#session-form .date-parts"));if(a.error){t.textContent=a.error,t.classList.remove("hidden"),n.disabled=!1;return}const s={title:document.getElementById("sf-title").value.trim(),date:a.iso||null,is_session_zero:document.getElementById("sf-is-session-zero").checked,raw_transcript:document.getElementById("sf-transcript").value.trim()||null};try{const i=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions`,s);allSessions.unshift(i),_tfResortSessions(),expandedSessionId=i.id,closeSessionModal();try{const o=document.getElementById("tab-chronicle2");o&&!o.classList.contains("hidden")&&!o.querySelector("input, textarea")&&renderChronicleTimeline()}catch{}}catch(i){t.textContent=i.message,t.classList.remove("hidden")}finally{n.disabled=!1}});let allQuests=[],editingQuestId=null;function fireQuestSync(e){api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${e}/sync-quests`).then(t=>{if(t&&t.in_progress)return;const n=t.created||[],a=t.updated||[];if(n.length||a.length){_questsSurfaceVisible()&&loadQuests();const s=[];n.length&&s.push(`${n.length} added`),a.length&&s.push(`${a.length} updated`),showToast(`\u{1F4DC} Quest log: ${s.join(", ")}.`)}else showToast("\u{1F4DC} Quest log: nothing new.",2500)}).catch(t=>{const n=t&&t.message?t.message:"connection lost";
1272showToast("Quest log did not sync ("+n+"). Generate the recap again to retry.",7e3)})}function renderQuests(){const e=_q2Host();e&&_questsSurfaceVisible()&&!_q2EditorOpen(e.el)&&renderQuests2({cid:currentCampaign&&currentCampaign.id})}async function loadQuests(){const e=currentCampaign?.id;try{const t=await api("GET",`/api/campaigns/${e}/quests`);if(_campaignStale(e))return;allQuests=t,renderQuests()}catch(t){if(_campaignStale(e))return;console.error("Quest refresh failed:",t.message)}}function openQuestModal(e){editingQuestId=e;const t=e?allQuests.find(s=>s.id===e):null;document.getElementById("quest-modal-title").textContent=t?"Edit Quest":"New Quest",document.getElementById("qf-title").value=t?.title||"",document.getElementById("qf-status").value=t?.status||"active",document.getElementById("qf-description").value=t?.description||"",document.getElementById("quest-form-error").classList.add("hidden");const n=document.getElementById("qf-pc");n.innerHTML=`<option value="">${escHtml(_partyLabel())} (all)</option>`,allEntities.filter(s=>s.type==="pc").forEach(s=>{const i=document.createElement("option");i.value=s.id,i.textContent=s.name,s.id===t?.pc_entity_id&&(i.selected=!0),n.appendChild(i)});const a=document.getElementById("qf-session");a.innerHTML='<option value="">(none)</option>',allSessions.forEach(s=>{const i=document.createElement("option");i.value=s.id,i.textContent=`Session ${getSessionNum(s)}: ${s.title}`,s.id===t?.introduced_session_id&&(i.selected=!0),a.appendChild(i)}),document.getElementById("qf-background").checked=t?.is_background??!1,document.getElementById("qf-private").checked=!!(t&&t.is_private),document.getElementById("qf-stage").checked=!!(t&&t.pending_reveal),document.getElementById("qf-stage-row").classList.toggle("hidden",!_isGm()),_questModalSyncPrivateRow(),document.getElementById("quest-modal").classList.remove("hidden"),document.getElementById("qf-title").focus()}function _questModalSyncPrivateRow(){const e=document.getElementById("qf-private-row"),t=_isGm()&&!!document.getElementById("qf-pc").value;e.classList.toggle("hidden",!t);const n=document.getElementById("qf-private");t||(n.checked=!1);const a=document.getElementById("qf-background");n.checked&&(a.checked=!0),a.disabled=n.checked}document.getElementById("qf-private").addEventListener("change",_questModalSyncPrivateRow);function closeQuestModal(){document.getElementById("quest-modal").classList.add("hidden")}document.getElementById("quest-modal-close").addEventListener("click",closeQuestModal),document.getElementById("quest-form-cancel").addEventListener("click",closeQuestModal),document.getElementById("qf-pc").addEventListener("change",()=>{document.getElementById("qf-pc").value&&(document.getElementById("qf-background").checked=!0),_questModalSyncPrivateRow()});let _questModalMousedownOnBackdrop=!1;document.getElementById("quest-modal").addEventListener("mousedown",e=>{_questModalMousedownOnBackdrop=e.target===document.getElementById("quest-modal")}),document.getElementById("quest-modal").addEventListener("click",e=>{e.target===document.getElementById("quest-modal")&&_questModalMousedownOnBackdrop&&closeQuestModal()}),document.getElementById("quest-form").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("quest-form-error");t.classList.add("hidden");const n=e.target.querySelector("[type=submit]");n.disabled=!0;const a=document.getElementById("qf-session").value,s=document.getElementById("qf-pc").value,i={title:document.getElementById("qf-title").value.trim(),status:document.getElementById("qf-status").value,description:document.getElementById("qf-description").value.trim()||null,introduced_session_id:a?parseInt(a):null,pc_entity_id:s?parseInt(s):null,is_background:document.getElementById("qf-background").checked};_isGm()&&(i.is_private=!!s&&document.getElementById("qf-private").checked,i.pending_reveal=document.getElementById("qf-stage").checked);try{if(editingQuestId){const o=await api("PUT",`/api/campaigns/${currentCampaign.id}/quests/${editingQuestId}`,i);allQuests=allQuests.map(r=>r.id===o.id?o:r)}else{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/quests`,i);allQuests.unshift(o)}closeQuestModal(),renderQuests()}catch(o){t.textContent=o.message,t.classList.remove("hidden")}finally{n.disabled=!1}});let chatMessages=[],chatLoaded=!1;function toggleChatPopout(e){if(_isReadOnlyRole()&&e!==!1)return;const t=document.getElementById("chat-popout"),n=document.getElementById("chat-fab"),a=!t.classList.contains("hidden"),s=e!==void 0?e:!a;t.classList.toggle("hidden",!s),n.classList.toggle("chat-fab-open",s),s&&(loadChat(),setTimeout(()=>document.getElementById("chat-input")?.focus(),100))}document.getElementById("chat-fab").addEventListener("click",()=>toggleChatPopout()),document.getElementById("chat-close-btn").addEventListener("click",()=>toggleChatPopout(!1));function renderChat(){const e=document.getElementById("chat-messages");if(chatMessages.length===0){e.innerHTML='<div class="chat-empty"><p>What scandal needs untangling? Ask after a name, a fight, a romance gone sidew
1272ays, a bargain that went south, I\u2019ve stanzas half-finished about most of it.</p></div>';return}e.innerHTML="";let t=0,n=0;for(;t<chatMessages.length;){const a=chatMessages[t],s=chatMessages[t+1];let i=null,o=null;a.role==="user"&&s&&s.role==="assistant"?(i=a,o=s,t+=2):a.role==="assistant"?(o=a,t+=1):(i=a,t+=1);const r=!!(i&&o&&(o.content||"").trim().length>=40),c=document.createElement("div");c.className="chat-pair",c.dataset.shareable=r?"true":"false";let l=n>0?'<div class="chat-pair-divider" aria-hidden="true"></div>':"";if(i&&(l+='<div class="chat-msg chat-msg-user"><div class="chat-bubble">'+escHtml(i.content).replace(/\n/g,"<br>")+"</div></div>"),o){const d=Array.isArray(o.tools_used)&&o.tools_used.length?_renderToolsUsed(o.tools_used):"";l+='<div class="chat-msg chat-msg-assistant"><div class="chat-bubble">'+_renderChatMarkdown(o.content)+d+"</div></div>"}r&&(l+='<div class="chat-pair-actions"><button type="button" class="chat-pair-share-btn" title="Share this exchange" aria-label="Share this question and answer"><svg aria-hidden="true" viewBox="0 0 24 24" width="13" height="13"><path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8M16 6l-4-4-4 4M12 2v13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Share</span></button></div>'),c.innerHTML=l,r&&c.querySelector(".chat-pair-share-btn").addEventListener("click",d=>{d.stopPropagation(),_bardShareStart({question:i.content,answer:o.content},d.currentTarget)}),e.appendChild(c),n++}e.scrollTop=e.scrollHeight}async function _bardShareStart(e,t){const n=currentCampaign&&currentCampaign.id;if(!n||t.dataset.busy==="1")return;t.dataset.busy="1";const a=t.querySelector("span"),s=a?a.textContent:"Share";a&&(a.textContent="Summoning\u2026");try{const i=await api("POST",`/api/campaigns/${n}/bard-cards`,{question:e.question,answer:e.answer});let o=i,r=0;for(;o&&o.status!=="done"&&o.status!=="failed"&&r<25;){if(await new Promise(c=>setTimeout(c,1200)),!currentCampaign||currentCampaign.id!==n)return;o=await api("GET",`/api/campaigns/${n}/bard-cards/${i.id}`),r++}if(!o||o.status!=="done"||!o.png_url){showToast("The bard is still composing, try Share again in a moment.");return}_bardShareSheet(o.png_url,_bardShareText(e.question),t)}catch{showToast("Could not make the card. Try again.")}finally{t.dataset.busy="0",a&&(a.textContent=s)}}function _bardShareText(e){const t=(e||"").trim();return(t?`"${t}" \u2014 `:"")+"asked of the Bardic Companion \xB7 thread-fall.com"}function _tfNativeSharePreferred(){try{const e=navigator.userAgent||"";if(/android|iphone|ipod|ipad/i.test(e)||/macintosh/i.test(e)&&navigator.maxTouchPoints>1)return!0}catch{}return!1}function _bardShareSheet(e,t,n){document.querySelectorAll(".tf-share-menu").forEach(c=>c.remove());let a=!1;try{a=!!(navigator.canShare&&navigator.canShare({files:[new File([new Blob],"c.png",{type:"image/png"})]}))}catch{a=!1}const s=document.createElement("div");s.className="tf-share-menu",s.innerHTML='<button type="button" class="tf-share-opt tf-share-opt--primary" data-sh="file">Share</button><button type="button" class="tf-share-opt" data-sh="copy-img">Copy image</button><button type="button" class="tf-share-opt" data-sh="x">Share on X</button><button type="button" class="tf-share-opt" data-sh="download">Download</button>',document.body.appendChild(s);const i=n.getBoundingClientRect();s.style.top=`${Math.max(8,Math.min(i.bottom+6,window.innerHeight-s.offsetHeight-8))}px`,s.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-s.offsetWidth-8))}px`;const o=()=>s.remove(),r=c=>s.querySelector(`[data-sh="${c}"]`);r("file").onclick=async()=>{if(a&&_tfNativeSharePreferred())try{const c=await fetch(e).then(l=>l.blob());await navigator.share({files:[new File([c],"threadfall-bardic-exchange.png",{type:"image/png"})],text:t})}catch{}else _tfCopyText(e?`${t}
1273${e}`:t,"Post copied. Paste into Discord. Text and card image included.");o()},r("download").onclick=()=>{const c=document.createElement("a");c.href=e,c.download="threadfall-bardic-exchange.png",document.body.appendChild(c),c.click(),c.remove(),o()},r("x").onclick=()=>{window.open("https://twitter.com/intent/tweet?text="+encodeURIComponent(t),"_blank","noopener"),o()},r("copy-img")&&(r("copy-img").onclick=async()=>{try{const c=await fetch(e).then(l=>l.blob());await navigator.clipboard.write([new ClipboardItem({[c.type||"image/png"]:c})]),showToast("Card copied, paste it into any chat or Discord.")}catch{try{await navigator.clipboard.writeText(e),showToast("Link copied.")}catch{}}o()}),setTimeout(()=>document.addEventListener("click",function c(l){s.contains(l.target)||(o(),document.removeEventListener("click",c))}),0)}const _SHOW_CHAT_TOOLS_USED=!1;function _renderToolsUsed(e){if(!_SHOW_CHAT_TOOLS_USED||!Array.isArray(e)||!e.length)return"";const t=e.map(n=>{const a=String(n.name||"?"),s=n.input||{},i=Object.keys(s)[0];let o="";if(i!==void 0){const r=s[i];o=typeof r=="string"?`"${r}"`:String(r)}return`${a}(${o})`}).join(" \u2192 ");return`<div class="chat-tools-used"><span class="chat-tools-used-icon">\u24D8</span> ${escHtml(t)}</div>`}function _renderChatMarkdown(e){if(!e)return"";let t=String(e).replace(/^[ \t]*#{1,6}[ \t]+/gm,"").replace(/^[ \t]*[-*•·][ \t]+/gm,"").replace(/^[ \t]*\d+[.)][ \t]+/gm,"").replace(/\n{3,}/g,`
1274
1275`);
1275return t=escHtml(t),t=t.replace(/\*\*([^\n*][^\n]*?)\*\*/g,"<strong>$1</strong>").replace(/__([^\n_][^\n]*?)__/g,"<strong>$1</strong>"),t=t.replace(/(^|[\s(])\*([^\s*][^*\n]*?)\*(?=[\s.,!?;:)\]]|$)/g,"$1<em>$2</em>").replace(/(^|[\s(])_([^\s_][^_\n]*?)_(?=[\s.,!?;:)\]]|$)/g,"$1<em>$2</em>"),t.replace(/\n/g,"<br>")}async function loadChat(){if(chatLoaded){renderChat();return}const e=currentCampaign?.id;try{const t=await _apiAllPages("/api/campaigns/"+e+"/messages");if(_campaignStale(e))return;chatMessages=t,chatLoaded=!0,renderChat()}catch(t){document.getElementById("chat-messages").innerHTML='<p class="error placeholder--padded">'+t.message+"</p>"}}const _BARDIC_PLACEHOLDERS=["Out with it\u2026","What scandal needs untangling?","Whose folly tonight?","Whisper a name, I\u2019ll spin a stanza\u2026","Ask, I\u2019ve stanzas half-written about most of it.","Whose bed, whose grave, whose bargain?","What needs a limerick today?","A name, a fight, a regrettable romance\u2026","Try me. I\u2019ve a verse for everything.","Speak, and I\u2019ll dredge up the appropriate gossip."];function _rotateChatPlaceholder(){const e=document.getElementById("chat-input");if(!e)return;const t=Math.floor(Math.random()*_BARDIC_PLACEHOLDERS.length);e.placeholder=_BARDIC_PLACEHOLDERS[t]}async function sendChatMessage(){const e=document.getElementById("chat-input"),t=e.value.trim();if(!t)return;const n=document.getElementById("chat-send-btn");n.disabled=!0,e.disabled=!0,chatMessages.push({role:"user",content:t}),e.value="",renderChat();const a=document.getElementById("chat-messages"),s=document.createElement("div");s.className="chat-msg chat-msg-assistant",s.id="chat-typing",s.innerHTML='<div class="chat-bubble chat-typing"><span></span><span></span><span></span></div>',a.appendChild(s),a.scrollTop=a.scrollHeight;try{const i=await api("POST","/api/campaigns/"+currentCampaign.id+"/chat",{content:t});chatMessages.push({role:"assistant",content:i.content,tools_used:i.tools_used||[]}),renderChat()}catch(i){chatMessages.pop(),chatMessages.push({role:"assistant",content:"\u26A0\uFE0F Error: "+i.message}),renderChat()}finally{document.getElementById("chat-typing")?.remove(),n.disabled=!1,e.disabled=!1,_rotateChatPlaceholder(),e.focus()}}document.getElementById("chat-send-btn").addEventListener("click",sendChatMessage),document.getElementById("chat-input").addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),sendChatMessage())}),document.getElementById("chat-clear-btn").addEventListener("click",async()=>{if(await confirmDialog({title:"Clear Chat History",message:"Clear your Bardic Companion history for this campaign? Other members\u2019 conversations are not affected.",confirmLabel:"Clear"}))try{await api("DELETE","/api/campaigns/"+currentCampaign.id+"/messages"),chatMessages=[],renderChat()}catch(e){showToast("Clear failed: "+e.message)}});let _rosterPcs=[],_rosterMembers=[],_rosterSeries=null;async function loadRoster(){const e=document.getElementById("roster-table-wrap");if(!e||!currentCampaign)return;const t=currentCampaign.id;e.innerHTML='<div class="loading-row loading-row--center"><span class="spinner"></span></div>';try{const[n,a,s]=await Promise.all([api("GET",`/api/campaigns/${t}/roster`),api("GET",`/api/campaigns/${t}/members`),api("GET",`/api/campaigns/${t}/series-viewers`).catch(()=>null)]);if(_campaignStale(t))return;_rosterPcs=n,_rosterMembers=a,_rosterSeries=s&&s.shared?s:null,renderRoster();const i=document.getElementById("camp-member-count");if(i){const o=(_rosterMembers||[]).length;i.textContent=o===1?"1 member":`${o} members`}}catch(n){e.innerHTML=`<p class="error placeholder--padded">${escHtml(n.message)}</p>`}}function _tfOwnershipChangeAllowedAt(){const e=currentCampaign&&currentCampaign.primary_gm_changed_at;if(!e)return null;const t=new Date(e).getTime();return isNaN(t)?null:t+30*24*60*60*1e3}function renderRoster(){const e=document.getElementById("roster-table-wrap");if(!e)return;typeof _tfManageRailDot=="function"&&_tfManageRailDot("players",_rosterMembers.some(o=>o.role!=="gm")||_rosterPcs.some(o=>o.assigned_user_id));const t=o=>
1275o==="gm"?"GM":o==="co_gm"?"Co-GM":o==="observer"?"Observer":"Player";let n="";if(_rosterSeries&&_rosterSeries.shared){const o=(_rosterSeries.viewers||[]).map(r=>{const c=escHtml(r.display_name||r.email||"Unknown user"),l=(r.via||[]).map(d=>"a "+t(d.role)+" in "+escHtml(d.campaign_name)).join(", ");return`<div class="roster-series-row"><strong>${c}</strong><span class="roster-series-via">Can read this campaign as ${l}</span></div>`}).join("");n=`<div class="roster-series-block"><span class="form-section-label">Series access</span><p class="form-helper-text">"${escHtml(_rosterSeries.anthology_title||"This series")}" is shared with players, so everyone in its campaigns can read this one. Control this with the share toggle in Manage Anthologies.</p>`+(o||'<p class="form-helper-text">No one outside this campaign is in the series yet.</p>')+"</div>"}if(!_rosterMembers.length){e.innerHTML='<p class="placeholder placeholder--padded">No members yet. Share the invite code above so players can join.</p>'+n;return}const a=_rosterPcs.filter(o=>!o.assigned_user_id),s=_rosterMembers.filter(o=>o.role==="gm").length,i=_rosterMembers.map(o=>{const r=o.display_name?`<strong>${escHtml(o.display_name)}</strong><span class="roster-member-email">${escHtml(o.email)}</span>`:`<strong>${escHtml(o.email)}</strong>`,c=currentCampaign&&o.user_id===currentCampaign.primary_gm_id,l=(o.role==="gm"?'<span class="camp-role-badge camp-role-gm">GM</span>'+(c?` <span class="tf-badge tf-badge--gold" title="Campaign owner: their plan sets this campaign's AI features and session quota, and the GM credit follows them">Owner</span>`:""):o.role==="co_gm"?'<span class="camp-role-badge camp-role-co-gm">Co-GM</span>':o.role==="observer"?`<span class="camp-role-badge camp-role-observer" title="Can read the campaign and watch sessions live, but can't post or edit">Observer</span>`:'<span class="camp-role-badge camp-role-member">Player</span>')+(o.verified===!1?` <span class="camp-role-badge roster-unverified" title="Hasn't verified their email yet. They stay on the roster, but must verify before joining a Live-at-the-table session, that's where we capture recording consent.">Unverified</span>`:""),d=o.role==="gm"&&s>1;let u="";o.role==="gm"?u=`<button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="co_gm">\u2192 Co-GM</button>`+(d?` <button class="btn-ghost btn-ghost--danger btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="member">\u2192 Player</button>`:""):o.role==="co_gm"?u=`<button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="gm">\u2191 GM</button> <button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="member">\u2193 Player</button>`:o.role==="observer"?u=`<button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="member">\u2191 Player</button>`:u=`<button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="co_gm">\u2191 Co-GM</button> <button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="gm">\u2191 GM</button> <button class="btn-ghost btn-xs roster-role-btn" data-uid="${o.user_id}" data-new-role="observer">\u2193 Observer</button>`;const p=currentUser&&o.email&&o.email===currentUser.email,m=currentCampaign&&o.user_id===currentCampaign.primary_gm_id,f=_rosterMembers.some(_=>_.role==="gm"&&_.email!==(currentUser&&currentUser.email)),g=_rosterMembers.find(_=>currentCampaign&&_.user_id===currentCampaign.primary_gm_id),h=!!(currentUser&&(currentUser.id&&currentCampaign&&currentUser.id===currentCampaign.primary_gm_id||g&&g.email&&g.email===currentUser.email||currentUser.is_admin));if(o.role==="gm"&&!m&&h){const _=_tfOwnershipChangeAllowedAt(),w=_&&_>Date.now()&&!(currentUser&&currentUser.is_admin),$=w?`Ownership changed recently. It can change again on ${new Date(_).toLocaleDateString(void 0,{month:"short",day:"numeric"})}.`:"Hand campaign ownership to this GM";u+=` <button class="btn-ghost btn-xs roster-owner-btn" data-uid="${o.user_id}" data-name="${escHtml(o.display_name||o.email)}
1275" title="${escHtml($)}"${w?" disabled":""}>Make owner</button>`}p?(!m&&o.role!=="gm"||f)&&(u+=' <button class="btn-ghost btn-ghost--danger btn-xs roster-leave-btn" title="Leave this campaign">Leave campaign</button>'):m||(u+=` <button class="btn-ghost btn-ghost--danger btn-xs roster-remove-btn" data-uid="${o.user_id}" data-name="${escHtml(o.display_name||o.email)}" title="Remove from campaign">Remove</button>`);const b=_rosterPcs.filter(_=>_.assigned_user_id===o.user_id).map(_=>`
1276      <span class="pc-chip" data-pc-id="${_.id}">
1277        <span class="pc-chip-name">${escHtml(_.name)}</span>
1278        <button class="pc-chip-remove" data-pc-id="${_.id}" title="Unassign PC" aria-label="Unassign ${escHtml(_.name)}">\xD7</button>
1279      </span>
1280    `).join(""),y=a.length&&o.role!=="observer"?`<button class="pc-add-btn btn-new btn-xs" data-uid="${o.user_id}">+ Add</button>`:"";return`
1281      <div class="tf-card tf-card--md manage-roster-row" data-uid="${o.user_id}">
1282        <div class="roster-member-cell">${r}</div>
1283        <div class="roster-pcs-cell">
1284          <div class="pc-chips">${b}${y}</div>
1285        </div>
1286        <div class="roster-role-cell">
1287          ${l}
1288          <span class="roster-role-actions">${u}</span>
1289        </div>
1290      </div>
1291    `}).join("");e.innerHTML=`
1292    <div class="manage-roster">
1293      <div class="manage-roster-hdr" aria-hidden="true"><span>Member</span><span>Characters</span><span>Role</span></div>
1294      ${i}
1295    </div>
1296  `+n,_bindRosterCombinedRows(),e.querySelectorAll(".roster-role-btn").forEach(o=>{o.addEventListener("click",async()=>{const r=parseInt(o.dataset.uid),c=o.dataset.newRole;if(!(c==="co_gm"&&!await confirmDialog({title:"Promote to Co-GM?",message:"Co-GMs can fire AI features (Recap, Loose Threads, Fates, Bardic) in this campaign. Their AI usage draws from YOUR session allotment, not theirs. You're paying for the AI they trigger here. Confirm only if you trust this user with that access.",confirmLabel:"Promote to Co-GM",cancelLabel:"Keep as Player",danger:!1})))try{await api("PUT",`/api/campaigns/${currentCampaign.id}/members/${r}`,{role:c});const l=_rosterMembers.find(d=>d.user_id===r);l&&(l.role=c),renderRoster()}catch(l){showToast(l.message||"Role change failed")}})}),e.querySelectorAll(".roster-owner-btn").forEach(o=>{o.disabled||o.addEventListener("click",async()=>{const r=parseInt(o.dataset.uid),c=o.dataset.name||"this GM";if(await confirmDialog({title:"Hand off ownership?",message:`${c} becomes the owner of this campaign. Their plan then decides which AI features it has and how many sessions a month it gets, the GM credit on quote cards and sheets follows them, and you stay on as a GM. Ownership can change once every 30 days.`,confirmLabel:"Make owner",cancelLabel:"Keep it",danger:!0})){o.disabled=!0;try{const d=await api("POST",`/api/campaigns/${currentCampaign.id}/primary-gm`,{user_id:r});currentCampaign&&(currentCampaign.primary_gm_id=d.primary_gm_id,currentCampaign.primary_gm_changed_at=new Date().toISOString());const u=(_csAllCampaigns||[]).find(p=>p.id===currentCampaign.id);u&&(u.primary_gm_id=d.primary_gm_id),showToast(`${c} now owns this campaign.`,4e3),renderRoster()}catch(d){o.disabled=!1,showToast(d.message||"Could not hand off ownership.",5e3)}}})}),e.querySelectorAll(".roster-remove-btn").forEach(o=>{o.addEventListener("click",async()=>{const r=parseInt(o.dataset.uid),c=o.dataset.name||"this member";if(await confirmDialog({title:"Remove from campaign?",message:`Remove ${c} from this campaign? They lose access to Live, recaps, party, and codex here. Any PCs they claimed stay in the campaign, and you can re-invite them with the campaign code.`,confirmLabel:"Remove",cancelLabel:"Keep",danger:!0}))try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/members/${r}`),_rosterMembers=_rosterMembers.filter(d=>d.user_id!==r),renderRoster(),showToast(`${c} removed from the campaign.`)}catch(d){showToast(d&&d.message||"Could not remove member.")}})}),e.querySelector(".roster-leave-btn")?.addEventListener("click",async()=>{const o=currentCampaign&&currentCampaign.name||"this campaign",r=_rosterMembers.find(u=>u.email===(currentUser&&currentUser.email)),c=r&&currentCampaign&&r.user_id===currentCampaign.primary_gm_id,l=_rosterMembers.find(u=>u.role==="gm"&&u.email!==(currentUser&&currentUser.email));if(await confirmDialog({title:"Leave campaign?",message:`Leave \u201C${o}\u201D? You'll lose access to it.`+(c&&l?` Ownership passes to ${l.display_name||l.email}.`:" The GM can re-invite you with the campaign code."),confirmLabel:"Leave",cancelLabel:"Stay",danger:!0}))try{await api("POST",`/api/campaigns/${currentCampaign.id}/leave`),showToast(`You left \u201C${o}\u201D.`),document.getElementById("nav-logo-btn")?.click()}catch(u){showToast(u&&u.message||"Could not leave the campaign.")}})}function _bindRosterCombinedRows(){const e=document.getElementById("roster-table-wrap");e&&(e.querySelectorAll(".pc-chip-remove").forEach(t=>{t.addEventListener("click",async()=>{const n=parseInt(t.dataset.pcId);try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/roster/${n}/assignment`);const a=_rosterPcs.find(s=>s.id===n);a&&(a.assigned_user_id=null,a.assigned_email=null,a.assigned_display_name=null,allEntities=allEntities.map(s=>s.id===n?{...s,assigned_user_id:null,assigned_email:null,assigned_display_name:null}:s)),renderRoster()}catch(a){showToast(a.message||"Unassign failed")}})}),e.querySelectorAll(".pc-add-btn").forEach(t=>{t.addEventListener("click",()=>_openPcAddPicker(t,parseInt(t.dataset.uid)))}
1296))}function _openPcAddPicker(e,t){const n=_rosterPcs.filter(o=>!o.assigned_user_id);if(!n.length){showToast("No unassigned PCs. Create one from the Party or Codex tab.");return}const a=e.parentElement.querySelector(".pc-add-select");if(a){a.remove(),e.classList.remove("hidden");return}const s=document.createElement("select");s.className="pc-add-select roster-link-select",s.innerHTML='<option value="">(pick a PC)</option>'+n.map(o=>`<option value="${o.id}">${escHtml(o.name)}</option>`).join(""),e.classList.add("hidden"),e.parentElement.appendChild(s),s.focus();let i=!1;s.addEventListener("change",async()=>{const o=parseInt(s.value);if(o){i=!0;try{await api("PUT",`/api/campaigns/${currentCampaign.id}/roster/${o}/assignment`,{user_id:t});const r=_rosterPcs.find(l=>l.id===o),c=_rosterMembers.find(l=>l.user_id===t);r&&c&&(r.assigned_user_id=t,r.assigned_email=c.email,r.assigned_display_name=c.display_name,allEntities=allEntities.map(l=>l.id===o?{...l,assigned_user_id:t,assigned_email:c.email,assigned_display_name:c.display_name}:l)),renderRoster()}catch(r){showToast(r.message||"Assign failed"),s.remove(),e.classList.remove("hidden")}}}),s.addEventListener("blur",()=>{setTimeout(()=>{i||s.parentElement&&(s.remove(),e.classList.remove("hidden"))},150)})}let liveSession=null,liveLogEntries=[],livePcSidebarPcs=[],livePcSidebarIdx=0,livePcVitalsView=!0,liveTimerInterval=null,liveElapsedSeconds=0,_liveMinimized=!1,_lastActiveTab="sessions",_liveWallAnchorMs=0,_liveWallAnchorTs=0;function _updateWallAnchor(e){const t=(e||[]).reduce((n,a)=>a.timestamp_seconds!=null?Math.max(n,a.timestamp_seconds):n,-1);t>=0&&(_liveWallAnchorTs=t,_liveWallAnchorMs=Date.now())}function _memberElapsedSeconds(){return _liveWallAnchorMs?_liveWallAnchorTs+Math.floor((Date.now()-_liveWallAnchorMs)/1e3):null}function _updateLiveRecordBar(){const e=document.getElementById("live-record-bar"),t=document.getElementById("lrb-dot"),n=document.getElementById("lrb-label"),a=document.getElementById("lrb-timer"),s=document.getElementById("lrb-end-btn");if(e)if(liveRecording)t.className="lrb-dot lrb-dot--rec",n.textContent="Recording in background",a.textContent=fmtTime(liveElapsedSeconds),s.classList.add("hidden");else if(_tfDiscordRecActive)t.className="lrb-dot lrb-dot--rec",n.textContent="Recording on Discord",a.textContent=_fmtRemoteClock(Date.now()-_tfDiscordRecClockBase),s.classList.add("hidden");else{t.className="lrb-dot lrb-dot--idle";const i=liveSession?.title?`Live: ${liveSession.title}`:"Live Session Active";n.textContent=i,a.textContent="",s.classList.remove("hidden")}}function _tfRestoreLastTab(){document.querySelectorAll(".tab-btn, .nav-utility-btn[data-tab]").forEach(i=>i.classList.remove("active")),document.querySelectorAll(".tab-panel").forEach(i=>i.classList.add("hidden"));const e=_lastActiveTab||"sessions",t=document.querySelector(`[data-tab="${e}"]`),n=document.getElementById(`tab-${e}`),a=currentCampaign&&currentCampaign.id,s=()=>{t&&t.classList.add("active")};e==="sessions"?renderChronicleTimeline():e==="party"?(s(),_party2Panel().classList.remove("hidden"),renderParty2({cid:a,fromCache:!0})):e==="quests"?_partyOpenQuests(a):e==="map"?(s(),_atlas2Panel().classList.remove("hidden"),renderAtlas2({cid:a})):t&&n?(s(),n.classList.remove("hidden")):renderChronicleTimeline(),_scrollActiveTabIntoView()}function _minimizeLiveSession(){_liveMinimized=!0,document.getElementById("live-session-screen").classList.add("hidden"),document.getElementById("main-app").classList.remove("hidden"),_updateLiveRecordBar(),document.getElementById("live-record-bar").classList.remove("hidden"),_tfRestoreLastTab()}function _returnToLiveSession(){if(_liveMinimized=!1,document.getElementById("live-record-bar").classList.add("hidden"),document.getElementById("main-app").classList.add("hidden"),document.getElementById("live-session-screen").classList.remove("hidden"),_tfHeldRecording&&_tfShowHeldRecordingBanner(),renderLiveLog(),renderLiveQuestPanel(),liveRecording?(document.getElementById("rec-ready")?.classList.add("hidden"),document.getElementById("rec-active")?.classList.remove("hidden"),document.getElementById("live-header-rec-dot")?.classList.remove("hidden")):_tfDiscordRecActive&&(document.getElementById("rec-ready")?.classList.add("hidden"),document.getElementById("rec-remote-active")?.classList.remove("hidden"),document.getElementById("live-header-rec-dot")?.classList.remove("hidden")),liveTranscriptSegments.length>0||liveTranscriptAccum)if(document.getElementById("live-transcript-panel")?.classList.remove("hidden"),liveTranscriptSegments.length>0)updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,"");else{const e=document.getElementById("live-transcript-box"),t=document.getElementById("live-transcript-wordcount");if(e&&(e.innerHTML=_buildTranscriptHTML(liveTranscriptAccum),e.scrollTop=e.scrollHeight),t){const n=liveTranscriptAccum.trim().split(/\s+/).filter(
1296Boolean).length;t.textContent=`${n} word${n!==1?"s":""}`}}_switchLiveTransTab(_liveTransTab)}let liveMap=null,_liveResizeObs=null,livePartyMarker=null,liveEntryMarkers=[],liveEntityMarkers=[],liveMapLabels=[],livePartyPos=null,liveAllMaps=[],_liveAllCampaignMaps=[],_livePlacements={},liveCurrentMapIdx=0,liveSettingPos=!1,liveImgH=0,liveImgW=0,_liveDir=null,_liveDirRoads=[],_liveDirRouteLayers=[],_liveDirScrubLayers=[],_liveDirCampMarkers=[],_liveDirAltLayers=[],_liveNearbyPace="normal",liveRecording=!1,_testRecordingMode=!1;function _liveTokenUrl(){const e=currentCampaign&&currentCampaign.id,t=[];return e&&t.push("campaign_id="+encodeURIComponent(e)),_testRecordingMode&&t.push("test=1"),"/api/live/token"+(t.length?"?"+t.join("&"):"")}function _tfLiveCapNotice(e){const t=e&&e.message||"";/capped at 4 hours/i.test(t)&&showToast(t,15e3)}let _testAllowance=null,_testClipTimer=null;const _TEST_LABEL="=== TEST RECORDING (free trial \u2014 not included in recaps) ===";let liveSpeechRec=null,liveTranscriptAccum="",_priorTranscriptBaseline="";const _MIC_CHECK_MAX_MS=45e3,_MIC_CHECK_MAX_CHARS=400;let _liveAutoCreatedSessionId=null,liveTranscriptSegments=[],liveCurrentSegText="",liveCurrentSegHas=!1,liveSpeakerIdx=0,_liveSpeakerIdxBase=0,_liveCurTurnOrders=[],liveTranscriptSaveTimer=null,liveSegmentIdleTimer=null,liveRecordingStartTime=0,liveAnalyser=null,liveSpeakerDetectInterval=null,liveMediaRecorder=null,liveMicStream=null,liveDisplayStream=null,_chimeTimeout=null,allFeedItems=[],_feedLatestId=0,_feedPollTimer=null,_feedLoaded=!1,_liveTransTab="transcript";function _switchLiveTransTab(e){_liveTransTab=e,document.getElementById("live-tab-transcript-btn")?.classList.toggle("active",e==="transcript"),document.getElementById("live-tab-speakers-btn")?.classList.toggle("active",e==="speakers"),document.getElementById("live-transcript-box")?.classList.toggle("hidden",e!=="transcript"),document.getElementById("live-transcript-wordcount")?.classList.toggle("hidden",e!=="transcript"),document.getElementById("live-spk-bar-wrap")?.classList.toggle("hidden",e!=="speakers")}const LIVE_SCAN_INTERVAL_MS=6e4;let _liveScanOffset=0,_liveScanTimer=null,_broadcastActive=!1,_watcherPollTimer=null,_watcherLastSeq=0,_watcherIsLive=!1,_watcherPanelOpen=!1;function _playChime(){try{const e=new AudioContext,t=e.currentTime;[[880,t],[660,t+.38]].forEach(([n,a])=>{const s=e.createOscillator(),i=e.createGain();s.type="sine",s.frequency.value=n,i.gain.setValueAtTime(0,a),i.gain.linearRampToValueAtTime(.18,a+.04),i.gain.exponentialRampToValueAtTime(.001,a+.5),s.connect(i),i.connect(e.destination),s.start(a),s.stop(a+.5)}),setTimeout(()=>e.close().catch(()=>{}),1200)}catch{}}function _scheduleChime(e){_cancelChime();let t,n,a;e==="first"?(t=5e3,n=0,a=!1):(t=18e5,n=6e5,a=!1);const s=t+(Math.random()-.5)*2*n;_chimeTimeout=setTimeout(()=>{liveMediaRecorder?.state==="recording"&&(_playChime(),_scheduleChime(a))},s)}function _cancelChime(){_chimeTimeout&&(clearTimeout(_chimeTimeout),_chimeTimeout=null)}function _setCaptureState(e){if(_captureState===e)return;const t=_captureState;_captureState=e,_liveTel("state_change",{from:t,to:e});const n=document.querySelector(".rec-dot");n&&(n.classList.remove("capture-lost","recovering","recovery-failed"),e===_CSTATE.CAPTURE_LOST||e===_CSTATE.TRANSCRIPTION_STALLED?n.classList.add("capture-lost"):e===_CSTATE.RECOVERING?n.classList.add("recovering"):e===_CSTATE.RECOVERY_FAILED&&n.classList.add("recovery-failed"))}function _showCaptureAlert(e,t="error",n="",a=null){const s=document.getElementById("capture-health-banner"),i=document.getElementById("capture-health-msg"),o=document.getElementById("capture-health-detail"),r=document.getElementById("capture-health-icon"),c=document.getElementById("capture-health-actions");if(!(!s||!i)){if(i.textContent=e,o.textContent=n,r.textContent=t==="error"||t==="warning"?"\u26A0":"\u2139",s.className=`capture-health-banner ${t}`,c){c.textContent="";const l=Array.isArray(a)?a.filter(d=>d&&d.label):[];for(const d of l){const u=document.createElement("button");u.type="button",u.className=d.cls||"btn-ghost btn-sm",u.textContent=d.label,u.addEventListener("click",()=>{try{d.onClick(u)}catch(p){console.error("[capture-alert]",p)}}),c.appendChild(u)}c.classList.toggle("hidden",l.length===0),s.classList.toggle("has-actions",l.length>0)}_liveTel("alert_shown",{level:t,msg:e,detail:n,actions:(a||[]).length})}}function _hideCaptureAlert(){document.getElementById("capture-health-banner")?.classList.add("hidden")}async function _tfCaptureAlertDismissClick(){if(_tfHeldRecording){if(!await confirmDialog({title:"Discard this recording?",message:"It has not been saved anyw
1296here. Download it first if you want to keep it.",confirmLabel:"Discard",cancelLabel:"Keep",danger:!0}))return;_liveTel("held_recording_discard",{bytes:_tfHeldRecording.blob?.size||0}),_tfHeldRecording=null}_hideCaptureAlert()}document.addEventListener("DOMContentLoaded",()=>{document.getElementById("capture-health-dismiss")?.addEventListener("click",_tfCaptureAlertDismissClick),_initPortraitCrop()});function _insertTranscriptGap(e){if(!liveRecordingStartTime)return;const t=Date.now()-liveRecordingStartTime,n=Math.floor(t/6e4),a=Math.floor(t%6e4/1e3),s=`[${String(n).padStart(2,"0")}:${String(a).padStart(2,"0")}]`,i=`
1297
1298${s} --- [CAPTURE INTERRUPTED: ${e}] ---
1299
1300`;liveTranscriptAccum+=i,scheduleSaveTranscript(),_liveTel("gap_marker_inserted",{ts:s,reason:e})}function _startWatchdog(){_stopWatchdog(),_wdLastChunkMs=Date.now(),_wdLastTranscriptMs=Date.now(),_wdSilentChunks=0,_wdRecoveryCount=0,_wdLastSoundMs=0,_wdQuietNoticeShown=!1,_liveTel("watchdog_start"),_wdInterval=setInterval(_runWatchdog,6e3)}function _stopWatchdog(){_wdInterval&&(clearInterval(_wdInterval),_wdInterval=null)}function _wdSampleSound(){try{if(!liveAnalyser||typeof liveAnalyser.getByteTimeDomainData!="function")return;const e=liveAnalyser.fftSize||2048;(!_wdSoundBuf||_wdSoundBuf.length!==e)&&(_wdSoundBuf=new Uint8Array(e)),liveAnalyser.getByteTimeDomainData(_wdSoundBuf);let t=0;for(let n=0;n<e;n++){const a=(_wdSoundBuf[n]-128)/128;t+=a*a}Math.sqrt(t/e)>=_WD_SOUND_RMS&&(_wdLastSoundMs=Date.now())}catch{}}function _runWatchdog(){if(!liveMediaRecorder||liveMediaRecorder.state!=="recording")return;const e=Date.now();_wdSampleSound();const t=e-_wdLastChunkMs;if(t>_WD_CHUNK_STALL_MS&&_captureState===_CSTATE.HEALTHY&&_wdLastChunkMs!==0){_liveTel("watchdog_capture_stall",{chunkAgeMs:t}),_handleCaptureFailure("No audio frames received for "+Math.round(t/1e3)+"s");return}liveAudioContext&&liveAudioContext.state==="suspended"&&(_liveTel("watchdog_audio_ctx_suspended"),liveAudioContext.resume().then(()=>{_liveTel("audio_ctx_resumed_watchdog")}).catch(s=>_liveTel("audio_ctx_resume_failed",{error:s.message})));const n=e-_wdLastTranscriptMs,a=_wdLastSoundMs!==0&&_wdLastSoundMs<=_wdLastTranscriptMs+_WD_SOUND_GRACE_MS;if(n>_WD_TRANSCRIPT_STALL_MS&&_captureState===_CSTATE.HEALTHY&&_wdLastTranscriptMs!==0&&a?_wdQuietNoticeShown||(_wdQuietNoticeShown=!0,_liveTel("watchdog_quiet_table",{txAgeMs:n}),_showCaptureAlert(_WD_QUIET_MSG,"info","On a break? Nothing to do. If people are talking, check the microphone.")):n>_WD_TRANSCRIPT_STALL_MS&&_captureState===_CSTATE.HEALTHY&&_wdLastTranscriptMs!==0&&(_wdQuietNoticeShown=!1,_liveTel("watchdog_transcript_stall",{txAgeMs:n}),_setCaptureState(_CSTATE.TRANSCRIPTION_STALLED),_showCaptureAlert("Transcription may have stalled, audio is still recording.","warning","No new text for "+Math.round(n/6e4)+" min"),_restartTranscriptionProvider()),t<5e3&&_captureState===_CSTATE.CAPTURE_LOST&&(_liveTel("watchdog_capture_self_recovered"),_setCaptureState(_CSTATE.HEALTHY),_hideCaptureAlert(),showToast("\u2713 Audio capture recovered.",3e3)),_wdQuietNoticeShown&&n<_WD_TRANSCRIPT_STALL_MS/3&&_captureState===_CSTATE.HEALTHY){_wdQuietNoticeShown=!1;const s=document.getElementById("capture-health-msg");s&&s.textContent===_WD_QUIET_MSG&&_hideCaptureAlert()}n<_WD_TRANSCRIPT_STALL_MS/3&&_captureState===_CSTATE.TRANSCRIPTION_STALLED&&(_setCaptureState(_CSTATE.HEALTHY),_hideCaptureAlert())}async function _handleCaptureFailure(e){if(_liveTel("capture_failure",{reason:e,recoveryCount:_wdRecoveryCount}),_setCaptureState(_CSTATE.CAPTURE_LOST),_insertTranscriptGap(e),_wdRecoveryCount>=_WD_MAX_RECOVERY){_liveTel("recovery_max_reached"),_setCaptureState(_CSTATE.RECOVERY_FAILED),_showCaptureAlert("Recording capture has stopped. Please stop and restart recording manually.","error","All captured audio so far is preserved.");return}_wdRecoveryCount++,_setCaptureState(_CSTATE.RECOVERING),_showCaptureAlert("Attempting to recover audio capture\u2026","info","Attempt "+_wdRecoveryCount+" of "+_WD_MAX_RECOVERY),_liveTel("recovery_attempt",{attempt:_wdRecoveryCount});try{if(liveAudioContext&&liveAudioContext.state==="suspended"&&(await liveAudioContext.resume(),_liveTel("audio_ctx_resumed_recovery")),liveMicStream){const t=liveMicStream.getAudioTracks();if(t.length>0&&t.every(a=>a.readyState==="ended")){_liveTel("mic_track_reacquire_start");try{const a=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1,channelCount:1}});liveAudioContext&&liveAudioContext.state!=="closed"&&(liveAudioContext.createMediaStreamSource(a).connect(liveAnalyser),liveMicStream=a,_attachMicTrackHandlers(a),_liveTel("mic_track_reacquired"))}catch(a){throw _liveTel("mic_reacquire_failed",{error:a.message}),new Error("Microphone could not be re-acquired: "+a.message)}}}if(_wdLastChunkMs=Date.now(),await new Promise(t=>setTimeout(t,6e3)),Date.now()-_wdLastChunkMs<8e3)_setCaptureState(_CSTATE.HEALTHY),_hideCaptureAlert(),showToast("\u2713 Audio capture recovered.",3e3),_liveTel("recovery_success",{attempt:_wdRecoveryCount});else throw new Error("No audio chunks after recovery wait")}catch(t){_liveTel("recovery_failed",{attempt:_wdRecoveryCount,error:t.message}),_wdRecoveryCount>=_WD_MAX_RECOVERY?(_setCaptureState(_CSTATE.RECOVERY_FAILED),_showCaptureAlert("Audio capture could not be recovered. Stop and restart recording manually.","error","All captured audio so far is preserved.")):(_setCaptureState(_CSTATE.CAPTURE_LOST),_showCaptureAlert("Recording may be interrupted. Check your microphone.","error",e))}}
1300async function _restartTranscriptionProvider(){if(_aaiCycling){_liveTel("transcription_restart_skipped_cycling");return}if(_liveTel("transcription_restart_attempt",{usingAAI:_usingAAI}),_usingAAI)(!liveWs||liveWs.readyState!==WebSocket.OPEN)&&await _attemptAaiReconnect();else{if(liveSpeechRec)try{liveSpeechRec.stop()}catch{}setTimeout(()=>{liveMediaRecorder?.state==="recording"&&_startChromeWebSpeech()},500)}}async function _attemptAaiReconnect(){if(!(_wdRecoveryCount>=_WD_MAX_RECOVERY)&&!_aaiCycling){_closeRetiringAai("reconnect"),_liveTel("aai_reconnect_attempt",{attempt:_wdRecoveryCount+1});try{const e=await api("GET",_liveTokenUrl());if(liveWsSessionComplete=!1,_stopPcmCapture(),liveWs){try{liveWs.close()}catch{}liveWs=null}_liveSpeakerIdxBase+=_getSpeakerLabelPool().length,_startAssemblyAI(e.token),_liveTel("aai_reconnect_success"),_setCaptureState(_CSTATE.HEALTHY),_wdLastTranscriptMs=Date.now(),_hideCaptureAlert(),showToast("\u2713 Transcription stream reconnected.",3e3)}catch(e){_liveTel("aai_reconnect_failed",{error:e.message}),_tfLiveCapNotice(e),_fallbackToWebSpeechWithNotice()}}}function _attachMicTrackHandlers(e){e.getAudioTracks().forEach(t=>{t.addEventListener("ended",()=>{_liveTel("mic_track_ended",{label:t.label}),liveMediaRecorder?.state==="recording"&&(_showCaptureAlert("Microphone input was interrupted.","error",t.label||""),_handleCaptureFailure("Microphone track ended"))}),t.addEventListener("mute",()=>{_liveTel("mic_track_muted",{label:t.label}),_showCaptureAlert("Microphone is muted, audio capture may be interrupted.","warning")}),t.addEventListener("unmute",()=>{_liveTel("mic_track_unmuted",{label:t.label}),_captureState!==_CSTATE.CAPTURE_LOST&&_captureState!==_CSTATE.RECOVERY_FAILED&&_hideCaptureAlert()})})}let liveAudioChunks=[],liveAudioSession=null,livePublishing=!0,liveAudioContext=null,_userInitiatedStop=!1;const _CSTATE={IDLE:"idle",HEALTHY:"healthy",CAPTURE_LOST:"capture_lost",TRANSCRIPTION_STALLED:"transcription_stalled",RECOVERING:"recovering",RECOVERY_FAILED:"recovery_failed"};let _captureState=_CSTATE.IDLE,_wdLastChunkMs=0,_wdLastTranscriptMs=0,_wdSilentChunks=0,_wdRecoveryCount=0,_wdInterval=null,_wdLastSoundMs=0,_wdQuietNoticeShown=!1;const _WD_QUIET_MSG="Quiet at the table. Still recording.";let _wdSoundBuf=null;const _WD_SOUND_RMS=.015,_WD_SOUND_GRACE_MS=2e4,_WD_CHUNK_STALL_MS=12e3,_WD_TRANSCRIPT_STALL_MS=18e4,_WD_MAX_RECOVERY=3,_liveEvents=[];let _liveTelCarry=null;const _LIVE_EVENT_MAX=600;function _liveTel(e,t={}){const n={t:Date.now(),event:e,...t};_liveEvents.push(n),_liveEvents.length>_LIVE_EVENT_MAX&&_liveEvents.shift(),e.startsWith("error")||e.startsWith("watchdog")||e.startsWith("recovery")||e.startsWith("capture")?console.warn("[live-tel]",e,t):console.log("[live-tel]",e,t)}let _liveTelFlushed=!1,_tfWakeLock=null;async function _tfAcquireWakeLock(){try{if(!("wakeLock"in navigator)||_tfWakeLock)return;const e=await navigator.wakeLock.request("screen");_tfWakeLock=e,e.addEventListener("release",()=>{_tfWakeLock===e&&(_tfWakeLock=null)}),_liveTel("wakelock_acquired")}catch(e){_tfWakeLock=null,_liveTel("wakelock_failed",{reason:e?.name||String(e)})}}function _tfReleaseWakeLock(){try{_tfWakeLock?.release?.()}catch{}_tfWakeLock=null}document.addEventListener("visibilitychange",()=>{try{document.visibilityState==="visible"&&typeof liveRecording<"u"&&liveRecording&&!_tfWakeLock&&_tfAcquireWakeLock()}catch{}});const _TF_DIM_IDLE_MS=60*1e3;let _tfDimArmed=!1,_tfDimIdleTimer=null,_tfDimClockTimer=null;function _tfDimEl(){return document.getElementById("live-dim-overlay")}let _tfDimTxKey="";function _tfDimTick(){try{const e=document.getElementById("live-dim-clock");e&&(e.textContent=fmtTime(typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0));const t=document.getElementById("live-dim-transcript");if(!t)return;const n=Array.isArray(liveTranscriptSegments)?liveTranscriptSegments.slice(-8):[],a=((typeof liveCurrentSegText=="string"?liveCurrentSegText:"")+" "+(typeof _liveInterimLast=="string"?_liveInterimLast:"")).trim(),s=n.length+"|"+(n[n.length-1]?.text||"")+"|"+a;if(s===_tfDimTxKey)return;_tfDimTxKey=s;let i=n.map(o=>`<div class="live-dim-turn"><span class="live-dim-spk">${escHtml(_resolveSegmentName(o))}</span><span class="live-dim-text">${o.text}</span></div>`).join("");a&&(i+=`<div class="live-dim-turn live-dim-turn--live"><span class="live-dim-text">${a}</span></div>`),i||(i='<div class="live-dim-turn live-dim-turn--live"><span class="live-dim-text">Listening\u2026</span></div>'),t.innerHTML=i}catch{}}function _tfDimShow(){try{const e=_tfDimEl();if(!e||!_tfDimArmed)return;_tfDimTxKey="",_tfDimTick(),e.classList.remove("hidden"),clearInterval(_tfDimClockTimer),_tfDimClockTimer=setInterval(_tfDimTick,1e3)}catch{}}function _tfDimHide(){try{_tfDimEl()?.classList.add("hidden")}catch{}clearInterval(_tfDimClockTimer),_tfDimClockTimer=null}function _tfDimRearm(){clearTimeout(_tfDimIdleTimer),_tfDimIdleTimer=null,_tfDimArmed&&(_tfDimIdleTimer=setTimeout(_tfDimShow,_TF_DIM_IDLE_MS))}function _tfDimStart(){try{if(_tfDeviceHint().device_hint!=="mobile"||typeof _testRecordingMode<"u"&&_testRecordingMode)return;_tfDimArmed=!0,_tfDimRearm()}catch{}}function _tfDimStop(){_tfDimArmed=!1,clearTimeout(_tfDimIdleTimer),_tfDimIdleTimer=null,_tfDimHide()}document.addEventListener("pointerdown",()=>{try{_tfDimArmed&&_tfDimRearm()}catch{}},!0),document.addEventListener("keydown",()=>{try{_tfDimArmed&&_tfDimRearm()}catch{}},!0);function _tfDimBind(){try{_tfDimEl()?.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation(),_tfDimHide(),_tfDimRearm()})}catch{}}
1300document.readyState!=="loading"?_tfDimBind():document.addEventListener("DOMContentLoaded",_tfDimBind);function _tfDeviceHint(){const e={device_hint:"desktop",os_hint:"other",standalone:!1};try{const t=navigator.userAgent||"";/iphone|ipod|ipad/i.test(t)||/macintosh/i.test(t)&&navigator.maxTouchPoints>1?(e.device_hint="mobile",e.os_hint="ios"):/android/i.test(t)?(e.device_hint="mobile",e.os_hint="android"):/macintosh/i.test(t)?e.os_hint="macos":/windows/i.test(t)&&(e.os_hint="windows"),e.standalone=!!(window.matchMedia?.("(display-mode: standalone)")?.matches||navigator.standalone)}catch{}return e}let _liveTelOutcome=null,_liveTelDeferOutcome=!1,_liveTelDeferTimer=null;function _liveTelDeferStart(){_liveTelOutcome=null,_liveTelDeferOutcome=!0,clearTimeout(_liveTelDeferTimer),_liveTelDeferTimer=setTimeout(()=>{_liveTelDeferOutcome=!1,_flushLiveTelemetry("stop")},15*60*1e3)}function _liveTelDeferEnd(e){_liveTelOutcome=e||null,_liveTelDeferOutcome=!1,clearTimeout(_liveTelDeferTimer),_liveTelDeferTimer=null,_flushLiveTelemetry("stop")}function _liveTelSummary(){try{const e=Array.isArray(_liveEvents)?_liveEvents:[],t=s=>e.filter(i=>i&&i.event===s).length;let n=0,a=0;try{n=new Set((liveTranscriptSegments||[]).map(s=>s.speakerIdx)).size}catch{}try{a=Object.keys(liveSpeakerNameMap||{}).length}catch{}return{event_count:e.length,reconnect_attempts:t("aai_reconnect_attempt"),reconnect_success:t("aai_reconnect_success"),reconnect_failed:t("aai_reconnect_failed"),ws_unexpected_closes:e.filter(s=>s&&s.event==="aai_ws_closed"&&s.code&&s.code!==1e3).length,ws_errors:t("aai_ws_error"),session_cycles:t("aai_session_cycle_done"),cycles_abandoned:t("aai_cycle_abandoned"),speaker_revisions:t("aai_speaker_revision"),fell_back_to_webspeech:t("aai_ws_error")>0,speaker_idx_base_final:typeof _liveSpeakerIdxBase=="number"?_liveSpeakerIdxBase:0,distinct_speaker_idx:n,named_speakers:a,elapsed_seconds:typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0,outcome:_liveTelOutcome,..._tfDeviceHint()}}catch{return{}}}function _flushLiveTelemetry(e){try{if(_liveTelFlushed||_liveTelDeferOutcome&&e==="stop")return;const t=Array.isArray(_liveEvents)?_liveEvents:[],a=(e==="stop"||e==="pagehide")&&(typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0)>0;if(!t.length&&!a)return;const s=currentCampaign?.id;if(!s)return;_liveTelFlushed=!0;let i="";try{i=localStorage.getItem("tf_jwt")||""}catch{}fetch(`/api/campaigns/${s}/live/telemetry`,{method:"POST",headers:{"Content-Type":"application/json",...i?{Authorization:"Bearer "+i}:{}},body:JSON.stringify({reason:e||"stop",session_id:typeof liveSession<"u"&&liveSession&&liveSession.id||null,summary:_liveTelSummary(),events:t.slice(-300)}),keepalive:!0}).catch(()=>{})}catch{}}window.addEventListener("pagehide",()=>{try{if(_liveTelDeferOutcome){_liveTelDeferOutcome=!1,_flushLiveTelemetry("stop");return}typeof liveRecording<"u"&&liveRecording&&!_liveTelFlushed&&_flushLiveTelemetry("pagehide")}catch{}});const LIVE_TAG_CFG={note:{label:"Note",color:"var(--text-2)"},key_event:{label:"Key Event",color:"var(--accent-amber)"},travel:{label:"Travel",color:"var(--accent-teal)"},gif:{label:"GIF",color:"var(--color-purple, #9b59b6)"},system_event:{label:"Story",color:"var(--color-gold)"},combat_event:{label:"Combat",color:"var(--color-warning)"},context_event:{label:"Context",color:"var(--color-muted-violet)"},flag:{label:"Flare",color:"var(--accent-amber)"}};let liveMyReactions={};function fmtTime(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),a=e%60;return[t,n,a].map(s=>String(s).padStart(2,"0")).join(":")}function _showConsentBadge(){const e=document.getElementById("consent-accepted-badge");e&&e.classList.remove("hidden")}async function _hasRecordingConsent(){if(_recConsentAcknowledged)return!0;if(liveSession){if(_consentedSessionId===liveSession.id)return _showConsentBadge(),!0;try{if((await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/consent`)).consented)return _consentedSessionId=liveSession.id,_recConsentAcknowledged=!0,_showConsentBadge(),!0}catch{}}return!1}document.getElementById("consent-accept-btn").addEventListener("click",async()=>{const e=document.getElementById("recording-consent-modal");if(_recConsentAcknowledged=!0,_consentRequiredToStay=!1,liveSession){try{await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/consent`)}catch{}_consentedSessionId=liveSession.id}e&&e.classList.add("hidden"),_showConsentBadge(),_pendingRecStart&&(_pendingRecStart=!1,document.getElementById("rec-start-btn").click())}),document.getElementById("consent-leave-btn").addEventListener("click",()=>{_pendingRecStart=!1,_recConsentAcknowledged=!1;const e=document.getElementById("recording-consent-modal");e&&e.classList.add("hidden"),_consentRequiredToStay&&(_consentRequiredToStay=!1,showToast("Left the live session, recording consent declined.",3500),document.getElementById("live-back-btn")?.click())});let _consentRequiredToStay=!1;async function _enforceLiveConsent(){if(!liveSession||!currentCampaign||liveRecording||await _hasRecordingConsent())return;let e=!!_watcherIsLive;if(!e)try{const t=await api("GET",`/api/campaigns/${currentCampaign.id}/broadcast`);e=!!(t&&t.is_live)}catch{}e&&(_consentRequiredToStay=!0,document.getElementById("recording-consent-modal")?.classList.remove("hidden"))}async function loadLivePcSidebar(){if(livePcSidebarPcs=[],livePcSidebarIdx=0,livePcVitalsView=!0,_isGm())livePcSidebarPcs=allEntities.filter(t=>t.type==="pc"&&!t.is_inactive&&!t.is_dead).map(t=>({entity_id:t.id,name:t.name}));else try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/my-pc`);livePcSidebarPcs=Array.isArray(e)?e:e?[e]:[]}catch{livePcSidebarPcs=[]}livePcSidebarPcs.push({entity_id:null,name:"Party Treasury",isTreasury:!0}),renderLivePcSidebar(),_isGm()&&(_ablyChannel?_ablyChannel.presence.get().then(e=>_updateLiveViewersBar(e)).catch(()=>_updateLiveViewersBar([])):_updateLiveViewersBar([]))}let _liveSaveDcItems=null,_liveSaveDcCid=null,_liveSaveDcMeta=null;const _LV_DC_LBL={concentration:"Conc",con:"CON",dex:"DEX",int:"INT",wis:"WIS",cha:"CHA"};function _liveSaveDcFooterHtml(e,t){const n=t&&Number.isFinite(t.with_data)?t.with_data:null,a=t&&Number.isFinite(t.active_total)?t.active_total:null;if(!e||!e.length)return a&&(n===0||n==null)?'<div class="lv-savedc lv-savedc--empty"><div class="lv-savedc-hdr form-section-label">Save DCs \xB7 \xBD the party fails</div><div class="lv-savedc-note">Add PC sheet data (D&amp;D Beyond import or stats) to compute these.</div></div>':"";const s=e.map(o=>`<span class="lv-savedc-item"><span class="lv-savedc-lbl">${_LV_DC_LBL[o.key]||escHtml(String(o.key))}</span><span class="lv-savedc-dc">${escHtml(String(o.tip_dc))}+</span></span>
1300`).join(""),i=n!=null&&a!=null&&n<a?`<div class="lv-savedc-note">Based on ${n} of ${a} PCs with sheet data.</div>`:"";return`<div class="lv-savedc"><div class="lv-savedc-hdr form-section-label">Save DCs \xB7 \xBD the party fails</div><div class="lv-savedc-grid">${s}</div>${i}</div>`}function _renderLiveSaveDcInto(e,t){if(!(!e||!t||!_isGm())){if(_liveSaveDcCid===t&&_liveSaveDcItems){e.innerHTML=_liveSaveDcFooterHtml(_liveSaveDcItems,_liveSaveDcMeta);return}api("GET",`/api/campaigns/${t}/party-render-spec`).then(n=>{const a=n&&Array.isArray(n.save_dcs)?n.save_dcs.filter(o=>o&&o.tip_dc!=null):[],s=n&&n.dice&&n.dice.d6||[];_liveSaveDcItems=a.length?a:s.filter(o=>o&&o.tip_dc!=null),_liveSaveDcMeta={with_data:n&&n.dice?n.dice.with_data:null,active_total:n&&n.dice?n.dice.active_total:null},_liveSaveDcCid=t;const i=document.getElementById("lv-savedc-footer");i&&(i.innerHTML=_liveSaveDcFooterHtml(_liveSaveDcItems,_liveSaveDcMeta))}).catch(()=>{})}}const _LIVE_PASS_DC_DEFAULT=13;let _livePassDc=String(_LIVE_PASS_DC_DEFAULT),_livePassDcCid=null;function _livePassDcKey(e){return`tf_live_pass_dc_${e}`}function _livePassDcHydrate(){const e=currentCampaign&&currentCampaign.id;if(e==null||e===_livePassDcCid)return;_livePassDcCid=e;let t=null;try{t=localStorage.getItem(_livePassDcKey(e))}catch{}_livePassDc=t===null?String(_LIVE_PASS_DC_DEFAULT):t}function _livePassDcPersist(){if(_livePassDcCid!=null)try{localStorage.setItem(_livePassDcKey(_livePassDcCid),_livePassDc)}catch{}}let _livePassInvOpen=!1,_liveAway=null,_liveAwayCid=null;function _liveAwayKey(e){return`tf_live_away_${e}`}function _liveAwayNight(){const e=new Date(Date.now()-18e6);return`${e.getFullYear()}-${e.getMonth()+1}-${e.getDate()}`}function _liveAwayIds(){const e=currentCampaign&&currentCampaign.id;if(e==null)return new Set;if(e!==_liveAwayCid||!_liveAway){_liveAwayCid=e,_liveAway=new Set;try{const t=JSON.parse(localStorage.getItem(_liveAwayKey(e))||"null");t&&t.night===_liveAwayNight()&&Array.isArray(t.ids)&&(_liveAway=new Set(t.ids))}catch{}}return _liveAway}function _liveAwayToggle(e){const t=currentCampaign&&currentCampaign.id;if(t==null)return;const n=_liveAwayIds();n.has(e)?n.delete(e):n.add(e);try{localStorage.setItem(_liveAwayKey(t),JSON.stringify({night:_liveAwayNight(),ids:[...n]}))}catch{}}function _liveSpkPcOptions(e){const t=_liveAwayIds(),n=(allEntities||[]).filter(s=>s.type==="pc"&&!s.is_dead&&!s.is_inactive),a=(s,i)=>`<option value="${escHtml(s.name)}"${s.name===e?" selected":""}${i?' class="spk-opt-away"':""}>${escHtml(s.name)}${i?" (absent tonight)":""}</option>`;return n.filter(s=>!t.has(s.id)).map(s=>a(s,!1)).join("")+`<option value="GM"${/^gm$/i.test(e||"")?" selected":""}>GM</option>`+n.filter(s=>t.has(s.id)).map(s=>a(s,!0)).join("")}function _livePassives(){const e=[],t=_liveAwayIds();return _activePcs().forEach(n=>{if(t.has(n.id)||!n.dnd_beyond_data)return;let a;try{a=JSON.parse(n.dnd_beyond_data)}catch{return}if(a.ai_generated===!0)return;const s=a.ability_scores||{},i=a.skills||{},o=s.WIS?s.WIS.mod:0,r=s.INT?s.INT.mod:0;e.push({name:n.name,perception:a.passive_perception??10+(i.Perception??o),insight:a.passive_insight??10+(i.Insight??o),investigation:a.passive_investigation??10+(i.Investigation??r)})}),e}let _liveCompEditing=new Set;function _liveCompanionPassives(){const e={};(allEntities||[]).forEach(n=>{e[n.id]=n});const t=[];return(allEntities||[]).forEach(n=>{if(n.type!=="companion"||!n.parent_entity_id||n.is_dead||n.is_inactive)return;const a=e[n.parent_entity_id];if(a&&(a.is_dead||a.is_inactive))return;let s={};if(n.dnd_beyond_data)try{s=JSON.parse(n.dnd_beyond_data)}catch{s={}}if(s.ai_generated===!0)return;const i=s.passive_perception,o=i!=null&&i!==""&&isFinite(Number(i)),r=s.ability_scores||{},c=10+(r.WIS&&r.WIS.mod||0);t.push({id:n.id,name:n.name,owner:a?a.name:null,perception:o?Number(i):c,confirmed:o})}),t.sort((n,a)=>a.perception-n.perception)}function _liveRecolorPassives(e){const t=_livePassDc!==""?parseInt(_livePassDc,10):null;e.querySelectorAll(".lv-pass-pill").forEach(n=>{if(n.classList.contains("lv-pass-pill--pending"))return;const a=parseInt(n.dataset.v,10);n.classList.toggle("lv-pass-pill--pass",t!=null&&!isNaN(a)&&a>=t),n.classList.toggle("lv-pass-pill--fail",t!=null&&!isNaN(a)&&a<t)})}function _liveRenderPassives(e){if(!e)return;_livePassDcHydrate();const t=_livePassives(),n=_liveCompanionPassives();if(!t.length&&!n.length){e.innerHTML="";return}const a=_livePassDc,s=a!==""?parseInt(a,10):null,i=h=>{const v=String(h||"").trim();return v.split(/\s+/)[0]||v},o=h=>'<div class="lv-pass-grid">'+t.slice().sort((v,b)=>b[h]-v[h]).map(v=>{const b=v[h];return`<span class="lv-pass-pill${s!=null?b>=s?" lv-pass-pill--pass":" lv-pass-pill--fail":""}" data-v="${escHtml(String(b))}
1300" title="${escHtml(v.name)} \xB7 ${escHtml(String(b))}"><span class="lv-pass-pn">${escHtml(i(v.name))}</span><span class="lv-pass-pv">${escHtml(String(b))}</span></span>`}).join("")+"</div>",r=(h,v)=>`<div class="lv-pass-block"><span class="lv-pass-sublbl pc-stats-field-lbl">${h}</span>${o(v)}</div>`,c=h=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${h?"M6 15l6-6 6 6":"M6 9l6 6 6-6"}"/></svg>`,l=_isGm(),d='<svg class="lv-pass-comp-glyph" viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-companion"/></svg>',u='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>',p=n.map(h=>{const v=h.owner?String(h.owner).trim().split(/\s+/)[0]:"",b=v?` <span class="lv-pass-comp-owner">(${escHtml(v)})</span>`:"",y=`${d}<span class="lv-pass-pn">${escHtml(h.name)}${b}</span>`,_=h.perception;if(l&&(!h.confirmed||_liveCompEditing.has(h.id)))return`<span class="lv-pass-pill lv-pass-comp lv-pass-pill--pending" data-v="${escHtml(String(_))}" data-comp-id="${h.id}" title="Set ${escHtml(h.name)}\u2019s passive perception, then confirm">${y}<input class="lv-pass-comp-input" type="number" inputmode="numeric" value="${escHtml(String(_))}" min="0" max="30" aria-label="${escHtml(h.name)} passive perception"><button type="button" class="lv-pass-comp-confirm" data-comp-id="${h.id}" aria-label="Confirm ${escHtml(h.name)}\u2019s passive perception">${u}</button></span>`;if(!h.confirmed)return"";const $=s!=null?_>=s?" lv-pass-pill--pass":" lv-pass-pill--fail":"",E=l?`<span class="lv-pass-pv lv-pass-comp-val" role="button" tabindex="0" data-comp-id="${h.id}" title="Edit ${escHtml(h.name)}\u2019s passive perception">${escHtml(String(_))}</span>`:`<span class="lv-pass-pv">${escHtml(String(_))}</span>`;return`<span class="lv-pass-pill lv-pass-comp${$}" data-v="${escHtml(String(_))}" data-comp-id="${h.id}" title="${escHtml(h.name)} \xB7 Passive Perception ${escHtml(String(_))}">${y}${E}</span>`}).filter(Boolean).join(""),m=p?`<div class="lv-pass-companions"><div class="lv-pass-grid lv-pass-comp-grid">${p}</div></div>`:"";e.innerHTML=`<div class="lv-pass">
1301    <div class="lv-pass-hdr">
1302      <span class="lv-pass-eyebrow form-section-label">Passive checks</span>
1303      <span class="lv-pass-dcwrap"><label class="lv-pass-dclbl pc-stats-field-lbl">DC</label><span class="lv-pass-dc-field"><input type="number" inputmode="numeric" class="lv-pass-dc-input" value="${escHtml(a)}" min="0" max="40" placeholder="\u2014" aria-label="Difficulty class"><span class="lv-pass-dc-steppers"><button type="button" class="lv-pass-dc-step" data-step="1" aria-label="Increase DC">${c(!0)}</button><button type="button" class="lv-pass-dc-step" data-step="-1" aria-label="Decrease DC">${c(!1)}</button></span></span></span>
1304    </div>
1305    ${t.length?r("Perception","perception"):""}
1306    ${m}
1307    ${t.length?r("Insight","insight"):""}
1308    ${t.length&&_livePassInvOpen?r("Investigation","investigation"):""}
1309    ${t.length?`<button type="button" class="lv-pass-inv-toggle">${_livePassInvOpen?"\u2212 Hide Investigation":"+ Investigation"}</button>`:""}
1310  </div>`;const f=e.querySelector(".lv-pass-dc-input"),g=()=>{_liveRecolorPassives(e),_livePassDcPersist()};f?.addEventListener("input",()=>{_livePassDc=f.value.trim(),g()}),e.querySelectorAll(".lv-pass-dc-step").forEach(h=>h.addEventListener("click",()=>{const v=parseInt(_livePassDc,10),b=isNaN(v)?_LIVE_PASS_DC_DEFAULT:Math.min(40,Math.max(0,v+parseInt(h.dataset.step,10)));_livePassDc=String(b),f&&(f.value=_livePassDc),g()})),e.querySelector(".lv-pass-inv-toggle")?.addEventListener("click",()=>{_livePassInvOpen=!_livePassInvOpen,_liveRenderPassives(e)}),e.querySelectorAll(".lv-pass-comp-confirm").forEach(h=>h.addEventListener("click",()=>_liveCompConfirmPassive(parseInt(h.dataset.compId,10)))),e.querySelectorAll(".lv-pass-comp-input").forEach(h=>h.addEventListener("keydown",v=>{if(v.key==="Enter"){v.preventDefault();const b=h.closest("[data-comp-id]");b&&_liveCompConfirmPassive(parseInt(b.dataset.compId,10))}})),e.querySelectorAll(".lv-pass-comp-val").forEach(h=>{const v=()=>{const b=parseInt(h.dataset.compId,10);_liveCompEditing.add(b),_liveRenderPassives(e),requestAnimationFrame(()=>{const y=e.querySelector(`.lv-pass-pill--pending[data-comp-id="${b}"] .lv-pass-comp-input`);y&&(y.focus(),y.select())})};h.addEventListener("click",v),h.addEventListener("keydown",b=>{(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),v())})})}async function _liveCompConfirmPassive(e){const t=document.getElementById("lv-pass-mount"),n=t&&t.querySelector(`.lv-pass-pill--pending[data-comp-id="${e}"] .lv-pass-comp-input`);if(!n)return;let a=parseInt(n.value,10);isFinite(a)||(a=10),a=Math.max(0,Math.min(30,a));const s=currentCampaign&&currentCampaign.id;if(s==null)return;try{await api("PATCH",`/api/campaigns/${s}/entities/${e}/sheet-overrides`,{passive_perception:a})}catch{showToast("Could not save passive perception.");return}
1310if(typeof _campaignStale=="function"&&_campaignStale(s))return;const i=(allEntities||[]).find(o=>o.id===e);if(i){let o={};try{o=JSON.parse(i.dnd_beyond_data||"{}")}catch{o={}}o.passive_perception=a,i.dnd_beyond_data=JSON.stringify(o)}_liveCompEditing.delete(e),_liveRenderPassives(document.getElementById("lv-pass-mount"))}function _dhSheet(e){if(!e||!e.dnd_beyond_data)return null;try{const t=JSON.parse(e.dnd_beyond_data);return t&&t.system==="daggerheart"?t:null}catch{return null}}const _DH_TRAITS=["agility","strength","finesse","instinct","presence","knowledge"],_DH_TRAIT_LBL={agility:"AGI",strength:"STR",finesse:"FIN",instinct:"INS",presence:"PRE",knowledge:"KNO"},_DH_D12_TOP="12,5.34 17.33,8.97 15.29,14.86 8.71,14.86 6.67,8.97",_DH_D12_S1="12,5.34 12,3.02 18.14,4.05 21.08,9.23 17.33,8.97",_DH_D12_S2="17.33,8.97 21.08,9.23 21.94,15.04 17.61,19.26 15.29,14.86",_DH_D12_S3="15.29,14.86 17.61,19.26 12,21.82 6.39,19.26 8.71,14.86",_DH_D12_S4="8.71,14.86 6.39,19.26 2.06,15.04 2.92,9.23 6.67,8.97",_DH_D12_S5="6.67,8.97 2.92,9.23 5.86,4.05 12,3.02 12,5.34";function _dhD12Svg(){return`<svg viewBox="0 0 24 24" aria-hidden="true"><polygon class="d12-s5" points="${_DH_D12_S5}"/><polygon class="d12-s1" points="${_DH_D12_S1}"/><polygon class="d12-s2" points="${_DH_D12_S2}"/><polygon class="d12-s3" points="${_DH_D12_S3}"/><polygon class="d12-s4" points="${_DH_D12_S4}"/><polygon class="d12-top" points="${_DH_D12_TOP}"/></svg>`}let _liveFearBusy=!1;function _liveFearRowHtml(){const e=Math.max(0,Math.min(12,currentCampaign&&currentCampaign.fear_pool||0)),t=_isGm(),n=Array.from({length:12},(s,i)=>{const o=i<e,r=e>0&&i===e-1,c=t?`Set Fear to ${i+1}`:`Fear ${i+1}`;return`<button type="button" class="lv-fear-pip${o?" on":""}${r?" current":""}" data-fear-set="${i+1}" ${t?"":"disabled "}title="${c}" aria-label="${c}">${_dhD12Svg()}</button>`}).join(""),a=(s,i)=>`<button type="button" class="lv-fear-step" data-fear-step="${s}" ${t?"":"disabled "}title="${i}" aria-label="${i}">${s<0?"\u2212":"+"}</button>`;return`<div class="lv-fear" role="group" aria-label="GM Fear pool">
1311    <span class="lv-fear-eyebrow form-section-label">Fear</span>
1312    ${a(-1,"Spend a Fear")}
1313    <span class="lv-fear-pips">${n}</span>
1314    ${a(1,"Gain a Fear")}
1315    <span class="lv-fear-count" aria-live="polite">${e}/12</span>
1316  </div>`}async function _liveFearSet(e,t){const n=Math.max(0,Math.min(12,currentCampaign&&currentCampaign.fear_pool||0));e=Math.max(0,Math.min(12,e));const a=currentCampaign&&currentCampaign.id;if(a==null||e===n||_liveFearBusy)return;_liveFearBusy=!0;const s=()=>{!t||!t.isConnected||(t.innerHTML=_liveFearRowHtml(),_liveWireFearRow(t))};currentCampaign.fear_pool=e,s();try{const i=await api("PUT",`/api/campaigns/${a}/fear`,{fear:e});_campaignStale(a)||(currentCampaign.fear_pool=i.fear_pool)}catch{_campaignStale(a)||(currentCampaign.fear_pool=n,s(),showToast("Could not save the Fear pool."))}finally{_liveFearBusy=!1}}function _liveRenderFearBar(){const e=document.getElementById("live-fear-mount");if(e){if(!_tfIsDaggerheart()){e.innerHTML="";return}e.innerHTML=_liveFearRowHtml(),_liveWireFearRow(e)}}function _liveWireFearRow(e){_isGm()&&(e.querySelectorAll(".lv-fear-pip").forEach(t=>{t.addEventListener("click",n=>{n.stopPropagation(),_liveFearSet(parseInt(t.dataset.fearSet,10),e)})}),e.querySelectorAll(".lv-fear-step").forEach(t=>{t.addEventListener("click",n=>{n.stopPropagation();const a=Math.max(0,Math.min(12,currentCampaign&&currentCampaign.fear_pool||0));_liveFearSet(a+parseInt(t.dataset.fearStep,10),e)})}))}const _DH_FEAR_SPELLED={one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10,eleven:11,twelve:12,thirteen:13,fourteen:14,fifteen:15,sixteen:16,seventeen:17,eighteen:18,nineteen:19,twenty:20,thirty:30,forty:40},_DH_FEAR_BLOCKS=[/\bwith\s+hope\b/i,/\b(?:or|which|what)\b/i,/\b(?:if|when|whenever|suppose)\b/i,/\bspen(?:d|t|ding)\b/i,/\b(?:last\s+time|earlier|used\s+to|before)\b/i,/\b(?:crit|crits|critical|doubles|matching)\b/i];function _dhFearClaim(e){const t=String(e||""),n=/([^.,;:!?\n]+)([.,;:!?\n]*)/g;let a;for(;a=n.exec(t);){const s=a[1];if(!s.trim()||!/\bwith\s+fear\b/i.test(s)||a[2].includes("?")||_DH_FEAR_BLOCKS.some(r=>r.test(s)))continue;const i=/\b(\d{1,2})\b/.exec(s);let o=i?parseInt(i[1],10):null;if(o==null){const r=/\b([a-z]+)\b/gi;let c;for(;c=r.exec(s);){const l=_DH_FEAR_SPELLED[c[1].toLowerCase()];if(l){o=l;break}}}if(o!=null&&o>=1)return o}return null}const _DH_FEAR_COOLDOWN_MS=8e3,_DH_FEAR_WINDOW_MS=6e3;let _dhFearLastFireAt=0,_dhFearPrev=null;function _dhFearObserve(e,t){try{if(!_tfIsDaggerheart()||!_isGm()||(e=String(e||"").trim(),!e))return;const n=Date.now();let a=_dhFearClaim(e);if(a==null&&_dhFearPrev&&n-_dhFearPrev.at<=_DH_FEAR_WINDOW_MS&&(a=_dhFearClaim(_dhFearPrev.text+" "+e)),_dhFearPrev={text:e,at:n},a==null||n-_dhFearLastFireAt<_DH_FEAR_COOLDOWN_MS)return;_dhFearLastFireAt=n,_liveTel("dh_fear_heard",{n:a,src:t}
1316),_dhFearFlash(a)}catch{}}let _dhFearFlashTimer=null;function _dhFearFlash(e){const t=document.getElementById("live-fear-mount"),n=t&&t.querySelector(".lv-fear");if(!n)return;const a=Math.max(0,Math.min(12,currentCampaign&&currentCampaign.fear_pool||0));if(a>=12)return;_dhFearFlashClear();const s=n.querySelector(`.lv-fear-pip[data-fear-set="${a+1}"]`);if(!s)return;const i=`Heard ${e} with Fear \u2014 tap to gain a Fear (${a+1}/12)`;s.classList.add("lv-fear-pip--heard"),s.title=i,s.setAttribute("aria-label",i),n.classList.add("lv-fear--heard"),_dhFearFlashTimer=setTimeout(_dhFearFlashClear,_DH_FEAR_COOLDOWN_MS)}function _dhFearFlashClear(){clearTimeout(_dhFearFlashTimer),_dhFearFlashTimer=null,document.querySelectorAll(".lv-fear--heard").forEach(e=>e.classList.remove("lv-fear--heard")),document.querySelectorAll(".lv-fear-pip--heard").forEach(e=>{e.classList.remove("lv-fear-pip--heard");const t=`Set Fear to ${e.dataset.fearSet}`;e.title=t,e.setAttribute("aria-label",t)})}const _CD_DIE_PTS={4:"20,3 37,35 3,35",6:"5,5 35,5 35,35 5,35",8:"20,2 37,20 20,38 3,20",10:"20,2 36,15 28,36 12,36 4,15",12:"20,2 37,16 30,36 10,36 3,16",20:"20,2 35,11 35,29 20,38 5,29 5,11"},_CD_RING_R=18,_CD_RING_C=2*Math.PI*_CD_RING_R;function _cdDieSvg(e,t,n,a){const s=Math.max(1,a||t||1),i=Math.max(0,Math.min(1,t/s)),o=i>0&&i<=.25,r=`<circle class="cd-die-ring-bg" cx="20" cy="20" r="${_CD_RING_R}"/><circle class="cd-die-ring${o?" cd-die-ring--low":""}" cx="20" cy="20" r="${_CD_RING_R}" stroke-dasharray="${_CD_RING_C.toFixed(1)}" stroke-dashoffset="${(_CD_RING_C*(1-i)).toFixed(1)}" transform="rotate(-90 20 20)"/>`,c=_CD_DIE_PTS[e],l=c?`d${e} showing ${t} of ${s}`:`${t} of ${s}`,d=c?`<polygon class="cd-die-face" points="${c}"/><text class="cd-die-num" x="20" y="${e===4?27:21}" text-anchor="middle" dominant-baseline="middle">${t}</text>`:`<text class="cd-die-num cd-die-num--plain" x="20" y="21" text-anchor="middle" dominant-baseline="middle">${t}</text>`;return`<svg class="cd-die-svg${n?" is-zero":""}" viewBox="0 0 40 40" role="img" aria-label="${l}">`+r+d+"</svg>"}const _CD_KIND_LBL={progress:"Progress",consequence:"Consequence"},_CD_TONE={critical:"crit",success_hope:"hope",success_fear:"fear",failure_hope:"hope",failure_fear:"fear"},_CD_KIND_ICO={consequence:'<svg class="cd-kind-ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 1.8h8M4 14.2h8M4.8 1.8v2.6L8 8l3.2-3.6V1.8M4.8 14.2v-2.6L8 8l3.2 3.6v2.6"/></svg>',progress:'<svg class="cd-kind-ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.4 2v12M11.4 2.6 5 4.9l6.4 2.3M2.6 6.2l2 1.8-2 1.8"/></svg>'},_CD_OUTCOME_LBL={critical:"Critical Success",success_hope:"Success with Hope",success_fear:"Success with Fear",failure_hope:"Failure with Hope",failure_fear:"Failure with Fear"};let _cdState={list:[],rules:null,book:null,isGm:!1,isCustom:!1},_cdQueue=Promise.resolve();async function _cdLoad(){const e=currentCampaign&&currentCampaign.id;if(e!=null)try{const t=await api("GET",`/api/campaigns/${e}/countdowns`);if(_campaignStale(e))return;_cdState={list:t.countdowns||[],rules:t.rules||null,book:t.book_rules||null,isGm:!!t.is_gm,isCustom:!!t.is_custom},_cdRenderPanel()}catch{}}function _cdRenderPanel(){const e=document.getElementById("live-cd-panel"),t=document.getElementById("live-cd-panel-body");if(!e||!t)return;const n=_isGm(),a=_tfIsDaggerheart(),s=document.getElementById("live-cd-panel-title");s&&(s.textContent=a?"Countdown":"Skill Challenge"),a||document.getElementById("live-cd-rules-btn")?.classList.add("hidden"),e.classList.toggle("hidden",!n&&!_cdState.list.length);const i=document.getElementById("live-cd-add-btn");if(i&&(i.classList.toggle("hidden",!n||_cdState.list.length>0),i.title=a?"Start a countdown":"Start a skill challenge or countdown"),!_cdState.list.length){t.innerHTML=n?`<p class="placeholder placeholder--padded">${a?"No countdown running. Start one when something begins closing in.":"Nothing running. Start a skill challenge when the party attempts something that takes several rolls, or a countdown when something closes in."}</p>`:"";return}t.innerHTML=_cdState.list.map(o=>_cdCardHtml(o,n)).join(""),_cdWireCards(t),n||_cdRevealForPlayer(_cdState.list[0])}function _cdRevealForPlayer(e){if(!e)return;const t=document.getElementById("live-cd-panel");
1316if(!t)return;const n=`tf_cd_seen_${e.id}`;let a=null;try{a=localStorage.getItem(n)}catch{return}if(!a){try{localStorage.setItem(n,"1")}catch{}t.classList.remove("collapsed")}}function _cdCardHtml(e,t){if(e.kind==="challenge")return _cdChallengeCardHtml(e,t);const n=e.current_value<=0,a=e.advancement||{},s=d=>{const u=a[d]||0,p=_CD_OUTCOME_LBL[d],m=u?`${p}: tick down ${u}`:`${p}: this countdown holds`;return`<button type="button" class="cd-out cd-out--${_CD_TONE[d]||"crit"}${u?"":" cd-out--none"}" data-cd-out="${d}"${n?" disabled":""} title="${m}" aria-label="${m}">${u||"\u2014"}</button>`},i=t&&_tfIsDaggerheart()?`<div class="cd-outgrid" role="group" aria-label="Apply a roll result">
1317        <span class="cd-outhd"></span><span class="cd-outhd cd-outhd--hope">Hope</span><span class="cd-outhd cd-outhd--fear">Fear</span>
1318        <span class="cd-outrow">Success</span>${s("success_hope")}${s("success_fear")}
1319        <span class="cd-outrow">Failure</span>${s("failure_hope")}${s("failure_fear")}
1320        <span class="cd-outrow">Critical</span><span class="cd-outcrit">${s("critical")}</span>
1321      </div>`:"",o=t?`<div class="cd-stepper" role="group" aria-label="Tick by hand">
1322        <button type="button" class="cd-step" data-cd-step="-1" title="Tick down 1 by hand"${n?" disabled":""}>\u2212</button>
1323        <span class="cd-step-lbl">By hand</span>
1324        <button type="button" class="cd-step" data-cd-step="1" title="Tick back up 1"${e.current_value>=e.start_value?" disabled":""}>+</button>
1325      </div>`:"",r=t&&(e.recent||[]).length?`<div class="cd-trail">${e.recent.map(d=>`<span class="cd-trail-row"><span class="cd-trail-lbl">${escHtml(d.label||"")}</span><span class="cd-trail-d${d.delta?"":" cd-trail-d--hold"}">${d.delta>0?"\u2212"+d.delta:d.delta<0?"+"+-d.delta:"held"}</span></span>`).join("")}</div>`:"",c=t?`<div class="cd-meta">
1326        <button type="button" class="cd-mbtn" data-cd-hide="${e.is_hidden?"0":"1"}" title="${e.is_hidden?"Hidden from players, show it":"Visible to players, hide it"}" aria-label="${e.is_hidden?"Show to players":"Hide from players"}">${e.is_hidden?_CD_ICO_EYE_OFF:_CD_ICO_EYE}</button>
1327        <button type="button" class="cd-mbtn" data-cd-reset="1" title="Reset to its starting value of ${e.start_value}" aria-label="Reset to starting value"${e.current_value>=e.start_value?" disabled":""}>${_CD_ICO_RESET}</button>
1328        <button type="button" class="cd-mbtn cd-mbtn--danger" data-cd-del="1" title="Delete this countdown" aria-label="Delete countdown">${_CD_ICO_TRASH}</button>
1329      </div>`:"",l=n&&t?`<div class="cd-zero"><span class="cd-zero-lbl">Zero. Play it out.</span>
1330        <button type="button" class="btn-new btn-xs cd-resolve" data-cd-resolve="1">Resolve</button></div>`:n?'<div class="cd-zero"><span class="cd-zero-lbl">Zero.</span></div>':"";return`<article class="cd-card cd-card--${e.kind}${n?" cd-card--zero":""}${e.is_hidden?" cd-card--hidden":""}" data-cd="${e.id}">
1331    <div class="cd-top">
1332      <span class="cd-die">${_cdDieSvg(e.die_size,e.current_value,n,e.start_value)}</span>
1333      <div class="cd-id">
1334        <span class="cd-name">${escHtml(e.name)}</span>
1335        <span class="cd-kind">${_CD_KIND_ICO[e.kind]||""}${_CD_KIND_LBL[e.kind]||e.kind}${e.is_hidden?" \xB7 hidden":""}</span>
1336      </div>
1337      ${c}
1338    </div>
1339    ${i}${o}${r}${l}
1340  </article>`}function _cdChallengeCardHtml(e,t){const n=e.fail_limit||3,a=Math.max(0,e.start_value-e.current_value),s=e.challenge_state||null,i=!!s,o=(p,m,f)=>{let g="";for(let h=0;h<m;h++)g+=`<span class="cd-ch-pip ${f}${h<p?" is-filled":""}"></span>`;return g},r=`<div class="cd-ch-tracks">
1341      <div class="cd-ch-track"><span class="cd-ch-track-lbl">Successes</span>
1342        <span class="cd-ch-pips" role="img" aria-label="${a} of ${e.start_value} successes">${o(a,e.start_value,"cd-ch-pip--succ")}</span>
1343        <span class="cd-ch-count">${a}/${e.start_value}</span></div>
1344      <div class="cd-ch-track"><span class="cd-ch-track-lbl">Failures</span>
1345        <span class="cd-ch-pips" role="img" aria-label="${e.fail_value} of ${n} failures">
1345${o(e.fail_value,n,"cd-ch-pip--fail")}</span>
1346        <span class="cd-ch-count">${e.fail_value}/${n}</span></div>
1347    </div>`,c=t&&!i?`<div class="cd-ch-taps" role="group" aria-label="Record a roll">
1348        <button type="button" class="cd-ch-tap cd-ch-tap--succ" data-ch-track="success" data-ch-step="1" title="They made the roll">Success</button>
1349        <button type="button" class="cd-ch-tap cd-ch-tap--fail" data-ch-track="failure" data-ch-step="1" title="They blew the roll">Failure</button>
1350      </div>
1351      <div class="cd-stepper" role="group" aria-label="Undo">
1352        <button type="button" class="cd-step" data-ch-track="success" data-ch-step="-1" title="Undo a success"${a<=0?" disabled":""}>\u2212S</button>
1353        <span class="cd-step-lbl">Undo</span>
1354        <button type="button" class="cd-step" data-ch-track="failure" data-ch-step="-1" title="Undo a failure"${e.fail_value<=0?" disabled":""}>\u2212F</button>
1355      </div>`:"",l=t&&(e.recent||[]).length?`<div class="cd-trail">${e.recent.map(p=>`<span class="cd-trail-row"><span class="cd-trail-lbl">${escHtml(p.label||"")}</span></span>`).join("")}</div>`:"",d=t?`<div class="cd-meta">
1356        <button type="button" class="cd-mbtn" data-cd-hide="${e.is_hidden?"0":"1"}" title="${e.is_hidden?"Hidden from players, show it":"Visible to players, hide it"}" aria-label="${e.is_hidden?"Show to players":"Hide from players"}">${e.is_hidden?_CD_ICO_EYE_OFF:_CD_ICO_EYE}</button>
1357        <button type="button" class="cd-mbtn" data-cd-reset="1" title="Reset both tracks to zero" aria-label="Reset the challenge"${a===0&&e.fail_value===0?" disabled":""}>${_CD_ICO_RESET}</button>
1358        <button type="button" class="cd-mbtn cd-mbtn--danger" data-cd-del="1" title="Delete this challenge" aria-label="Delete challenge">${_CD_ICO_TRASH}</button>
1359      </div>`:"",u=i?`<div class="cd-zero"><span class="cd-zero-lbl">${s==="won"?"They made it. Play it out.":"It goes wrong. Play it out."}</span>
1360        ${t?'<button type="button" class="btn-new btn-xs cd-resolve" data-cd-resolve="1">Resolve</button>':""}</div>`:"";return`<article class="cd-card cd-card--challenge${i?` cd-card--${s}`:""}${e.is_hidden?" cd-card--hidden":""}" data-cd="${e.id}">
1361    <div class="cd-top">
1362      <div class="cd-id">
1363        <span class="cd-name">${escHtml(e.name)}</span>
1364        <span class="cd-kind">Skill Challenge${e.is_hidden?" \xB7 hidden":""}</span>
1365      </div>
1366      ${d}
1367    </div>
1368    ${r}${c}${l}${u}
1369  </article>`}const _CD_ICO_EYE='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 8s2.6-4.5 7-4.5S15 8 15 8s-2.6 4.5-7 4.5S1 8 1 8Z"/><circle cx="8" cy="8" r="1.8"/></svg>',_CD_ICO_EYE_OFF='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 8s2.6-4.5 7-4.5c1.2 0 2.2.3 3.1.8M15 8s-2.6 4.5-7 4.5c-1.2 0-2.2-.3-3.1-.8"/><path d="M2 2l12 12"/></svg>',_CD_ICO_RESET='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.5 8a5.5 5.5 0 1 1-1.7-4"/><path d="M12.4 1.5v2.8H9.6"/></svg>',_CD_ICO_TRASH='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.6 9.5h6.8L12 4"/></svg>';function _cdPatch(e,t){return _cdQueue=_cdQueue.then(()=>_cdPatchNow(e,t)).catch(()=>{}),_cdQueue}async function _cdPatchNow(e,t){const n=currentCampaign&&currentCampaign.id;if(n!=null){if(t&&t.from_value!=null){const a=_cdState.list.find(s=>s.id===e);a&&(t=Object.assign({},t,{from_value:a.current_value}))}try{const a=await api("PATCH",`/api/campaigns/${n}/countdowns/${e}`,t);if(_campaignStale(n))return;const s=_cdState.list.findIndex(i=>i.id===e);s>=0&&(_cdState.list[s]=a),_cdRenderPanel()}catch(a){a&&a.status===409?(showToast("That countdown already moved."),_cdLoad()):showToast("Could not update the countdown.")}}}function _cdWireCards(e){_isGm()&&e.querySelectorAll(".cd-card").forEach(t=>{const n=parseInt(t.dataset.cd,10),a=()=>(_cdState.list.find(c=>c.id===n)||{}).current_value;t.querySelectorAll("[data-cd-out]").forEach(c=>c.addEventListener("click",l=>{l.stopPropagation(),c.classList.remove("cd-out--tapped"),c.offsetWidth,c.classList.add("cd-out--tapped"),_cdPatch(n,{outcome:c.dataset.cdOut,from_value:a()})})),t.querySelectorAll("[data-cd-step]").forEach(c=>c.addEventListener("click",l=>{l.stopPropagation(),_cdPatch(n,{step:parseInt(c.dataset.cdStep,10)})})),t.querySelectorAll("[data-ch-track]").forEach(c=>c.addEventListener("click",l=>{l.stopPropagation(),c.classList.remove("cd-out--tapped"),c.offsetWidth,c.classList.add("cd-out--tapped"),_cdPatch(n,{challenge:{track:c.dataset.chTrack,step:parseInt(c.dataset.chStep,10)}})}));const s=t.querySelector("[data-cd-hide]");s&&s.addEventListener("click",c=>{c.stopPropagation(),_cdPatch(n,{is_hidden:s.dataset.cdHide==="1"})});const i=t.querySelector("[data-cd-reset]");i&&i.addEventListener("click",c=>{c.stopPropagation(),_cdPatch(n,{reset:!0})});const o=t.querySelector("[data-cd-resolve]");o&&o.addEventListener("click",async c=>{c.stopPropagation();const l=currentCampaign&&currentCampaign.id;try{await api("POST",`/api/campaigns/${l}/countdowns/${n}/resolve`),_campaignStale(l)||_cdLoad()}catch{showToast("Could not resolve the countdown.")}});const r=t.querySelector("[data-cd-del]");r&&r.addEventListener("click",async c=>{c.stopPropagation();const l=(_cdState.list.find(p=>p.id===n)||{}).name||"this countdown";if(!await confirmDialog({title:"Delete countdown",message:`Delete "${l}"? This does not resolve it, it removes it entirely.`,confirmLabel:"Delete"}))return;const u=currentCampaign&&currentCampaign.id;try{await api("DELETE",`/api/campaigns/${u}/countdowns/${n}`),_campaignStale(u)||_cdLoad()}catch{showToast("Could not delete the countdown.")}})})}const _CD_KIND_HELP={consequence:"Something is closing in. Failure moves it, success mostly holds it off. Tick it by hand any time the fiction says so.",progress:"The party is getting somewhere. Success moves it, failure stalls. Tick it by hand any time the fiction says so."};function _cdSetKind(e){const t=document.getElementById("cd-new-kind");if(!t)return;t.dataset.kind=e,t.querySelectorAll(".cd-kind-btn").forEach(a=>{const s=a.dataset.kind===e;a.classList.toggle("is-active",s),a.setAttribute("aria-checked",s?"true":"false")});const n=document.getElementById("cd-new-kind-help");
1369n&&(n.textContent=_CD_KIND_HELP[e]||"")}function _cdDiePreview(){const e=document.getElementById("cd-new-die-preview"),t=document.getElementById("cd-new-start");if(!e||!t)return;const n=parseInt(t.value,10);if(!isFinite(n)||n<1||n>99){e.innerHTML="";return}const a=[4,6,8,10,12,20].find(s=>n<=s);e.innerHTML=a?`${_cdDieSvg(a,n,!1)}<span class="cd-new-die-lbl">d${a} at ${n}</span>`:'<span class="cd-new-die-lbl">Too long for a die, shows as a number</span>'}function _cdSetType(e){const t=document.getElementById("cd-new-type");if(!t)return;t.dataset.type=e,t.querySelectorAll(".cd-kind-btn").forEach(c=>{const l=c.dataset.type===e;c.classList.toggle("is-active",l),c.setAttribute("aria-checked",l?"true":"false")});const n=e==="challenge";document.getElementById("cd-new-challenge-fields")?.classList.toggle("hidden",!n),document.getElementById("cd-new-countdown-fields")?.classList.toggle("hidden",n);const a=document.getElementById("cd-new-name-lbl"),s=document.getElementById("cd-new-name"),i=document.getElementById("cd-new-name-help"),o=document.getElementById("cd-new-save"),r=document.getElementById("cd-new-modal-title");n?(r&&(r.textContent="Start a Skill Challenge"),a&&(a.textContent="What are they attempting"),s&&(s.placeholder="Cross the storm-wracked pass"),i&&(i.textContent="Name it for the thing the party is trying to pull off."),o&&(o.textContent="\uFF0B Start Challenge")):(r&&(r.textContent="Start a Countdown"),a&&(a.textContent="What is closing in"),s&&(s.placeholder="The ritual completes"),i&&(i.textContent="Name it for the thing that happens at zero."),o&&(o.textContent="\uFF0B Start Countdown"))}function _cdOpenNew(){const e=document.getElementById("cd-new-modal");if(!e)return;const t=_tfIsDaggerheart();document.getElementById("cd-new-name").value="",_cdSetKind("consequence"),document.getElementById("cd-new-start").value="6",document.getElementById("cd-new-succ").value="4",document.getElementById("cd-new-fails").value="3",document.getElementById("cd-new-hidden").checked=!1,document.getElementById("cd-new-type-lbl")?.classList.toggle("hidden",t),document.getElementById("cd-new-type")?.classList.toggle("hidden",t),_cdSetType(t?"countdown":"challenge"),_cdDiePreview(),e.classList.remove("hidden"),document.getElementById("cd-new-name").focus()}async function _cdCreate(){const e=currentCampaign&&currentCampaign.id;if(e==null)return;const t=document.getElementById("cd-new-type")?.dataset.type==="challenge"&&!_tfIsDaggerheart(),n=(document.getElementById("cd-new-name").value||"").trim();if(!n){showToast(t?"Give the challenge a name.":"Give the countdown a name.");return}const a=t?{name:n,kind:"challenge",start_value:parseInt(document.getElementById("cd-new-succ").value,10),fail_limit:parseInt(document.getElementById("cd-new-fails").value,10),is_hidden:document.getElementById("cd-new-hidden").checked}:{name:n,kind:document.getElementById("cd-new-kind").dataset.kind||"consequence",start_value:parseInt(document.getElementById("cd-new-start").value,10),is_hidden:document.getElementById("cd-new-hidden").checked};try{await api("POST",`/api/campaigns/${e}/countdowns`,a),document.getElementById("cd-new-modal").classList.add("hidden"),_campaignStale(e)||_cdLoad()}catch(s){const i=s&&s.detail;i&&i.code==="countdown_active"?showToast(i.message):showToast(s&&s.message||"Could not start the countdown.")}}const _CD_RULE_ROWS=[["critical","Critical Success"],["success_hope","Success with Hope"],["success_fear","Success with Fear"],["failure_hope","Failure with Hope"],["failure_fear","Failure with Fear"]];function _cdRulesGrid(e){const t=document.getElementById("cd-rules-grid");if(!t)return;const n='<span class="cd-rules-hd"></span><span class="cd-rules-hd">Progress</span><span class="cd-rules-hd">Consequence</span>',a=_CD_RULE_ROWS.map(([s,i])=>{const o=(e.progress||{})[s]||0,r=(e.consequence||{})[s]||0;return`<span class="cd-rules-row">${i}</span><input type="number" class="cd-rules-in" min="0" max="12" data-cd-rule="progress" data-cd-out="${s}" value="${o}" aria-label="${i}, progress countdown" /><input type="number" class="cd-rules-in" min="0" max="12" data-cd-rule="consequence" data-cd-out="${s}" value="${r}" aria-label="${i}, consequence countdown" />`}).join("");t.innerHTML=n+a}function _cdOpenRules(){const e=document.getElementById("cd-rules-modal");e&&(_cdRulesGrid(_cdState.rules||_cdState.book||{}),e.classList.remove("hidden"))}function _cdReadRulesGrid(){const e={progress:{},consequence:{}};return document.querySelectorAll("#cd-rules-grid .cd-rules-in").forEach(t=>{const n=parseInt(t.value,10);e[t.dataset.cdRule][t.dataset.cdOut]=isFinite(n)?Math.max(0,Math.min(12,n)):0}),e}async function _cdSaveRules(e){const t=currentCampaign&&currentCampaign.id;if(t!=null)try{await api("PUT",`/api/campaigns/${t}/countdown-rules`,{rules:e}),document.getElementById("cd-rules-modal").classList.add("hidden"),_campaignStale(t)||_cdLoad()}catch{showToast("Could not save the advancement settings.")}}function _cdWirePanel(){if(document.getElementById("cd-add-once"))return;const t=document.createElement("span");t.id="cd-add-once",t.hidden=!0,document.body.appendChild(t);const n=(i,o,r)=>{const c=document.getElementById(i);c&&c.addEventListener(r||"click",o)};n("live-cd-add-btn",_cdOpenNew),n("live-cd-rules-btn",_cdOpenRules),n("live-cd-collapse-btn",()=>{const i=document.getElementById("live-cd-panel");i&&i.classList.toggle("collapsed")}),n("cd-new-close",()=>document.getElementById("cd-new-modal").classList.add("hidden")),n("cd-new-cancel",()=>document.getElementById("cd-new-modal").classList.add("hidden")),n("cd-new-save",_cdCreate),n("cd-new-start",_cdDiePreview,"input");const a=document.getElementById("cd-new-kind");a&&a.addEventListener("click",i=>{const o=i.target.closest(".cd-kind-btn");o&&_cdSetKind(o.dataset.kind)});const s=document.getElementById("cd-new-type");s&&s.addEventListener("click",i=>{const o=i.target.closest(".cd-kind-btn");o&&_cdSetType(o.dataset.type)}),n("cd-rules-close",()=>document.getElementById("cd-rules-modal").classList.add("hidden")),n("cd-rules-cancel",()=>document.getElementById("cd-rules-modal").classList.add("hidden")),n("cd-rules-save",()=>_cdSaveRules(_cdReadRulesGrid())),n("cd-rules-reset",()=>_cdRulesGrid(_cdState.book||{}))}function _liveRenderTraitSpread(e){if(!e)return;const t=_activePcs().map(i=>({e:i,s:_dhSheet(i)})).filter(i=>i.s&&i.s.traits);if(!t.length){e.innerHTML="";return}const n=i=>(i>=0?"+":"")+i,a=i=>String(i||"").trim().split(/\s+/)[0]||"",s=_DH_TRAITS.map(i=>{const o=t.map(l=>({name:l.e.name,v:Number((l.s.traits||{})[i])})).filter(l=>isFinite(l.v)).sort((l,d)=>d.v-l.v);if(!o.length)return"";
1369const r=o[0],c=o.map(l=>`${l.name} ${n(l.v)}`).join(" \xB7 ");return`<span class="lv-trait-cell" title="${escHtml(c)}"><span class="lv-trait-lbl pc-stats-field-lbl">${_DH_TRAIT_LBL[i]}</span><span class="lv-trait-best">${n(r.v)}</span><span class="lv-trait-who">${escHtml(a(r.name))}</span></span>`}).join("");e.innerHTML=`<div class="lv-pass">
1370    <div class="lv-pass-hdr"><span class="lv-pass-eyebrow form-section-label">Trait spread \xB7 best in party</span></div>
1371    <div class="lv-trait-grid">${s}</div>
1372    ${_dhDifficultyScaleHtml({gradient:!0})}
1373  </div>`}const _DH_DIFFICULTY=[[5,"Very Easy"],[10,"Easy"],[15,"Average"],[20,"Hard"],[25,"Very Hard"],[30,"Nearly Impossible"]];function _dhDifficultyScaleHtml(e){const t=!!(e&&e.gradient),n=_DH_DIFFICULTY.map(([s,i],o)=>{const r=o%2===0;return`<div class="dh-diff-col"><span class="dh-diff-lbl">${r?escHtml(i):""}</span><span class="dh-diff-node">${s}</span><span class="dh-diff-lbl">${r?"":escHtml(i)}</span></div>`}).join(""),a=_DH_DIFFICULTY.map(([s,i])=>`${s} ${i}`).join(", ");return`<div class="dh-diff-scale${t?" dh-diff-scale--grad":""}" role="img" aria-label="Difficulty ladder: ${a}"><span class="dh-diff-eyebrow form-section-label">Difficulty</span><div class="dh-diff-track">${t?'<span class="dh-diff-grad" aria-hidden="true"></span>':""}${n}</div></div>`}let _liveThreshDmg="";function _dhTier(e,t,n){return isFinite(e)?e>=n?{txt:"Severe \xB7 3 HP",cls:"lv-thr--severe"}:e>=t?{txt:"Major \xB7 2 HP",cls:"lv-thr--major"}:{txt:"Minor \xB7 1 HP",cls:"lv-thr--minor"}:null}function _liveRecolorThresholds(e){if(!e)return;const t=parseInt(_liveThreshDmg,10);e.querySelectorAll(".lv-thr-pill").forEach(n=>{const a=Number(n.dataset.major),s=Number(n.dataset.severe),i=_dhTier(t,a,s);n.classList.remove("lv-thr--minor","lv-thr--major","lv-thr--severe"),i&&n.classList.add(i.cls);const o=n.querySelector(".lv-thr-tier");o&&(o.textContent=i?i.txt:"")})}function _dhSeedDmg(e){const t=e.map(n=>Number(n.s.thresholds.major)).sort((n,a)=>n-a);return t.length?String(t[Math.floor((t.length-1)/2)]):""}function _liveRenderThresholdBoard(e,t){if(!e)return;const n=!!(t&&t.bare),s=_activePcs().map(d=>({e:d,s:_dhSheet(d)})).filter(d=>d.s&&d.s.thresholds&&isFinite(Number(d.s.thresholds.major))&&isFinite(Number(d.s.thresholds.severe)));if(!s.length){e.innerHTML='<div class="lv-savedc lv-savedc--empty"><div class="lv-savedc-hdr form-section-label">Damage thresholds</div><div class="lv-savedc-note">Add character stats (\u270F\uFE0F Edit on the Party tab) to compute these.</div></div>';return}const i=d=>String(d||"").trim().split(/\s+/)[0]||"",o=s.map(d=>{const u=Number(d.s.thresholds.major),p=Number(d.s.thresholds.severe);return`<span class="lv-thr-pill" data-major="${u}" data-severe="${p}" title="${escHtml(d.e.name)} \xB7 Major ${u} \xB7 Severe ${p}"><span class="lv-pass-pn">${escHtml(i(d.e.name))}</span><span class="lv-thr-nums">${u}/${p}</span><span class="lv-thr-tier"></span></span>`}).join("");n&&t.seedDmg&&!_liveThreshDmg&&(_liveThreshDmg=_dhSeedDmg(s));const r=`<span class="lv-pass-dcwrap"><label class="lv-pass-dclbl pc-stats-field-lbl">DMG</label><input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" class="lv-thr-dmg-input" value="${escHtml(_liveThreshDmg)}" placeholder="\u2014" aria-label="Damage total"><span class="dh-thr-step"><button type="button" class="dh-thr-up" aria-label="Increase damage">&#9650;</button><button type="button" class="dh-thr-dn" aria-label="Decrease damage">&#9660;</button></span></span>`;e.innerHTML=n?`<div class="dh-thr-bare">
1374        <div class="dh-diff-eyebrow form-section-label">Damage thresholds</div>
1375        <div class="dh-thr-dmgrow">${r}</div>
1376        <div class="lv-thr-grid">${o}</div>
1377      </div>`:`<div class="lv-savedc">
1378    <div class="lv-savedc-hdr form-section-label">Damage thresholds
1379      ${r}
1380    </div>
1381    <div class="lv-thr-grid">${o}</div>
1382  </div>`;const c=d=>{const u=e.querySelector(".lv-thr-dmg-input");if(!u)return;const p=Math.max(0,(parseInt(u.value,10)||0)+d);u.value=String(p),_liveThreshDmg=u.value,_liveRecolorThresholds(e)};e.querySelector(".dh-thr-up")?.addEventListener("click",()=>c(1)),e.querySelector(".dh-thr-dn")?.addEventListener("click",()=>c(-1));const l=e.querySelector(".lv-thr-dmg-input");l?.addEventListener("input",()=>{const d=l.selectionStart,u=l.value.replace(/[^0-9]/g,"");if(u!==l.value){l.value=u;try{l.setSelectionRange(Math.max(0,d-1),Math.max(0,d-1))}catch{}}_liveThreshDmg=u,_liveRecolorThresholds(e)}),_liveRecolorThresholds(e)}function _renderLivePcVitalsStrip(e,t){const n=c=>(c>=0?"+":"")+c,a=_activePcs();if(t.textContent="Party Vitals",a.length===0){e.innerHTML='<p class="placeholder placeholder--padded">No active party members.</p>';return}const s=_liveAwayIds(),i=_isGm(),o='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5v8Q7 15 9 15h8"/><path d="M17 15v4"/><path d="M9 15v3"/></svg>
1382',r=a.map((c,l)=>{const d=livePcSidebarPcs.findIndex(y=>y.entity_id===c.id),u=s.has(c.id),p=i?`<button type="button" class="lv-vitals-away-btn${u?" on":""}" data-away-id="${c.id}" aria-pressed="${u}" title="${u?`${escHtml(c.name)} is marked away tonight, tap to mark present`:`Mark ${escHtml(c.name)} not at the table tonight`}" aria-label="${u?`Mark ${escHtml(c.name)} present tonight`:`Mark ${escHtml(c.name)} not at the table tonight`}">${o}</button>`:"";if(_tfIsDaggerheart()){const y=_dhSheet(c),_=y&&y.evasion!=null?String(y.evasion):"\u2014",w=y&&y.thresholds&&y.thresholds.major!=null&&y.thresholds.severe!=null?`${y.thresholds.major}/${y.thresholds.severe}`:"\u2014",$=y&&y.hp_max!=null?String(y.hp_max):"\u2014",E=y&&y.stress_max!=null?String(y.stress_max):"\u2014";return`<div class="lv-vitals-row${u?" lv-vitals-row--away":""}" data-slot-idx="${d}" title="Open ${escHtml(c.name)}'s sheet">
1383        <span class="lv-vitals-name">${escHtml(c.name)}</span>
1384        ${u?'<span class="lv-vitals-away-tag">Away</span>':""}
1385        <span class="lv-vitals-stats">
1386          <span class="lv-vitals-stat"><span class="lv-vitals-label">EVA</span>${escHtml(_)}</span>
1387          <span class="lv-vitals-stat" title="Major/Severe damage thresholds"><span class="lv-vitals-label">THR</span>${escHtml(w)}</span>
1388          <span class="lv-vitals-stat"><span class="lv-vitals-label">HP</span>${escHtml($)}</span>
1389          <span class="lv-vitals-stat"><span class="lv-vitals-label">STRESS</span>${escHtml(E)}</span>
1390        </span>
1391        ${p}
1392      </div>`}let m="\u2014",f="\u2014",g="\u2014",h="\u2014",v="\u2014",b="";if(c.dnd_beyond_data)try{const y=JSON.parse(c.dnd_beyond_data),_=y.ability_scores||{};if(y.ai_generated!==!0){y.hp!=null&&(m=String(y.hp)),y.ac!=null&&(f=String(y.ac));const w=y.initiative??(_.DEX?_.DEX.mod:null);w!=null&&(g=n(w));const $=y.skills||{},E=y.passive_perception??10+($.Perception??(_.WIS?_.WIS.mod:0));E!=null&&(h=String(E)),y.speed!=null&&(v=String(y.speed));const k=y.extra_speeds||{},C=["fly","swim","climb","burrow"].filter(S=>k[S]).map(S=>S.charAt(0).toUpperCase()+S.slice(1)+" "+k[S]);C.length&&(b=` title="Walking ${v} ft \xB7 ${C.join(" \xB7 ")} ft"`)}}catch{}return`<div class="lv-vitals-row${u?" lv-vitals-row--away":""}" data-slot-idx="${d}" title="Open ${escHtml(c.name)}'s sheet">
1393      <span class="lv-vitals-name">${escHtml(c.name)}</span>
1394      ${u?'<span class="lv-vitals-away-tag">Away</span>':""}
1395      <span class="lv-vitals-stats">
1396        <span class="lv-vitals-stat"><span class="lv-vitals-label">HP</span>${escHtml(m)}</span>
1397        <span class="lv-vitals-stat"><span class="lv-vitals-label">AC</span>${escHtml(f)}</span>
1398        <span class="lv-vitals-stat"><span class="lv-vitals-label">INIT</span>${escHtml(g)}</span>
1399        <span class="lv-vitals-stat"${b}><span class="lv-vitals-label">SPD</span>${escHtml(v)}${b?" \u2726":""}</span>
1400      </span>
1401      ${p}
1402    </div>`}).join("");if(_tfIsDaggerheart()){e.innerHTML=`<div class="lv-vitals-strip">${r}</div><div id="lv-trait-mount"></div><div id="lv-thr-footer"></div>`,_liveRenderFearBar(),_liveRenderTraitSpread(document.getElementById("lv-trait-mount")),_liveRenderThresholdBoard(document.getElementById("lv-thr-footer"));const c=document.getElementById("lv-thr-footer");if(c&&!c.querySelector(".dhp-stress"))try{c.insertAdjacentHTML("beforeend",_dhStressHtml())}catch{}}else e.innerHTML=`<div class="lv-vitals-strip">${r}</div><div id="lv-pass-mount"></div><div id="lv-savedc-footer"></div>`,_liveRenderPassives(document.getElementById("lv-pass-mount")),_renderLiveSaveDcInto(document.getElementById("lv-savedc-footer"),currentCampaign&&currentCampaign.id);e.querySelectorAll(".lv-vitals-away-btn").forEach(c=>{c.addEventListener("click",l=>{l.stopPropagation(),_liveAwayToggle(parseInt(c.dataset.awayId,10)),renderLivePcSidebar()})}),e.querySelectorAll(".lv-vitals-row").forEach(c=>{c.addEventListener("click",()=>{const l=parseInt(c.dataset.slotIdx);livePcSidebarIdx=l>=0&&l<livePcSidebarPcs.length?l:0,livePcVitalsView=!1,renderLivePcSidebar()})})}function renderLivePcSidebar(){const e=document.getElementById("live-pc-sidebar"),t=document.getElementById("live-pc-sidebar-expander"),n=document.getElementById("live-pc-sidebar-body"),a=document.getElementById("live-pc-sidebar-hdr-title");if(!e||!n)return;if(_renderLiveBursar(),livePcSidebarPcs.length===0){e.classList.add("hidden"),t?.classList.add("hidden");return}const s=livePcSidebarPcs[livePcSidebarIdx],i=!!s.isTreasury,o=i?null:allEntities.find(h=>h.id===s.entity_id);if(!i&&o?.is_inactive&&!_isGm()){e.classList.add("hidden"),t?.classList.add("hidden");return}t&&!t.classList.contains("hidden")||e.classList.remove("hidden");const c=_activePcs().length,l=document.getElementById("live-pc-prev-btn"),d=document.getElementById("live-pc-next-btn");if(livePcVitalsView&&c>=2){l?.classList.add("hidden"),d?.classList.add("hidden"),t&&(document.getElementById("live-pc-sidebar-expander-name").textContent="Party"),_renderLivePcVitalsStrip(n,a);return}const u=!livePcVitalsView&&c>=2,p=i?"Party Treasury":o?.name||s.name||"Character";t&&(document.getElementById("live-pc-sidebar-expander-name").textContent=i?`\u{1F4B0} ${p}`:`\u2694 ${p}`);const m=livePcSidebarPcs.length;if(l&&d&&(m>1?(l.classList.remove("hidden"),d.classList.remove("hidden"),l.disabled=!1,d.disabled=!1):(l.classList.add("hidden"),d.classList.add("hidden"))),i)a.textContent="Party Treasury";else{const h=livePcSidebarPcs.filter(b=>
1402!b.isTreasury),v=livePcSidebarPcs.slice(0,livePcSidebarIdx+1).filter(b=>!b.isTreasury).length;a.textContent=h.length>1?`Character ${v} of ${h.length}`:"Character"}const f=i?renderPartyInventoryCard()||'<p class="placeholder" style="padding:0.75rem 0">Treasury not loaded.</p>':o?renderPartyCard(o):'<p class="placeholder" style="padding:0.75rem 0">No character data yet.</p>',g=u?'<button class="lv-vitals-back-btn btn-ghost btn-xs" title="Back to party vitals">\u25C2 Party</button>':"";n.innerHTML=g+f,n.querySelector(".lv-vitals-back-btn")?.addEventListener("click",()=>{livePcVitalsView=!0,renderLivePcSidebar()}),l&&(l.onclick=()=>{livePcSidebarIdx=(livePcSidebarIdx-1+m)%m,renderLivePcSidebar()}),d&&(d.onclick=()=>{livePcSidebarIdx=(livePcSidebarIdx+1)%m,renderLivePcSidebar()}),n.querySelector("#open-treasury-edit-btn")?.addEventListener("click",_openTreasuryModal),n.querySelector("#open-loot-splitter-btn")?.addEventListener("click",_openLootSplitter),n.querySelector("#open-treasury-log-btn")?.addEventListener("click",_openTreasuryLogModal),n.querySelectorAll(".party-pc-name-btn").forEach(h=>{h.addEventListener("click",()=>navigateToEntity(parseInt(h.dataset.id)))}),n.querySelectorAll(".ddb-refresh-btn").forEach(h=>{h.addEventListener("click",v=>{v.stopPropagation(),showToast("DDB direct pull was retired. Use \u{1F4C4} Import (Vision) or the Party Sync extension.")})}),n.querySelectorAll(".toggle-dead-btn").forEach(h=>{h.addEventListener("click",async v=>{v.stopPropagation();const b=parseInt(h.dataset.id),y=h.dataset.dead==="true";try{const _=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${b}`,{is_dead:!y});allEntities=allEntities.map(w=>w.id===_.id?_:w),renderLivePcSidebar()}catch(_){showToast("Failed: "+_.message)}})}),n.querySelectorAll(".toggle-inactive-btn, .rejoin-btn").forEach(h=>{h.addEventListener("click",async v=>{v.stopPropagation();const b=parseInt(h.dataset.id),y=h.classList.contains("rejoin-btn");try{const _=await api("PUT",`/api/campaigns/${currentCampaign.id}/entities/${b}`,y?{is_inactive:!1}:{is_inactive:!0,is_dead:!1});allEntities=allEntities.map(w=>w.id===_.id?_:w),renderLivePcSidebar()}catch(_){showToast("Failed: "+_.message)}})}),n.querySelectorAll(".companion-chip-btn").forEach(h=>{h.addEventListener("click",v=>{v.stopPropagation(),navigateToEntity(parseInt(h.dataset.id))})}),n.querySelectorAll(".add-companion-btn").forEach(h=>{h.addEventListener("click",v=>{v.stopPropagation(),openEntityModal(null,parseInt(h.dataset.pcId))})})}document.getElementById("live-pc-sidebar-collapse-btn").addEventListener("click",()=>{document.getElementById("live-pc-sidebar").classList.add("hidden"),document.getElementById("live-pc-sidebar-expander").classList.remove("hidden")}),document.getElementById("live-pc-sidebar-expander").addEventListener("click",()=>{document.getElementById("live-pc-sidebar-expander").classList.add("hidden"),document.getElementById("live-pc-sidebar").classList.remove("hidden")});function _openLiveAiDrawer(){if(_isGm()){document.getElementById("live-ai-expander").classList.add("hidden"),document.getElementById("live-ai-drawer").classList.remove("hidden");try{const e=document.getElementById("ln-standing");e&&!e.classList.contains("ln-collapsed")&&_lnStandingLoad()}catch{}}}function _closeLiveAiDrawer(){_isGm()&&(document.getElementById("live-ai-drawer").classList.add("hidden"),document.getElementById("live-ai-expander").classList.remove("hidden"))}document.getElementById("live-ai-drawer-collapse-btn").addEventListener("click",_closeLiveAiDrawer),document.getElementById("live-ai-expander").addEventListener("click",_openLiveAiDrawer);const _NOTEBOOK_SECTIONS=["context","standing","rules","threads","fates"],_NOTEBOOK_DEFAULT_OPEN=new Set(["context"]);function _notebookCollapseKey(e){return`tf_notebook_collapsed_${e}`}function _applyNotebookCollapseState(){for(const e of _NOTEBOOK_SECTIONS){const t=document.getElementById(`ln-${e}`);if(!t)continue;const n=localStorage.getItem(_notebookCollapseKey(e));let a;e==="rules"||n==="1"?a=!0:n==="0"?a=!1:a=!_NOTEBOOK_DEFAULT_OPEN.has(e),t.classList.toggle("ln-collapsed",a)}}function _setNotebookCollapsed(e,t){const n=document.getElementById(`ln-${e}`);if(n){n.classList.toggle("ln-collapsed",t);try{localStorage.setItem(_notebookCollapseKey(e),t?"1":"0")}catch{}}}document.querySelectorAll(".live-notebook-section-hdr").forEach(e
1402=>{e.addEventListener("click",t=>{if(t.target.closest(".live-preamble-close"))return;const n=e.closest(".live-notebook-section");if(!n)return;const a=n.id.replace(/^ln-/,"");_setNotebookCollapsed(a,!n.classList.contains("ln-collapsed")),a==="standing"&&!n.classList.contains("ln-collapsed")&&_lnStandingLoad()})}),document.addEventListener("DOMContentLoaded",_applyNotebookCollapseState);let _lnStandingCid=null;async function _lnStandingLoad(e){if(typeof _isGm=="function"&&!_isGm())return;const t=document.getElementById("ln-standing-body");if(!t)return;const n=currentCampaign?.id;if(n==null||_lnStandingCid===n&&!e)return;let a;try{a=await api("GET",`/api/campaigns/${n}/renown`)}catch{return}if(_campaignStale(n))return;_lnStandingCid=n;const s=a.factions||[],{all:i}=_p2RnRecencySorted(s);_setNotebookCount("standing",s.length);const o=i.slice(0,6),r=c=>{const l=["ln-standing-bubble"];c.killed?l.push("ln-standing-bubble--killed"):c.is_dead&&l.push("ln-standing-bubble--dead");const d=c.name+(c.killed?" (killed by the party)":c.is_dead?" (dead)":"");return c.image_url?`<span class="${l.join(" ")}" style="background-image:url('${escHtml(c.image_url)}')" title="${escHtml(d)}"></span>`:`<span class="${l.join(" ")} ln-standing-bubble--init" title="${escHtml(d)}">${escHtml(window.FactionMap?FactionMap.initialOf(c.name):(c.name||"?").charAt(0).toUpperCase())}</span>`};t.innerHTML=o.length?o.map(c=>{const l=c.members||[],d=l.slice(0,3),u=l.slice(3);return`<div class="ln-standing-row" data-fid="${c.entity_id}">
1403      <span class="ln-standing-name" title="${escHtml(c.name)}">${escHtml(c.name)}</span>
1404      ${_p2RnScaleHtml(c,!0)}
1405      <span class="ln-standing-bubbles">${d.map(r).join("")}${u.length?`<button type="button" class="ln-standing-more" aria-label="Show all ${l.length} members">\u2026</button>
1406           <span class="ln-standing-pop hidden">${l.map(r).join("")}</span>`:""}</span>
1407    </div>`}).join("")+(i.length>6?`<p class="form-helper-text ln-standing-rest">+ ${i.length-6} more \xB7 Party tab</p>`:""):'<p class="form-helper-text">No factions tracked yet.</p>',t.querySelectorAll(".p2-rn-tick[data-set]").forEach(c=>c.addEventListener("click",async()=>{if(!c.classList.contains("p2-rn-tick--current")){if(c.dataset.ghost){showToast("Proposed shift \u2014 review it on the Party tab.",4e3);return}try{const l=await api("POST",`/api/campaigns/${n}
1407/renown/${c.dataset.fid}/standing`,{standing:parseInt(c.dataset.set,10)});if(_campaignStale(n))return;_cx2RnCid=null,showToast("Standing set to "+(l&&l.label||""),4e3),_lnStandingLoad(!0)}catch(l){showToast("Failed: "+(l.message||"try again"))}}})),t.querySelectorAll(".ln-standing-more").forEach(c=>{const l=c.nextElementSibling;if(!l)return;const d=u=>l.classList.toggle("hidden",!u);c.addEventListener("mouseenter",()=>d(!0)),c.parentElement.addEventListener("mouseleave",()=>d(!1)),c.addEventListener("click",()=>d(l.classList.contains("hidden")))})}function _setNotebookCount(e,t){const n=document.getElementById(`ln-${e}-count`);n&&(n.textContent=String(t),n.dataset.count=String(t))}function _renderNotebookPlaceholders(){const e=i=>`<p class="live-notebook-empty">${escHtml(i)}</p>`,t=document.getElementById("ln-context-body"),n=document.getElementById("ln-threads-body"),a=document.getElementById("ln-foretellings-body"),s=document.getElementById("ln-fates-body");t&&!t.innerHTML&&(t.innerHTML=e("Entities mentioned twice or more this session will surface here.")),n&&!n.innerHTML&&(n.innerHTML=e("Active Loose Threads from the campaign will surface here.")),a&&!a.innerHTML&&(a.innerHTML=e("Pending foreshadows appear under each Fate.")),s&&!s.innerHTML&&(s.innerHTML=e("Saved Fates with unplanted foreshadows will surface here."))}document.addEventListener("DOMContentLoaded",_renderNotebookPlaceholders);function _syncLiveLeftMinimized(){const e=document.getElementById("live-map-panel"),t=document.getElementById("live-quest-panel"),n=document.querySelector(".live-body"),a=document.getElementById("live-left-rail");if(!n)return;const s=!!(e&&e.classList.contains("collapsed")&&t&&t.classList.contains("collapsed"));n.classList.toggle("live-left-min",s),a&&a.classList.toggle("hidden",!s);const i=document.getElementById("live-left-rail-label");if(i&&s){let o=0;try{o=(allQuests||[]).filter(r=>r.status==="active"&&!r.is_background).length}catch{}i.textContent=o?`\u{1F5FA} Map \xB7 \u2694 ${o} Quests`:"\u{1F5FA} Map \xB7 \u2694 Quests"}}document.getElementById("live-quest-collapse-btn").addEventListener("click",()=>{document.getElementById("live-quest-panel").classList.toggle("collapsed"),_syncLiveLeftMinimized()}),document.getElementById("live-map-collapse-btn").addEventListener("click",()=>{document.getElementById("live-map-panel").classList.toggle("collapsed"),_syncLiveLeftMinimized(),!document.getElementById("live-map-panel").classList.contains("collapsed")&&liveMap&&setTimeout(()=>liveMap.invalidateSize(),50)}),function(){const t=document.getElementById("live-left-rail");if(!t)return;const n=()=>{document.getElementById("live-map-panel").classList.remove("collapsed"),document.getElementById("live-quest-panel").classList.remove("collapsed"),_syncLiveLeftMinimized(),liveMap&&setTimeout(()=>liveMap.invalidateSize(),50)};t.addEventListener("click",n),t.addEventListener("keydown",a=>{(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),n())})}();async function openLiveSession(e){if(e?.id&&currentCampaign?.id)try{const o=await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${e.id}`);e=o,allSessions=allSessions.map(r=>r.id===o.id?o:r)}catch{}liveSession=e??null,_refreshTestAllowance(),_recConsentAcknowledged=!1,liveLogEntries=[],liveMyReactions={},liveElapsedSeconds=0,liveSettingPos=!1,_liveMinimized=!1,_priorTranscriptBaseline="",liveTranscriptAccum="",liveTranscriptSegments=[],liveCurrentSegText="",liveCurrentSegHas=!1,liveSpeakerIdx=0,_liveSpeakerIdxBase=0,liveSpeakerNameMap={},liveSpeakerQuotes={},_liveInterimLast="",_liveSpkBarLastTokens="",liveAllMaps=[],liveCurrentMapIdx=0,_voicesReset(),document.getElementById("live-record-bar").classList.add("hidden"),document.getElementById("main-app").classList.add("hidden"),document.getElementById("live-session-screen").classList.remove("hidden"),_tfHeldRecording&&_tfShowHeldRecordingBanner(),_restoreRecModePreference(),_resumeDiscordRecIfLive(),_cdWirePanel(),_cdLoad(),liveSession?.id&&currentCampaign?.id&&_checkTranscriptBackupRecovery(currentCampaign.id,liveSession.id),updateViewAsBar();const t=document.getElementById("live-session-select"),n=document.getElementById("live-create-first-session-btn"),a=allSessions.filter(o=>!o.is_session_zero);if(a.length===0&&_isGm())t.classList.add("hidden"),n.classList.remove("hidden"),n.textContent="\uFF0B Create Session 1";else{if(t.classList.remove("hidden"),n.classList.add("hidden"),t.innerHTML="",_isGm()){const o=a.length+1,r=document.createElement("option");r.value="__new__",r.textContent=`\uFF0B Create Session ${o}`,e||(r.selected=!0),t.appendChild(r)}
1407a.forEach(o=>{const r=document.createElement("option");r.value=o.id,r.textContent=`Session ${getSessionNum(o)}: ${o.title}`,e&&o.id===e.id&&(r.selected=!0),t.appendChild(r)})}if(document.getElementById("rec-clock").textContent="00:00:00",_setRecBtnLabel(),document.getElementById("live-entry-text").value="",document.getElementById("live-map-hint").classList.add("hidden"),document.getElementById("live-transcript-box").textContent="",document.getElementById("live-transcript-wordcount").textContent="0 words",document.getElementById("live-transcript-panel").classList.add("hidden"),clearInterval(liveTimerInterval),liveTimerInterval=null,e)try{liveLogEntries=await api("GET",`/api/campaigns/${currentCampaign.id}/sessions/${e.id}/log-entries`)}catch{liveLogEntries=[]}_updateWallAnchor(liveLogEntries),renderLiveLog();const i=!!(e&&(e.transcript_chars>0||typeof e.raw_transcript=="string"&&e.raw_transcript.length))?await _tfSessionTranscript(e.id,{quiet:!0}):"";if(i){liveTranscriptAccum=i;const o=document.getElementById("live-transcript-box"),r=document.getElementById("live-transcript-wordcount"),c=document.getElementById("live-transcript-panel");if(o&&(o.innerHTML=_buildTranscriptHTML(i)),r){const l=i.trim().split(/\s+/).filter(Boolean).length;r.textContent=`${l} word${l!==1?"s":""}`}c&&c.classList.remove("hidden"),o&&requestAnimationFrame(()=>{o.scrollTop=o.scrollHeight})}if(typeof _setRecBtnLabel=="function")try{_setRecBtnLabel()}catch{}if(await initLiveMap(),!allQuests.length)try{allQuests=await api("GET",`/api/campaigns/${currentCampaign.id}/quests`)}catch{}renderLiveQuestPanel(),_startLiveLogPoll(),_liveTranscriptPollLastText=i||"",_startTranscriptPoll(),e&&_ablyConnect(currentCampaign.id,e.id),_presEnterLive(),loadLivePcSidebar(),renderLiveSessionPreamble(),_isGm()&&(typeof _resetLiveContextState=="function"&&_resetLiveContextState(),typeof _startLiveContextScan=="function"&&_startLiveContextScan(),_loadNotebookData(),_openLiveAiDrawer()),_enforceLiveConsent()}function closeLiveSession(){_liveMinimized=!1,_pendingRecStart=!1,_recConsentAcknowledged=!1,_consentRequiredToStay=!1,document.getElementById("live-preamble")?.classList.add("hidden"),document.getElementById("live-ai-drawer")?.classList.add("hidden"),document.getElementById("live-ai-expander")?.classList.add("hidden"),document.getElementById("live-record-bar").classList.add("hidden"),clearInterval(liveTimerInterval),liveTimerInterval=null,typeof _stopLiveContextScan=="function"&&_stopLiveContextScan(),_tfDiscordRecActive&&_exitDiscordRecActive(),stopLiveTranscription(),liveMediaRecorder&&liveMediaRecorder.state!=="inactive"&&(_userInitiatedStop=!0,livePublishing=!1,liveMediaRecorder.stop()),resetRecordingUI(),_stopLiveLogPoll(),_stopTranscriptPoll(),_ablyDisconnect(),_presExitLive(),_presStartPoll(),liveMap&&(liveMap.remove(),liveMap=null,livePartyMarker=null,liveEntryMarkers=[],liveEntityMarkers=[]),_consentedSessionId=void 0,livePcSidebarPcs=[],livePcSidebarIdx=0,document.getElementById("live-pc-sidebar")?.classList.add("hidden"),document.getElementById("live-pc-sidebar-expander")?.classList.add("hidden"),document.getElementById("recording-consent-modal")?.classList.add("hidden"),document.getElementById("consent-accepted-badge")?.classList.add("hidden"),document.getElementById("live-session-screen").classList.add("hidden"),document.getElementById("main-app").classList.remove("hidden"),liveSession=null,_tfRestoreLastTab()}function _liveRoadDisplayName(e,t){const n=(e.label||"").trim();if(n)return n;if(e.encounter_id)return"Trail to encounter";const a=o=>o!=null&&t[o]?t[o]:null,s=a(e.from),i=a(e.to);return s&&i?`${s} \u27F7 ${i}`:s||i?`${s||i} \u27F7 open`:null}async function _liveRenderRoads(e,t){if(!liveMap||e==null||!window.AtlasRoutes)return;const a=liveMap;let s=[];try{const d=await api("GET",`/api/campaigns/${currentCampaign.id}/maps/${e}/roads`);s=d&&d.roads||[]}catch{return}if(liveMap!==a)return;const i=typeof liveSession<"u"&&liveSession&&liveSession.id||null;s=s.filter(d=>!d.encounter_id||i&&d.encounter_session_id===i),_liveDirRoads=s.filter(d=>!d.encounter_id).map(d=>{let u=d.waypoints;if(typeof u=="string")try{u=JSON.parse(u)}catch{u=[]}return{id:d.id,route_type:d.route_type,terrain:d.terrain||"road",label:d.label||null,hidden:!!d.hidden,diff_sections:d.diff_sections||null,ports:d.ports||null,waypoints:Array.isArray(u)?u:[]}});const o=(d,u)=>[t-u,d],r={};
1407(allEntities||[]).forEach(d=>{d&&!d.pending_reveal&&(r[d.id]=d.name)});const c=s.map((d,u)=>{let p=d.waypoints;if(typeof p=="string")try{p=JSON.parse(p)}catch{p=[]}const m={type:d.route_type==="water"?"water":"land",wps:Array.isArray(p)?p:[],label:d.label,roman:_at2Roman(u+1),entity_id:d.entity_id,from:d.from_entity_id,to:d.to_entity_id,hidden:!!d.hidden,terrain:d.terrain||"road",diff_sections:d.diff_sections||null,ports:d.ports||null,encounter_id:d.encounter_id||null};return m._name=_liveRoadDisplayName(m,r),m}),l=(_liveAllCampaignMaps||[]).find(d=>d&&d.id===e)||{};_tfRenderRoadLayer(liveMap,{roads:c,toLL:o,cid:currentCampaign&&currentCampaign.id,includeHidden:!1,hitWeight:24,pxPerMile:l.scale_px_per_mile||null,unit:l.scale_unit||"mi",travel:l.map_travel||null,popupMaxWidth:200,entityTip:!0,onLiveRoadClick:_liveRoadClickSetParty});try{await _liveRenderEncounters(e,o,a,i)}catch{}_liveRenderNearby()}async function _liveRenderEncounters(e,t,n,a){if(!liveMap||!a||!currentCampaign)return;let s=[];try{const i=await api("GET",`/api/campaigns/${currentCampaign.id}/maps/${e}/encounters`);s=i&&i.encounters||[]}catch{return}liveMap===n&&s.filter(i=>!i.hidden&&i.session_id===a).forEach(i=>{const o=i.encounter_no?`Encounter ${escHtml(String(i.encounter_no))}`:"Encounter",r=_at2EncChapterLabel(i),c=(i.description||"").trim();L.marker(t(i.x,i.y),{icon:L.divIcon({className:"at2-enc-wrap",html:`<div class="at2-enc-mark">${_MAP_ACT_ICO.swords}</div>`,iconSize:[22,22],iconAnchor:[11,11],popupAnchor:[0,-14]}),title:r?`${o} \xB7 ${r}`:o,keyboard:!1,zIndexOffset:240}).addTo(liveMap).bindPopup(`<div class="at2-pop"><b class="at2-pop-title">${o}</b>${r?`<div class="at2-pop-sub">${escHtml(r)}</div>`:""}${c?`<div class="at2-pop-sub">${escHtml(c)}</div>`:""}</div>`,{maxWidth:260,autoPan:!1})})}function _placeTier(e){const t=window.MapStyle,n=t?t.bucketOf(e.ledger&&e.ledger.kind||e.kind||null):"other";return e.area_map_id||n==="settlement"||n==="stronghold"||n==="sacred"?"beacon":e.image_url||n==="water"||n==="wildruin"?"named":"minor"}function _liveRenderEntityPins(){if(!liveMap)return;loadPinLockState(),liveEntityMarkers.forEach(s=>s.remove()),liveEntityMarkers=[];const e=liveAllMaps[liveCurrentMapIdx]||{},t=liveImgH,n=e.id??null,a=[];(allEntities||[]).forEach(s=>{if(s.type==="location"){const i=_liveResolveOnMap(s,n);i&&a.push({e:s,x:i.x,y:i.y,pinHidden:!!i.hidden})}else s.map_x!=null&&s.map_y!=null&&(n===null||s.map_id===n)&&a.push({e:s,x:s.map_x,y:s.map_y,pinHidden:!!s.map_pin_hidden})}),a.forEach(({e:s,x:i,y:o,pinHidden:r})=>{const c=makePin(s,s.type==="location"&&pinsLocked,r),l=L.marker([t-o,i],{icon:c});if(s.type==="location"){const d=(s.description||"").replace(/^#{1,6}\s+.+$/gm,"").replace(/\*\*/g,"").trim(),u=d.length>120?d.slice(0,120).replace(/\s+\S*$/,"")+"\u2026":d,p=`<div class="map-hover-card">
1408        <div class="map-hover-card-name">${escHtml(s.name)}</div>
1409        ${u?`<div class="map-hover-card-desc">${escHtml(u)}</div>`:""}
1410      </div>`;{const m=_safeTooltipDir(liveMap,l.getLatLng(),36);l.bindTooltip(p,{direction:m.direction,offset:m.offset,className:"map-hover-tooltip",opacity:1})}l.on("click",m=>{if(_liveDir&&!_liveDir.result){m&&m.originalEvent&&L.DomEvent.stop(m.originalEvent),_liveDirPickPin(s,l,i,o);return}_isGm()&&_liveOpenPinMenu(s,l,n)})}else{const d=_safeTooltipDir(liveMap,l.getLatLng(),32);l.bindTooltip(s.name,{direction:d.direction,offset:d.offset}),l.on("click",()=>{_liveDir&&!_liveDir.result||_isGm()&&_liveOpenPinMenu(s,l,n)})}l.addTo(liveMap),liveEntityMarkers.push(l)})}function _liveOpenPinMenu(e,t,n){const a=n!=null&&_livePlacements[e.id]?_livePlacements[e.id][n]:null,s=a?!!a.hidden:!!e.map_pin_hidden,i=document.createElement("div");i.className="map-popup-inner",i.innerHTML=`<div class="map-popup-name">${escHtml(e.name)}</div>
1411    <button type="button" class="btn-new map-popup-btn live-pin-party-btn">Set party here</button>
1412    <button type="button" class="btn-ghost map-popup-btn live-pin-hide-btn">${s?_MAP_ACT_ICO.eye+"<span>Reveal to players</span>":_MAP_ACT_ICO.eyeSlash+"<span>Hide from players</span>"}</button>`,i.querySelector(".live-pin-party-btn").addEventListener("click",async()=>{const o=t.getLatLng();try{t.closePopup()}catch{}await _liveSetPartyHere(o.lng,liveImgH-o.lat,e.id,e.name)}),i.querySelector(".live-pin-hide-btn").addEventListener("click",async()=>{const o=!s,r=currentCampaign?.id;try{if(a){if(await api("PATCH",`/api/campaigns/${r}/maps/${n}/placements/${e.id}`,{hidden:o}),_campaignStale(r))return;a.hidden=o}else if(await api("PUT",`/api/campaigns/${r}/entities/${e.id}`,{map_pin_hidden:o}),_campaignStale(r))return;(e.map_id===n||n==null)&&(e.map_pin_hidden=o);try{t.closePopup()}catch{}_liveRenderEntityPins(),showToast(o?`\u201C${e.name}\u201D is now hidden from players.`:`\u201C${e.name}\u201D is now visible to players.`)}catch(c){showToast(`Couldn't update \u201C${e.name}\u201D: ${c.message}`)}}),t.unbindPopup(),t.bindPopup(i,{maxWidth:240,closeButton:!0}).openPopup()}async function _liveSetPartyHere(e,t,n,a){const s=currentCampaign?.id;if(!s)return;const i=liveAllMaps[liveCurrentMapIdx]||{};try{if(await api("POST",`/api/campaigns/${s}/party-positions`,{map_x:e,map_y:t,map_id:i.id??null,location_entity_id:n??null,source:"manual",session_id:liveSession?.id??null,session_elapsed_seconds:liveElapsedSeconds||null}),_campaignStale(s)||!liveMap)return;livePartyPos={lat:liveImgH-t,lng:e},livePartyMarker&&livePartyMarker.setLatLng(livePartyPos),_liveRenderNearby(),showToast(`The party is at ${a}.`)}catch(o){showToast(`Couldn't move the party: ${o.message}`)}}
1412function _liveRoadClickSetParty(e,t){try{if(!_isGm()||_liveDir&&!_liveDir.result||liveSettingPos)return;e&&e.originalEvent&&L.DomEvent.stop(e.originalEvent);const n=window.AtlasRoutes,a={x:e.latlng.lng,y:liveImgH-e.latlng.lat},s=typeof _smoothPolyline=="function"?_smoothPolyline(t.wps||[]):t.wps||[],o=(n&&n.nearestOnPolylines?n.nearestOnPolylines(a,[{id:0,pts:s}],1/0):null)||a,r=t.label&&String(t.label).trim()||(t.type==="water"?"the waterway":"the road"),c=document.createElement("div");c.className="map-popup-inner",c.innerHTML=`<div class="map-popup-name">${escHtml(r)}</div>
1413      <button type="button" class="btn-new map-popup-btn live-road-party-btn">Set party here</button>`,c.querySelector(".live-road-party-btn").addEventListener("click",async()=>{try{liveMap.closePopup()}catch{}await _liveSetPartyHere(o.x,o.y,null,r)}),L.popup({maxWidth:240}).setLatLng(e.latlng).setContent(c).openOn(liveMap)}catch{}}function _onLivePartyPosEvent(e){try{if(!liveMap||!livePartyMarker||!currentCampaign||e.map_x==null||e.map_y==null)return;const t=liveAllMaps[liveCurrentMapIdx]||{};if(e.map_id!=null&&t.id!=null&&e.map_id!==t.id)return;livePartyPos={lat:liveImgH-e.map_y,lng:e.map_x},livePartyMarker.setLatLng(livePartyPos),_isGm()&&_liveRenderNearby()}catch{}}async function _liveLoadPlacements(){try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/placements`);_livePlacements={},(e&&e.placements||[]).forEach(t=>{(_livePlacements[t.entity_id]||(_livePlacements[t.entity_id]={}))[t.map_id]={x:t.x,y:t.y,hidden:!!t.hidden,is_port:!!t.is_port}})}catch{_livePlacements={}}}async function _onLiveMapPinEvent(e){try{if(!liveMap||!currentCampaign)return;const t=currentCampaign.id;if(await _liveLoadPlacements(),_campaignStale(t)||!liveMap)return;if(e&&e.entity_id)try{const n=await api("GET",`/api/campaigns/${t}/entities/${e.entity_id}`);if(_campaignStale(t))return;const a=(allEntities||[]).find(s=>s.id===e.entity_id);a&&n&&(a.map_x=n.map_x,a.map_y=n.map_y,a.map_id=n.map_id,a.map_pin_hidden=!!n.map_pin_hidden)}catch{}if(!liveMap)return;_liveRenderEntityPins(),_liveRenderLabels((liveAllMaps[liveCurrentMapIdx]||{}).id??null,liveImgH),_isGm()&&_liveRenderNearby()}catch{}}function _liveRenderLabels(e,t){(liveMapLabels||[]).forEach(n=>{try{n.lbl.remove()}catch{}}),liveMapLabels=[],liveMap&&((allEntities||[]).filter(n=>n.type==="location"&&n.map_x!=null&&n.map_y!=null&&(e==null||n.map_id===e)).forEach(n=>{const a=_placeTier(n),s=L.marker([t-n.map_y,n.map_x],{icon:L.divIcon({className:"",html:`<div class="map-place-label map-place-label--${a}">${escHtml(n.name)}</div>`,iconSize:[0,0],iconAnchor:[0,0]}),interactive:!1,keyboard:!1,zIndexOffset:450}).addTo(liveMap);liveMapLabels.push({lbl:s,e:n,tier:a})}),_liveLabelsLOD(),requestAnimationFrame(()=>{try{_liveLabelsLOD()}catch{}}),liveMap&&!liveMap.__labelLodBound&&(liveMap.__labelLodBound=!0,liveMap.on("zoomend",()=>{try{_liveLabelsLOD()}catch{}})))}function _liveLabelsLOD(){if(!liveMap)return;const e=liveMap.getZoom(),t=liveMap.getMinZoom(),n=liveMap.getMaxZoom(),a=Math.max(0,Math.min(1,(e-t)/Math.max(.001,n-t))),s={beacon:0,named:.4,minor:.72},i={beacon:0,named:1,minor:2},o=Math.round(5+a*a*55),r=[],c=liveMapLabels||[];
1413c.forEach(d=>{const u=d.lbl.getElement&&d.lbl.getElement();d._el=u&&u.querySelector(".map-place-label"),d._lod=d._el&&a>=(s[d.tier]!=null?s[d.tier]:1),d._show=!1});let l=0;c.filter(d=>d._lod).sort((d,u)=>i[d.tier]-i[u.tier]).forEach(d=>{if(l>=o)return;const u=liveMap.latLngToContainerPoint(d.lbl.getLatLng()),p=d._el.offsetWidth||60,m=d._el.offsetHeight||13,f={l:u.x-p/2-10,r:u.x+p/2+10,t:u.y-8,b:u.y+m+8};r.some(g=>f.l<g.r&&f.r>g.l&&f.t<g.b&&f.b>g.t)||(d._show=!0,r.push(f),l++)}),c.forEach(d=>{d._el&&d._el.classList.toggle("lbl-hidden",!d._show)});try{_liveRoadCanvasDraw()}catch{}}let _at2MapLabels=[];function _at2PlaceTier(e,t){const n=window.MapStyle,a=n?n.bucketOf(e.kind||null):"other";return e.area_map_id||a==="settlement"||a==="stronghold"||a==="sacred"?"beacon":t&&t.image_url||a==="water"||a==="wildruin"?"named":"minor"}function _at2RenderLabels(e,t,n){if((_at2MapLabels||[]).forEach(s=>{try{s.lbl.remove()}catch{}}),_at2MapLabels=[],!e)return;const a=_at2IsGm();(t||[]).forEach(s=>{if(s.x==null||s.y==null||s.map_pin_hidden&&!a)return;const i=(allEntities||[]).find(c=>c.id===s.entity_id),o=_at2PlaceTier(s,i),r=L.marker(n(s.x,s.y),{icon:L.divIcon({className:"",html:`<div class="map-place-label map-place-label--${o}">${escHtml(s.name)}</div>`,iconSize:[0,0],iconAnchor:[0,0]}),interactive:!1,keyboard:!1,zIndexOffset:250}).addTo(e);_at2MapLabels.push({lbl:r,l:s,tier:o})}),_at2LabelsLOD(e),requestAnimationFrame(()=>{try{_at2LabelsLOD(e)}catch{}}),e.__at2LabelLodBound||(e.__at2LabelLodBound=!0,e.on("zoomend",()=>{try{_at2LabelsLOD(e)}catch{}}))}function _at2LabelsLOD(e){if(!e)return;const t=e.getZoom(),n=e.getMinZoom(),a=e.getMaxZoom(),s=Math.max(0,Math.min(1,(t-n)/Math.max(.001,a-n))),i={beacon:0,named:.4,minor:.72},o={beacon:0,named:1,minor:2},r=Math.round(5+s*s*55),c=[],l=_at2MapLabels||[];l.forEach(u=>{const p=u.lbl.getElement&&u.lbl.getElement();u._el=p&&p.querySelector(".map-place-label"),u._lod=u._el&&s>=(i[u.tier]!=null?i[u.tier]:1),u._show=!1});let d=0;l.filter(u=>u._lod).sort((u,p)=>o[u.tier]-o[p.tier]).forEach(u=>{if(d>=r)return;const p=e.latLngToContainerPoint(u.lbl.getLatLng()),m=u._el.offsetWidth||60,f=u._el.offsetHeight||13,g={l:p.x-m/2-10,r:p.x+m/2+10,t:p.y-8,b:p.y+f+8};c.some(h=>g.l<h.r&&g.r>h.l&&g.t<h.b&&g.b>h.t)||(u._show=!0,c.push(g),d++)}),l.forEach(u=>{u._el&&u._el.classList.toggle("lbl-hidden",!u._show)});try{_liveRoadCanvasDraw(e)}catch{}}function _liveRoadStyleResolve(){const e=getComputedStyle(document.documentElement),t=(n,a)=>e.getPropertyValue(n).trim()||a;return{water:{color:t("--at2-kind-water","#5B91C4"),style:"italic",weight:"300",size:11,family:t("--type-display","Georgia, serif"),upper:!1,track:.13,halo:3.4},road:{color:t("--ember-base","#D4753A"),style:"normal",weight:"600",size:10,family:t("--type-ui","system-ui, sans-serif"),upper:!0,track:.08,halo:3},haloColor:"rgba(12,14,18,0.92)"}}function _liveRoadCanvasEnsure(e){if(e=e||liveMap,!e)return null;if(!e.getPane("roadLabels")){const n=e.createPane("roadLabels");n.style.zIndex=450,n.style.pointerEvents="none"}const t=e.getPane("roadLabels");if(!e.__roadCanvas||e.__roadCanvas.parentNode!==t){const n=document.createElement("canvas");n.className="live-road-label-canvas",t.appendChild(n),e.__roadCanvas=n}if(!e.__roadCanvasBound){e.__roadCanvasBound=!0;const n=()=>{try{_liveRoadCanvasDraw(e)}catch{}};e.on("move zoom moveend zoomend viewreset resize",n)}return e.__roadCanvas}function _rcCum(e){const t=[0];for(let n=1;n<e.length;n++)t[n]=t[n-1]+Math.hypot(e[n].x-e[n-1].x,e[n].y-e[n-1].y);return t}function _rcAt(e,t,n){const a=t[t.length-1];n=Math.max(0,Math.min(a,n));let s=1,i=t.length-1;for(;s<i;){const u=s+i>>1;t[u]<n?s=u+1:i=u}const o=s,r=e[o-1],c=e[o],l=t[o]-t[o-1]||1,d=(n-t[o-1])/l;return{x:r.x+(c.x-r.x)*d,y:r.y+(c.y-r.y)*d,ang:Math.atan2(c.y-r.y,c.x-r.x)}}function _rcVisibleRanges(e,t,n){const a=t[t.length-1];if(a<=0)return[];const s=Math.max(6,a/240),i=[];let o=null;for(let r=0;r<=a+.001;r+=s){const c=_rcAt(e,t,r);c.x>=n.x0&&c.x<=n.x1&&c.y>=n.y0&&c.y<=n.y1?o?o[1]=r:o=[r,r]:o&&(i.push(o),o=null)}return o&&i.push(o),i}function _rcMeasure(e,t,n){let a=0;for(const s of t)a+=e.measureText(s).width+n;return Math.max(0,a-n)}function _rcLayout(e,t,n,a,s,i,o){const r=_rcMeasure(e,t,i),c=_rcAt(n,a,s),d=_rcAt(n,a,s+r).x-c.x<0,u=[...t];let p=d?s+r:s;const m=[];let f=1e9,g=1e9,h=-1e9,v=-1e9;for(const y of u){const _=e.measureText(y).width,w=d?p-_/2:p+_/2,$=_rcAt(n,a,w),E=o*.6,k=_rcAt(n,a,w-E),C=_rcAt(n,a,w+E),S=Math.atan2(C.y-k.y,C.x-k.x)+(d?Math.PI:0);m.push({ch:y,x:$.x,y:$.y,ang:S}),f=Math.min(f,$.x),h=Math.max(h,$.x),g=Math.min(g,$.y),v=Math.max(v,$.y),p+=d?-(_+i):_+i}const b=o*.7;return{glyphs:m,box:{l:f-b,r:h+b,t:g-b,b:v+b}}}function _rcPaint(e,t,n,a){e.textAlign="center",e.textBaseline="middle",e.lineJoin="round",e.lineCap="round";for(const s of t.glyphs)e.save(),e.translate(s.x,s.y),e.rotate(s.ang),e.lineWidth=n.halo,e.strokeStyle=a,e.strokeText(s.ch,0,0),e.fillStyle=n.color,e.fillText(s.ch,0,0),e.restore(
1413)}function _rcStraightness(e,t,n,a){const s=_rcAt(e,t,n).ang;let i=0;for(let o=1;o<=5;o++){let r=_rcAt(e,t,n+a*o/5).ang-s;for(;r>Math.PI;)r-=2*Math.PI;for(;r<-Math.PI;)r+=2*Math.PI;i=Math.max(i,Math.abs(r))}return i}function _rcBestStart(e,t,n,a,s){if(a-n<s)return null;const i=Math.max(8,Math.min(60,Math.round((a-s-n)/10)));let o=null,r=1/0;for(let c=0;c<=i;c++){const l=n+(a-s-n)*c/i,d=_rcStraightness(e,t,l,s);d<r&&(r=d,o=l)}return o==null?null:{start:o,score:r}}function _liveRoadCanvasDraw(e){e=e||liveMap;const t=_liveRoadCanvasEnsure(e);if(!t||!e)return;const n=e.__roadNameData||[],a=e.getContainer(),s=a.clientWidth,i=a.clientHeight;if(s<=0||i<=0)return;const o=Math.min(window.devicePixelRatio||1,2);(t.width!==Math.round(s*o)||t.height!==Math.round(i*o))&&(t.width=Math.round(s*o),t.height=Math.round(i*o),t.style.width=s+"px",t.style.height=i+"px"),L.DomUtil.setPosition(t,e.containerPointToLayerPoint([0,0]));const r=t.getContext("2d");if(r.setTransform(o,0,0,o,0,0),r.clearRect(0,0,s,i),!n.length)return;const c=e.getZoom(),l=e.getMinZoom(),d=e.getMaxZoom(),u=Math.max(0,Math.min(1,(c-l)/Math.max(.001,d-l))),p={water:.3,road:.45},m=_liveRoadStyleResolve();r.globalAlpha=a.classList.contains("is-routing")?.3:1;const f=[],g=t.getBoundingClientRect();a.querySelectorAll(".map-place-label:not(.lbl-hidden)").forEach(b=>{const y=b.getBoundingClientRect();y.width&&f.push({l:y.left-g.left-3,r:y.right-g.left+3,t:y.top-g.top-3,b:y.bottom-g.top+3})});const h={x0:-30,y0:-30,x1:s+30,y1:i+30},v=n.slice().sort((b,y)=>(b.type==="water"?0:1)-(y.type==="water"?0:1));for(const b of v){const y=b.type==="water";if(u<(y?p.water:p.road))continue;const _=y?m.water:m.road,w=b.ll.map(I=>{const T=e.latLngToContainerPoint(I);return{x:T.x,y:T.y}}),$=_rcCum(w);r.font=`${_.style} ${_.weight} ${_.size}px ${_.family}`;const E=_.track*_.size,k=_.upper?b.name.toUpperCase():b.name,C=_rcMeasure(r,k,E);if(C<=0)continue;const S=_rcVisibleRanges(w,$,h).filter(I=>I[1]-I[0]>=C*1.25),q=[];for(const I of S){const T=_rcBestStart(w,$,I[0],I[1],C);T&&q.push(T)}q.sort((I,T)=>I.score-T.score);for(const I of q){const T=_rcLayout(r,k,w,$,I.start,E,_.size);if(!f.some(P=>T.box.l<P.r&&T.box.r>P.l&&T.box.t<P.b&&T.box.b>P.t)){_rcPaint(r,T,_,m.haloColor),f.push(T.box);break}}}r.globalAlpha=1}function _safeTooltipDir(e,t,n){const a=Math.abs(n||28),s={direction:"top",offset:[0,-a]};if(!e||!t)return s;try{const i=e.latLngToContainerPoint(t),o=e.getContainer(),r=o.clientWidth,c=o.clientHeight,l=110,d=220,u=130;if(i.x<u&&r-i.x>=d)return{direction:"right",offset:[a,0]};if(r-i.x<u&&i.x>=d)return{direction:"left",offset:[-a,0]};if(i.y>=l)return{direction:"top",offset:[0,-a]};if(c-i.y>=l)return{direction:"bottom",offset:[0,a]};if(r-i.x>=d)return{direction:"right",offset:[a,0]};if(i.x>=d)return{direction:"left",offset:[-a,0]}}catch{}return s}function _diffHatchCanvasEnsure(e){if(!e)return null;if(!e.getPane("diffHatch")){const n=e.createPane("diffHatch");n.style.zIndex=425,n.style.pointerEvents="none"}const t=e.getPane("diffHatch");if(!e.__diffCanvas||e.__diffCanvas.parentNode!==t){const n=document.createElement("canvas");n.className="tf-diff-canvas",t.appendChild(n),e.__diffCanvas=n}if(!e.__diffCanvasBound){e.__diffCanvasBound=!0;const n=()=>{try{_diffHatchCanvasDraw(e)}catch{}};e.on("move zoom moveend zoomend viewreset resize",n)}return e.__diffCanvas}function _diffHatchCanvasDraw(e){const t=_diffHatchCanvasEnsure(e);if(!t||!e)return;const n=e.getContainer(),a=n.clientWidth,s=n.clientHeight;if(a<=0||s<=0)return;const i=Math.min(window.devicePixelRatio||1,2);(t.width!==Math.round(a*i)||t.height!==Math.round(s*i))&&(t.width=Math.round(a*i),t.height=Math.round(s*i),t.style.width=a+"px",t.style.height=s+"px"),L.DomUtil.setPosition(t,e.containerPointToLayerPoint([0,0]));const o=t.getContext("2d");o.setTransform(i,0,0,i,0,0),o.clearRect(0,0,a,s);const r=e.__at2ToLL;if(!r)return;const c=getComputedStyle(n),l=(c.getPropertyValue("--ember-base")||c.getPropertyValue("--ember")||"#C26A2E").trim(),d=(c.getPropertyValue("--ink-3")||"#595E65").trim(),u=12,p=10,m=n.classList.contains("is-routing");function f(g,h,v,b){!g||!g.length||(o.strokeStyle=h,o.lineWidth=v,o.lineCap="round",g.forEach(y=>{b==="use-hidden"?o.globalAlpha=y.hidden?.4:m?.16:.9:o.globalAlpha=b;const _=y.pts;if(!_||_.length<2)return;const w=_.map(k=>e.latLngToContainerPoint(L.latLng(r(k.x,k.y))));o.beginPath();let $=0,E=u/2;for(let k=1;k<w.length;k++){const C=w[k-1],S=w[k],q=S.x-C.x,I=S.y-C.y,T=Math.hypot(q,I);if(T<1e-6)continue;const P=q/T,B=I/T,H=-B,A=P;for(;E<=$+T;){const x=E-$,M=C.x+P*x,D=C.y+B*x;o.moveTo(M+H*p/2,D+A*p/2),o.lineTo(M-H*p/2,D-A*p/2),E+=u}$+=T}o.stroke()}),o.globalAlpha=1)}f(e.__at2DiffSegs,l,1.5,"use-hidden"),f(e.__atDirRouteSegs,"rgba(255,255,255,.92)",1.6,1),f(e.__atDirAltSegs,d,1.2,.55)}async function initLiveMap(e=!1){if(_liveDirResetState(),_liveDirRoads=[],liveMap&&(liveMap.remove(),liveMap=null),!e||!liveAllMaps.length){try{const r=await api("GET",`/api/campaigns/${currentCampaign.id}/maps`);_liveAllCampaignMaps=r,liveAllMaps=r.filter(c=>c.visible_live)}catch{liveAllMaps.length||(liveAllMaps=[],_liveAllCampaignMaps=[])}await _liveLoadPlacements()}
1413liveAllMaps.length===0&&currentCampaign?.map_image_url&&(liveAllMaps=[{id:null,title:"Campaign Map",image_url:currentCampaign.map_image_url,visible_live:!0,visible_atlas:!0}]),liveCurrentMapIdx>=liveAllMaps.length&&(liveCurrentMapIdx=0),_renderLiveMapSwitcher(),liveMap&&(liveMap.remove(),liveMap=null);const t=document.getElementById("live-map-container");t.innerHTML="";const n=document.getElementById("live-map-empty"),a=liveAllMaps[liveCurrentMapIdx]??null;if(!a){t.style.display="none",n&&n.classList.remove("hidden");const r=document.getElementById("live-map-title-label");r&&(r.textContent="Map"),_liveDirEl("live-dir-pill")?.classList.add("hidden");return}t.style.display="",n&&n.classList.add("hidden"),_liveDirEl("live-dir-pill")?.classList.toggle("hidden",!_isGm());const s=document.getElementById("live-map-title-label");if(s){const r=a.location_entity_id?(_liveAllCampaignMaps||[]).filter(l=>l.location_entity_id===a.location_entity_id):[];let c="";if(r.length>1){r.sort((d,u)=>(d.level_ordinal==null)-(u.level_ordinal==null)||(d.level_ordinal||0)-(u.level_ordinal||0)||(d.sort_order||0)-(u.sort_order||0)||d.id-u.id);const l=r.findIndex(d=>d.id===a.id);c=` \xB7 ${escHtml(a.level_label||"Level "+(a.level_ordinal!=null?a.level_ordinal:l+1))}`}s.innerHTML=liveAllMaps.length>1?`${escHtml(a.title)}${c} <span class="live-map-count">(${liveCurrentMapIdx+1} of ${liveAllMaps.length})</span>`:"Map"}let i=null;try{const r=a.id?`/api/campaigns/${currentCampaign.id}/party-positions?map_id=${a.id}&limit=1`:`/api/campaigns/${currentCampaign.id}/party-positions?limit=1`,c=await api("GET",r);c.length>0&&(i=c[0])}catch{}const o=new Image;o.onload=()=>{const r=o.naturalWidth,c=o.naturalHeight;liveImgH=c,liveImgW=r;const l=[[0,0],[c,r]],d=document.getElementById("live-map-container"),u=.5;function p(){const h=d.clientWidth||400,v=d.clientHeight||300,b=Math.min(Math.log2(h/r),Math.log2(v/c));return Math.ceil(b/u)*u}liveMap=L.map("live-map-container",{crs:L.CRS.Simple,minZoom:p(),maxZoom:3.5,zoomSnap:u,attributionControl:!1,maxBounds:l,maxBoundsViscosity:1}),L.imageOverlay(a.image_url,l).addTo(liveMap),liveMap.fitBounds(l),_liveResizeObs&&_liveResizeObs.disconnect(),_liveResizeObs=new ResizeObserver(()=>{if(!liveMap)return;const h=p();liveMap.setMinZoom(h),liveMap.invalidateSize(),liveMap.getZoom()<h&&liveMap.setZoom(h)}),_liveResizeObs.observe(d),_liveRenderEntityPins();const m=a.id??null;livePartyPos=i?{lat:c-i.map_y,lng:i.map_x}:{lat:c/2,lng:r/2};const f=L.divIcon({className:"",html:'<div class="party-beacon"><div class="party-beacon-ring"></div><div class="party-beacon-dot"></div></div>',iconSize:[24,24],iconAnchor:[12,12]});_liveRenderRoads(m,c),_liveRenderLabels(m,c),livePartyMarker=L.marker([livePartyPos.lat,livePartyPos.lng],{icon:f,draggable:_isGm(),zIndexOffset:1e3}).addTo(liveMap),_liveApplyBeaconColor(a.image_url);const g=_isGm()?"Drag to Move Party":"Party";{const h=_safeTooltipDir(liveMap,[livePartyPos.lat,livePartyPos.lng],14);livePartyMarker.bindTooltip(g,{direction:h.direction,offset:h.offset})}livePartyMarker.on("dragend",async()=>{livePartyPos=livePartyMarker.getLatLng(),_liveRenderNearby();try{livePartyMarker.setTooltipContent(g)}catch{}try{await api("POST",`/api/campaigns/${currentCampaign.id}/party-positions`,{map_x:livePartyPos.lng,map_y:liveImgH-livePartyPos.lat,session_id:liveSession?.id??null,session_elapsed_seconds:liveElapsedSeconds||null,label:"dragged",map_id:a.id??null})}catch{}}),liveMap.on("click",async h=>{if(_liveDir&&!_liveDir.result){_liveDirPickPoint(h.latlng);return}if(liveSettingPos){livePartyPos=h.latlng,livePartyMarker.setLatLng(livePartyPos);try{livePartyMarker.setTooltipContent(g)}catch{}liveSettingPos=!1,document.getElementById("live-map-hint").classList.add("hidden");try{await api("POST",`/api/campaigns/${currentCampaign.id}/party-positions`,{map_x:livePartyPos.lng,map_y:liveImgH-livePartyPos.lat,session_id:liveSession?.id??null,session_elapsed_seconds:liveElapsedSeconds||null,label:"set",map_id:a.id??null})}catch{}return}if(!_isGm())return;const v={x:h.latlng.lng,y:liveImgH-h.latlng.lat},b=document.createElement("div");b.className="map-popup-inner",b.innerHTML='<button type="button" class="btn-new map-popup-btn live-click-party-btn">
1413Set party here</button>',b.querySelector(".live-click-party-btn").addEventListener("click",async()=>{try{liveMap.closePopup()}catch{}await _liveSetPartyHere(v.x,v.y,null,"the marked spot")}),L.popup({maxWidth:240}).setLatLng(h.latlng).setContent(b).openOn(liveMap)}),renderLiveEntryPins()},o.src=a.image_url}function _liveApplyBeaconColor(e){if(e)try{const t=new Image;t.crossOrigin="anonymous",t.onload=()=>{try{const n=_liveContrastColor(t),a=document.getElementById("live-map-container");a&&n&&a.style.setProperty("--beacon-color",n)}catch{}},t.onerror=()=>{},t.src=e}catch{}}function _liveContrastColor(e){const t=document.createElement("canvas");t.width=12,t.height=12;const n=t.getContext("2d");n.drawImage(e,0,0,12,12);const a=n.getImageData(0,0,12,12).data;let s=0,i=0,o=0,r=0;for(let u=0;u<a.length;u+=4)s+=a[u],i+=a[u+1],o+=a[u+2],r++;s/=r,i/=r,o/=r;const c=[[224,49,49],[240,140,0],[130,201,30],[12,166,120],[34,139,230],[190,75,220],[240,62,140],[248,249,250],[20,20,24]];let l=null,d=-1;return c.forEach(u=>{const p=u[0]-s,m=u[1]-i,f=u[2]-o,g=2*p*p+4*m*m+3*f*f;g>d&&(d=g,l=u)}),l?`rgb(${l[0]},${l[1]},${l[2]})`:null}function _liveDirEl(e){return document.getElementById(e)}function _liveDirResetState(){_liveDirClearRouteLayers(),_liveDir&&(_liveDir.fromEl&&_liveDir.fromEl.classList.remove("tf-pin--route-endpoint"),_liveDir.toEl&&_liveDir.toEl.classList.remove("tf-pin--route-endpoint")),_liveDir=null,_liveDirEl("live-map-container")?.classList.remove("is-routing","is-dir-arming"),_liveDirEl("live-dir-banner")?.classList.add("hidden"),_liveDirEl("live-dir-card")?.classList.add("hidden"),_liveDirEl("live-nav-info")?.classList.remove("dir-active"),_isGm()&&_liveDirEl("live-dir-pill")?.classList.remove("hidden")}function _liveDirClearRouteLayers(){if((_liveDirRouteLayers||[]).forEach(e=>{try{e.remove()}catch{}}),_liveDirRouteLayers=[],(_liveDirAltLayers||[]).forEach(e=>{try{e.remove()}catch{}}),_liveDirAltLayers=[],(_liveDirCampMarkers||[]).forEach(e=>{try{e.mk.remove()}catch{}}),_liveDirCampMarkers=[],liveMap){liveMap.__atDirRouteSegs=[],liveMap.__atDirAltSegs=[];try{_diffHatchCanvasDraw(liveMap)}catch{}}_liveDirClearScrub()}function _liveDirClearScrub(){(_liveDirScrubLayers||[]).forEach(e=>{try{e.remove()}catch{}}),_liveDirScrubLayers=[],_liveDirEl("live-map-container")?.classList.remove("has-scrubber")}function _liveDirStart(){!liveMap||!_isGm()||(_liveDirResetState(),_liveDir={from:null,to:null,fromEl:null,toEl:null,result:null},_liveDirSetBanner("Tap your start, a place or any point"),_liveDirEl("live-dir-banner")?.classList.remove("hidden"),_liveDirEl("live-dir-pill")?.classList.add("hidden"),_liveDirEl("live-map-container")?.classList.add("is-dir-arming"))}function _liveDirCancel(){_liveDirResetState()}function _liveDirPickPin(e,t,n,a){if(!_liveDir||_liveDir.result)return;const s=n??e.map_x,i=a??e.map_y,o={key:e.id,x:s,y:i,name:e.name,kind:_dirKindOf(e)};if(!_liveDir.from)_liveDir.from=o,_liveDir.fromEl=_liveDirMarkEndpoint(t),_liveDirSetBanner("Tap your destination");else{if(o.key===_liveDir.from.key)return;_liveDir.to=o,_liveDir.toEl=_liveDirMarkEndpoint(t),_liveDirCompute()}}function _liveDirMarkEndpoint(e){const t=e&&e.getElement&&e.getElement(),n=t&&t.querySelector(".tf-pin");return n&&n.classList.add("tf-pin--route-endpoint"),n||null}function _liveDirPickPoint(e){if(!_liveDir||_liveDir.result||!e)return;const t={x:e.lng,y:(liveImgH||1e3)-e.lat},n=(liveAllMaps[liveCurrentMapIdx]||{}).id??null,a=Math.sqrt((liveImgW||1e3)**2+(liveImgH||1e3)**2),s=a*.03;let i=null,o=s;(allEntities||[]).forEach(c=>{if(c.type!=="location"||c.map_x==null||c.map_y==null||n!=null&&c.map_id!==n)return;const l=Math.hypot(c.map_x-t.x,c.map_y-t.y);l<o&&(o=l,i=c)});const r=i?{key:i.id,x:i.map_x,y:i.map_y,name:i.name,kind:_dirKindOf(i)}:{key:"pt:"+Math.round(t.x)+":"+Math.round(t.y),x:t.x,y:t.y,name:"Map point",kind:""};if(!_liveDir.from)_liveDir.from=r,_liveDirSetBanner("Tap your destination");else{if(r.key===_liveDir.from.key)return;_liveDir.to=r,_liveDirCompute()}}function _liveDirSnapDist(){const e=liveImgW||1e3,t=liveImgH||1e3;return Math.sqrt(e*e+t*t)*.05}function _liveDirSnapToGraph(e,t){if(e&&_at2IsOffroadKind(e.kind))return{x:e.x,y:e.y};let n=null,a=_liveDirSnapDist();
1413return(t||_liveDirRoads||[]).forEach(s=>(s.waypoints||[]).forEach(i=>{const o=Math.hypot(i.x-e.x,i.y-e.y);o<a&&(a=o,n={x:i.x,y:i.y})})),n||{x:e.x,y:e.y}}function _liveDirDefaultMode(e){let t=null;try{const a=currentCampaign&&currentCampaign.id;a&&(t=localStorage.getItem("tf_dir_mode_"+a))}catch{}const n=e&&e.map_travel&&e.map_travel.default_mode;return(t||n)==="mounted"?"mounted":"foot"}function _liveDirDefaultWaterMode(e){let t=null;try{const a=currentCampaign&&currentCampaign.id;a&&(t=localStorage.getItem("tf_dir_watermode_"+a))}catch{}const n=e&&e.map_travel&&e.map_travel.default_watermode;return(t||n)==="rowboat"?"rowboat":"ship"}function _liveDirCompute(){const e=window.AtlasRoutes;if(!e||!_liveDir||!_liveDir.from||!_liveDir.to)return;const t=liveAllMaps[liveCurrentMapIdx]||{};_liveDir.mode||(_liveDir.mode=_liveDirDefaultMode(t)),_liveDir.watermode||(_liveDir.watermode=_liveDirDefaultWaterMode(t)),_liveDir.pace||(_liveDir.pace="normal");const n=(_liveDirRoads||[]).filter(l=>_liveDir.useHidden||!l.hidden),a=_liveDirSnapToGraph(_liveDir.from,n),s=_liveDirSnapToGraph(_liveDir.to,n),i=[{key:_liveDir.from.key,x:a.x,y:a.y},{key:_liveDir.to.key,x:s.x,y:s.y}],o=_liveDirPortPins(),r=e.planRoute(n,i,_liveDir.from.key,_liveDir.to.key,["land","water"],{portPins:o});if(_liveDirEl("live-dir-banner")?.classList.add("hidden"),r){_liveDir.crow=!1,_liveDir.result=r,_liveDir.altResult=_dirPickAlternate(e,n,i,_liveDir.from.key,_liveDir.to.key,r,o),_liveDirShowCard(_liveDirCardHtml(r,t)),_liveDirDrawRoute(r.points,r.legs);return}const c=e.planRouteFrom(n,[{key:_liveDir.to.key,x:_liveDir.to.x,y:_liveDir.to.y}],{x:_liveDir.from.x,y:_liveDir.from.y},_liveDir.to.key,["land","water"],{portPins:o});if(c){_liveDir.crow=(c.connectorLenPx||0)>.5,_liveDir.result=c,_liveDirShowCard(_liveDirCardHtml(c,t)),_liveDirDrawRoute(c.points,c.legs);return}_liveDir.crow=!1,_liveDir.result={none:!0},_liveDirShowCard(_liveDirNoRouteHtml())}function _liveDirReverse(){if(!_liveDir||!_liveDir.from||!_liveDir.to)return;const e=_liveDir.to,t=_liveDir.from,n=_liveDir.toEl,a=_liveDir.fromEl,s=_liveDir.mode,i=_liveDir.pace,o=_liveDir.watermode;_liveDirClearRouteLayers(),_liveDirEl("live-map-container")?.classList.remove("is-routing"),_liveDir={from:e,to:t,fromEl:n,toEl:a,result:null,mode:s,pace:i,watermode:o},_liveDirCompute()}function _liveDirDrawRoute(e,t){if(_liveDirClearRouteLayers(),!liveMap||!Array.isArray(e)||e.length<2)return;const n=liveMap.__at2ToLL||((i,o)=>[liveImgH-o,i]);_liveDirDrawAlt();const a=e.map(i=>n(i.x,i.y)),s=[L.polyline(a,{className:"tf-route-line tf-route-line--casing",interactive:!1}).addTo(liveMap)];Array.isArray(t)&&t.length?t.forEach(i=>{const o=i.type==="connector"?"tf-route-line tf-route-line--crow":i.type==="water"?"tf-route-line tf-route-line--core-water":"tf-route-line tf-route-line--core";s.push(L.polyline([n(i.a.x,i.a.y),n(i.b.x,i.b.y)],{className:o,interactive:!1}).addTo(liveMap))}):s.push(L.polyline(a,{className:"tf-route-line tf-route-line--core",interactive:!1}).addTo(liveMap)),Array.isArray(t)&&t.forEach(i=>{if(i.type==="water"||!i.diffLen||i.diffLen<1)return;const o=i.len>0?Math.min(i.diffLen,i.len)/i.len:0;o<.02||_liveDirRouteTicks([i.a,{x:i.a.x+(i.b.x-i.a.x)*o,y:i.a.y+(i.b.y-i.a.y)*o}],n,s)}),_liveDirRouteLayers=s,_liveDirEl("live-map-container")?.classList.add("is-routing");try{_diffHatchCanvasDraw(liveMap)}catch{}try{liveMap.fitBounds(s[0].getBounds(),{padding:L.point(26,26)})}catch{}_liveDirDrawCamps(),_liveDirDrawScrubber()}function _liveDirDrawAlt(){(_liveDirAltLayers||[]).forEach(o=>{try{o.remove()}catch{}}),_liveDirAltLayers=[];const e=_liveDir&&_liveDir.altResult;if(!liveMap||!e||!Array.isArray(e.points)||e.points.length<2)return;const t=liveMap.__at2ToLL||((o,r)=>[liveImgH-r,o]),n=e.points.map(o=>t(o.x,o.y)),a=L.polyline(n,{className:"tf-route-line tf-route-line--alt-hit",interactive:!0,bubblingMouseEvents:!1,opacity:0}).addTo(liveMap);a.bindTooltip("Alternate route, tap to use",{direction:"top",sticky:!0,className:"tf-route-alt-tip"}),a.on("click",_liveDirPromoteAlt);const s=[a],i=e.legs||[];i.length?(i.forEach(o=>{const r=o.type==="water"?"tf-route-line tf-route-line--alt-water":o.type==="connector"?"tf-route-line tf-route-line--alt-rough":"tf-route-line tf-route-line--alt";s.push(L.polyline([t(o.a.x,o.a.y),t(o.b.x,o.b.y)],{className:r,interactive:!1}).addTo(liveMap))}
1413),i.forEach(o=>{if(o.type==="water"||!o.diffLen||o.diffLen<1)return;const r=o.len>0?Math.min(o.diffLen,o.len)/o.len:0;r<.02||_liveDirAltTicks([o.a,{x:o.a.x+(o.b.x-o.a.x)*r,y:o.a.y+(o.b.y-o.a.y)*r}],t,s)})):s.push(L.polyline(n,{className:"tf-route-line tf-route-line--alt",interactive:!1}).addTo(liveMap)),_liveDirAltLayers=s}function _liveDirAltTicks(e,t,n){!liveMap||!Array.isArray(e)||e.length<2||(liveMap.__atDirAltSegs=liveMap.__atDirAltSegs||[],liveMap.__atDirAltSegs.push({pts:e.slice()}))}function _liveDirPromoteAlt(){if(!_liveDir||!_liveDir.altResult)return;const e=window.AtlasRoutes;if(!e)return;const t=liveAllMaps[liveCurrentMapIdx]||{},n=(_liveDirRoads||[]).filter(o=>_liveDir.useHidden||!o.hidden),a=_liveDirSnapToGraph(_liveDir.from,n),s=_liveDirSnapToGraph(_liveDir.to,n),i=[{key:_liveDir.from.key,x:a.x,y:a.y},{key:_liveDir.to.key,x:s.x,y:s.y}];_liveDir.result=_liveDir.altResult,_liveDir.crow=!1,_liveDir.altResult=_dirPickAlternate(e,n,i,_liveDir.from.key,_liveDir.to.key,_liveDir.result,_liveDirPortPins()),_liveDirShowCard(_liveDirCardHtml(_liveDir.result,t)),_liveDirDrawRoute(_liveDir.result.points,_liveDir.result.legs)}function _liveDirDrawCamps(){(_liveDirCampMarkers||[]).forEach(n=>{try{n.mk.remove()}catch{}}),_liveDirCampMarkers=[];const e=_liveDir&&_liveDir.result;if(!liveMap||!e||!Array.isArray(e.points)||e.points.length<2)return;const t=liveMap.__at2ToLL||((n,a)=>[liveImgH-a,n]);_dirCampNodes(e,liveAllMaps[liveCurrentMapIdx]||{},_liveDir||{},_liveDirRailCtx()).forEach(n=>{const a=L.marker(t(n.x,n.y),{icon:L.divIcon({className:"",html:_dirCampMarkerHtml(n),iconSize:[28,28],iconAnchor:[14,14]}),interactive:!1,keyboard:!1,zIndexOffset:880}).addTo(liveMap);_liveDirCampMarkers.push({mk:a,px:n.px})})}function _liveDirRouteTicks(e,t,n){!liveMap||!Array.isArray(e)||e.length<2||(liveMap.__atDirRouteSegs=liveMap.__atDirRouteSegs||[],liveMap.__atDirRouteSegs.push({pts:e.slice()}))}function _liveDirProfile(){return _dirRailProfile(_liveDir&&_liveDir.result,liveAllMaps[liveCurrentMapIdx]||{},_liveDir||{})}function _liveDirInterp(e,t){const n=e.pts;for(let a=1;a<n.length;a++)if(t<=n[a].px||a===n.length-1){const s=n[a].px-n[a-1].px,i=s>0?(t-n[a-1].px)/s:0;return{days:n[a-1].days+i*(n[a].days-n[a-1].days),hours:n[a-1].hours+i*(n[a].hours-n[a-1].hours)}}return{days:e.totalDays,hours:e.totalHours}}function _liveDirDetents(e){const t=[],n=e.pts;for(let a=1;a<e.totalDays+1e-9;a++)for(let s=1;s<n.length;s++)if(n[s].days>=a&&n[s-1].days<a){const i=n[s].days-n[s-1].days,o=i>0?(a-n[s-1].days)/i:0;t.push({day:a,px:n[s-1].px+o*(n[s].px-n[s-1].px)});break}return t}function _liveNearestPlaceName(e){const t=liveAllMaps[liveCurrentMapIdx]||{},n=t.id??null;let a=null,s=1/0;(allEntities||[]).forEach(o=>{if(o.type!=="location"||o.map_x==null||o.map_y==null||n!=null&&o.map_id!==n)return;const r=Math.hypot(o.map_x-e.x,o.map_y-e.y);r<s&&(s=r,a=o)});const i=Math.sqrt((liveImgW||1e3)**2+(liveImgH||1e3)**2);return a&&s<=i*.08?a.name:null}function _liveDirScrubText(e,t,n,a){const s=_liveDirInterp(e,t),i=_liveNearestPlaceName(n),o=i?` &middot; near ${escHtml(i)}`:"";if(a!=null)return`<b>End of Day ${a}</b> &middot; making camp${o}`;const r=Math.floor(s.days)+1,c=Math.max(0,Math.round((s.days-Math.floor(s.days))*e.landDayH));return`<b>Day ${r}</b> &middot; ~${c} hr${c===1?"":"s"} in${o}`}function _liveDirDrawScrubber(){_liveDirClearScrub();const e=_liveDir&&_liveDir.result;if(!liveMap||!_isGm()||!e||!Array.isArray(e.points)||e.points.length<2)return;const t=_liveDirProfile();if(!t||t.totalDays<.15)return;const n=window.AtlasRoutes,a=liveMap.__at2ToLL||((u,p)=>[liveImgH-p,u]),s=e.points,i=_liveDirDetents(t),o=[],r=n.pointAtFraction(s,0),c=L.marker(a(r.x,r.y),{icon:L.divIcon({className:"",html:'<div class="live-dir-scrubber" title="Drag along the route to scout the journey">&#9737;</div>',iconSize:[26,26],iconAnchor:[13,13]}),draggable:!0,zIndexOffset:1200}).addTo(liveMap);c.bindTooltip("",{permanent:!0,direction:"top",offset:[0,-15],className:"live-dir-scrub-tip",opacity:1});const l=u=>{let p=null,m=t.totalPx*.04;return i.forEach(f=>{const g=Math.abs(f.px-u);g<m&&(m=g,p=f)}),p},d=()=>{const u=c.getLatLng(),p={x:u.lng,y:liveImgH-u.lat};let m=n.projectFracOnPath(s,p),f=m*t.totalPx;const g=l(f);g&&(f=g.px,m=f/t.totalPx);const h=n.pointAtFraction(s,m);c.setLatLng(a(h.x,h.y)),c.setTooltipContent(_liveDirScrubText(t,f,h,g?g.day:null)),c.__onPt=h;let v=null,b=t.totalPx*.06;
1413return(_liveDirCampMarkers||[]).forEach(y=>{const _=Math.abs(y.px-f);_<b&&(b=_,v=y)}),(_liveDirCampMarkers||[]).forEach(y=>{const _=y.mk.getElement&&y.mk.getElement(),w=_&&_.querySelector(".dir-camp");w&&w.classList.toggle("is-detent-active",y===v)}),h};c.on("popupopen",u=>{const p=u.popup.getElement();p&&(p.querySelector("[data-scrub-move]")?.addEventListener("click",()=>{c.closePopup(),_liveDirMovePartyTo(c.__onPt)}),p.querySelector("[data-scrub-cancel]")?.addEventListener("click",()=>c.closePopup()))}),c.on("drag",d),c.on("dragend",()=>{d(),c.unbindPopup(),c.bindPopup('<div class="live-dir-scrub-commit"><span>Move the party here?</span><div class="live-dir-scrub-commit-row"><button type="button" class="btn-new btn-xs" data-scrub-move>&#10003; Move party</button><button type="button" class="btn-ghost btn-xs" data-scrub-cancel>&#10005;</button></div></div>',{className:"live-dir-scrub-pop",closeButton:!1,autoPanPadding:L.point(24,24)}).openPopup()}),o.push(c),_liveDirScrubLayers=o,_liveDirEl("live-map-container")?.classList.add("has-scrubber"),d()}async function _liveDirMovePartyTo(e){if(!e)return;const t=currentCampaign&&currentCampaign.id,n=liveAllMaps[liveCurrentMapIdx]||{};if(!t)return;const a=liveMap.__at2ToLL||((s,i)=>[liveImgH-i,s]);try{await api("POST",`/api/campaigns/${t}/party-positions`,{map_x:e.x,map_y:e.y,map_id:n.id??null,source:"manual"}),typeof showToast=="function"&&showToast("The party advances along the route."),livePartyPos={lat:liveImgH-e.y,lng:e.x},livePartyMarker&&livePartyMarker.setLatLng(a(e.x,e.y)),_liveRenderNearby()}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}function _liveDirDuration(e,t){if(e<t){const s=Math.max(1,Math.round(e));return{big:"~"+s,unit:s===1?"hr":"hrs"}}const n=Math.ceil(e/t);if(n<=10)return{big:"~"+n,unit:n===1?"day":"days"};const a=Math.ceil(n/7);return{big:"~"+a,unit:a===1?"week":"weeks"}}function _liveDirExact(e,t){const n=Math.floor(e/t),a=Math.round(e-n*t);if(n<=0){const s=Math.max(1,Math.round(e));return"\u2248 "+s+(s===1?" hr":" hrs")}return"\u2248 "+n+(n===1?" day":" days")+(a>0?", "+a+(a===1?" hr":" hrs"):"")}function _dirIsDh(){return typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()}const _DH_JOURNEY_BANDS=[{max:15,big:"Short",unit:"hop"},{max:40,big:"One day",unit:"of travel"},{max:120,big:"Several",unit:"days on the road"},{max:1/0,big:"A long",unit:"journey"}];function _dhJourneyBand(e,t,n){let a=_DH_JOURNEY_BANDS.findIndex(s=>(e||0)<=s.max);return a<0&&(a=_DH_JOURNEY_BANDS.length-1),(t==="mounted"||n)&&(a=Math.max(0,a-1)),_DH_JOURNEY_BANDS[a]}function _liveDirModeWord(e){return e==="mounted"?"mounted":"on foot"}function _liveDirWaterWord(e){return e==="rowboat"?"by rowboat":"by ship"}function _liveDirLegStr(e,t){if(e/t<1){const i=Math.max(1,Math.round(e));return i+(i===1?" hr":" hrs")}const n=Math.round(e/t*2)/2,a=Math.floor(n),s=n-a;return(a>0?a:"")+(s?"\xBD":"")+" days"}function _liveDirTimeModel(e,t,n,a,s){const i=window.AtlasRoutes,o=t.scale_px_per_mile||null;if(!o||o<=0)return null;const r=t.map_travel||null,c={slow:2/3,normal:1,fast:4/3}[a]||1,l=i.resolvePace(n,r),d=i.resolvePace(s==="rowboat"?"rowboat":"ship",r),u=l.mph*c,p=(e.landLenPx||0)/o,m=(e.waterLenPx||0)/o,f=(e.landDiffPx||0)/o,g=Math.max(0,p-f),h=u>0?g/u+f/u*2:0,v=d.mph>0?m/d.mph:0,b=l.dayH||8,y=d.dayH||24;return{totalMi:e.lenPx/o,landMi:p,waterMi:m,landDiffMi:f,landHours:h,waterHours:v,landDayH:b,waterDayH:y,landDays:h/b,waterDays:v/y,totalDays:h/b+v/y,hasWater:m>.01,hasLand:p>.01}}function _dirRailProfile(e,t,n){const a=window.AtlasRoutes;if(!e||!e.legs||!e.legs.length||!a)return null;const s=t.scale_px_per_mile||null;if(!s||s<=0)return null;const i=t.map_travel||null,o={slow:2/3,normal:1,fast:4/3}[n.pace]||1,r=a.resolvePace(n.mode||"foot",i),c=a.resolvePace(n.watermode==="rowboat"?"rowboat":"ship",i),l=r.mph*o,d=r.dayH||8,u=c.mph,p=c.dayH||24;let m=0,f=0,g=0;const h=[{px:0,hours:0,days:0}];return e.legs.forEach(v=>{let b,y;if(v.type==="water")b=u>0?v.len/s/u:0,y=p;else{const _=Math.max(0,v.len-v.diffLen);b=l>0?_/s/l+v.diffLen/s/l*2:0,y=d}m+=v.len,f+=b,g+=y>0?b/y:0,h.push({px:m,hours:f,days:g})}),{pts:h,totalPx:m,totalHours:f,totalDays:g,landDayH:d}}
1413const DIR_ALT_MAX_TIME=1.3,DIR_ALT_MAX_SHARED=.5,DIR_ALT_PENALTY=5;function _dirLegsMatch(e,t){if(e.roadId==null||e.roadId!==t.roadId)return!1;const n=1.5,a=Math.hypot(e.a.x-t.a.x,e.a.y-t.a.y)<n&&Math.hypot(e.b.x-t.b.x,e.b.y-t.b.y)<n,s=Math.hypot(e.a.x-t.b.x,e.a.y-t.b.y)<n&&Math.hypot(e.b.x-t.a.x,e.b.y-t.a.y)<n;return a||s}function _dirSharedLen(e,t){let n=0;return t.legs.forEach(a=>{e.legs.some(s=>_dirLegsMatch(a,s))&&(n+=a.len)}),n}function _dirPickAlternate(e,t,n,a,s,i,o){if(!e||!i||!(i.roadIds||[]).length)return null;const r=d=>(d.lenPx||0)+(d.diffLenPx||0),c=JSON.stringify((i.roadIds||[]).slice().sort());let l=null;return i.roadIds.forEach(d=>{const u=e.planRoute(t,n,a,s,["land","water"],{avoidRoadIds:{[d]:!0},penaltyMult:DIR_ALT_PENALTY,portPins:o||null});if(!u||!u.legs||!u.legs.length||JSON.stringify((u.roadIds||[]).slice().sort())===c)return;const p=r(u)/Math.max(1,r(i)),m=_dirSharedLen(i,u)/Math.max(1,u.lenPx||1);p>DIR_ALT_MAX_TIME||m>DIR_ALT_MAX_SHARED||(!l||p<l.timeRatio)&&(l={cand:u,timeRatio:p,sharedFrac:m})}),l?l.cand:null}function _dirPtAtLegPx(e,t){if(!e||!e.length)return null;let n=0;for(let s=0;s<e.length;s++){const i=e[s];if(n+i.len>=t||s===e.length-1){const o=i.len>0?Math.max(0,Math.min(1,(t-n)/i.len)):0;return{x:i.a.x+(i.b.x-i.a.x)*o,y:i.a.y+(i.b.y-i.a.y)*o,type:i.type}}n+=i.len}const a=e[e.length-1];return{x:a.b.x,y:a.b.y,type:a.type}}function _dirCampNodes(e,t,n,a){if(!e||!e.legs||!e.legs.length)return[];const s=_dirRailProfile(e,t,n);if(!s||!(s.totalPx>0))return[];a=a||{};const i=window.AtlasRoutes,o=Math.sqrt((a.imgW||1e3)**2+(a.imgH||1e3)**2),r=t.scale_px_per_mile||0,c=i?i.resolvePace(n.mode||"foot",t.map_travel||null):{mph:0},l={slow:2/3,normal:1,fast:4/3}[n.pace]||1,d=c.mph*l,u=r>0&&d>0?d*r:o*.015,p=o*.01;return _liveDirDetents(s).map(m=>{const f=_dirPtAtLegPx(e.legs,m.px)||{x:0,y:0,type:"land"};let g=null,h=1/0;(a.locations||[]).forEach(b=>{if(b.x==null||b.y==null||b.hidden&&!a.isGm||_at2IsOffroadKind(b.kind))return;const y=Math.hypot(b.x-f.x,b.y-f.y);y<h&&(h=y,g=b)});let v;return g&&h<=p?v=!0:f.type==="connector"?v=!1:v=!!(g&&h<=u),{day:m.day,px:m.px,x:f.x,y:f.y,atStop:v,placeName:v&&g?g.name:null}})}function _dirCampGlyph(e){return e?'<svg class="dir-camp__glyph" viewBox="0 0 12 10" aria-hidden="true" fill="none"><rect x="1" y="4.5" width="10" height="4" rx="0.8" fill="none" stroke="currentColor" stroke-width="1.1"/><rect x="1.5" y="5.2" width="2.8" height="2.5" rx="0.5" fill="currentColor" opacity="0.55"/><line x1="1" y1="1.5" x2="1" y2="5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="11" y1="1.5" x2="11" y2="5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/><line x1="1" y1="1.5" x2="11" y2="1.5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/></svg>':'<svg class="dir-camp__glyph" viewBox="0 0 12 12" aria-hidden="true" fill="none"><line x1="2" y1="9.5" x2="7" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.85"/><line x1="10" y1="9.5" x2="5" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.85"/><path d="M6 7 C5.2 5.5 4.8 4 5.8 2.5 C5.5 3.8 6.5 4.2 6.5 3 C7.2 4.5 7.5 5.5 6.8 6.8 Z" fill="currentColor" opacity="0.95"/><path d="M4.5 7 C4 5.8 3.8 4.5 4.5 3.5 C4.3 4.5 5 4.8 5 4 C5.4 5 5.5 6 4.8 7 Z" fill="currentColor" opacity="0.6"/><path d="M7.5 7 C8 5.8 8.2 4.5 7.5 3.5 C7.7 4.5 7 4.8 7 4 C6.6 5 6.5 6 7.2 7 Z" fill="currentColor" opacity="0.6"/></svg>'}function _dirCampMarkerHtml(e){const t=e.day,n=e.atStop?`Day ${t}: camp at ${e.placeName||"a settlement"} (inn)`:`Day ${t}: wilderness camp`;return`<div class="${e.atStop?"dir-camp dir-camp--inn":"dir-camp dir-camp--wild"}" title="${escHtml(n)}" aria-label="${escHtml(n)}">${_dirCampGlyph(e.atStop)}<span class="dir-camp__num">${t}</span></div>`}function _dirRailModel(e,t,n,a){const s=_dirRailProfile(e,t,n);if(!s||!(s.totalPx>0))return{calibrated:!1};a=a||{};const i=s.totalPx,o=window.AtlasRoutes,r=[];if(e.legs&&e.legs.length){let q=0;e.legs.forEach(T=>{r.push({x:T.a.x,y:T.a.y,px:q}),q+=T.len});const I=e.legs[e.legs.length-1];r.push({x:I.b.x,y:I.b.y,px:q})}else{const q=e.points||[];let I=0;q.forEach((T,P)=>{P&&(I+=Math.hypot(T.x-q[P-1].x,T.y-q[P-1].y)),r.push({x:T.x,y:T.y,px:I})})}const c=(r.length?r[r.length-1].px:0)||i,l=Math.sqrt((a.imgW||1e3)**2+(a.imgH||1e3)**2),d=l*.015,u=[{name:n.from.name,leftPct:0,isEnd:"a",hidden:!1},{name:n.to.name,leftPct:100,isEnd:"b",hidden:!1}];
1413(a.locations||[]).forEach(q=>{if(q.x==null||q.y==null||q.hidden&&!a.isGm||_at2IsOffroadKind(q.kind))return;let I=1/0,T=0;for(let B=0;B<r.length;B++){const H=Math.hypot(r[B].x-q.x,r[B].y-q.y);H<I&&(I=H,T=r[B].px)}if(I>d)return;const P=T/c*100;P<3.5||P>96.5||u.some(B=>!B.isEnd&&Math.abs(B.leftPct-P)<2)||u.push({name:q.name,leftPct:P,isEnd:null,hidden:!!q.hidden})}),u.sort((q,I)=>q.leftPct-I.leftPct);const p=t.scale_px_per_mile||null,m=t.scale_unit||"mi",f=t.map_travel||null,g=q=>q==null?null:q<1?"<1 hr":Math.round(q)+" hr"+(Math.round(q)===1?"":"s"),h=q=>q.type==="water"?"water":q.type==="connector"?"cross":(q.diffLen||0)>=q.len*.5?"rough":"road",v=[];let b=0;(e.legs||[]).forEach(q=>{const I=h(q),T=v[v.length-1];T&&T.kind===I?T._len+=q.len:v.push({kind:I,_start:b,_len:q.len}),b+=q.len}),v.forEach(q=>{q.leftPct=q._start/i*100,q.widthPct=q._len/i*100});const y=[];for(let q=0;q+1<u.length;q++){const I=u[q],T=u[q+1],P=T.leftPct-I.leftPct;if(P<=.1)continue;const B=I.leftPct/100*i,H=T.leftPct/100*i,A=p>0?(H-B)/p:null,x=_liveDirInterp(s,H).hours-_liveDirInterp(s,B).hours;let M=!1,D=!1;const O={};let N=0;(e.legs||[]).forEach(K=>{const se=N,Q=N+K.len;N=Q,!(Q<=B+.01||se>=H-.01)&&(K.type==="water"&&(D=!0),(K.diffLen||0)>0&&(M=!0),K.roadId!=null&&(O[K.roadId]=(O[K.roadId]||0)+K.len))});let G=null,F=-1;Object.keys(O).forEach(K=>{O[K]>F&&(F=O[K],G=K)});const W=G!=null&&a.roadLabels&&(a.roadLabels[G]||"").trim()||"",z=[],V=g(x);V&&z.push(V),A!=null&&o&&z.push(o.formatDistance(A,m,f)),z.push(W||`${I.name} \u2194 ${T.name}`),M&&z.push("difficult"),D&&z.push("water route"),y.push({leftPct:I.leftPct,widthPct:P,tip:z.join(" \xB7 ")})}const _=_dirCampNodes(e,t,n,a).map(q=>({day:q.day,leftPct:q.px/i*100,atStop:q.atStop,placeName:q.placeName}));let w=null,$=null,E=null,k=null,C=null;const S=n&&n.altResult;if(S&&Array.isArray(S.legs)&&S.legs.length){const q=S.legs.reduce((I,T)=>I+T.len,0);if(q>0){w=[];let I=0;S.legs.forEach(N=>{const G=h(N),F=w[w.length-1];F&&F.kind===G?F._len+=N.len:w.push({kind:G,_start:I,_len:N.len}),I+=N.len}
1413),w.forEach(N=>{N.leftPct=N._start/q*100,N.widthPct=N._len/q*100});const T=[];let P=0;S.legs.forEach(N=>{T.push({x:N.a.x,y:N.a.y,px:P}),P+=N.len});const B=S.legs[S.legs.length-1];T.push({x:B.b.x,y:B.b.y,px:P});const H=T[T.length-1].px||q;C=[],(a.locations||[]).forEach(N=>{if(N.x==null||N.y==null||N.hidden&&!a.isGm||_at2IsOffroadKind(N.kind))return;let G=1/0,F=0;for(let z=0;z<T.length;z++){const V=Math.hypot(T[z].x-N.x,T[z].y-N.y);V<G&&(G=V,F=T[z].px)}if(G>d)return;const W=F/H*100;W<3.5||W>96.5||C.some(z=>Math.abs(z.leftPct-W)<2)||C.push({leftPct:W})});const A=N=>N?(N.lenPx||0)+(N.diffLenPx||0):0,x=A(e);x>0&&($=A(S)/x);const M=_dirRailProfile(S,t,n);M&&M.totalPx>0&&(E=_liveDirDetents(M).map(N=>({day:N.day,leftPct:N.px/M.totalPx*100})));const D=_liveDirTimeModel(S,t,n.mode||"foot",n.pace||"normal",n.watermode||"ship"),O=["Alternate"];if(D){const N=D.landHours+D.waterHours,G=D.totalDays>=1?`~${Math.max(1,Math.ceil(D.totalDays))} ${D.totalDays>=2?"days":"day"}`:`~${Math.max(1,Math.round(N))} hrs`;O.push(G),o&&D.totalMi!=null&&O.push(o.formatDistance(D.totalMi,m,f));const F=(S.roadIds||[]).length;F&&O.push(F===1?"1 road":F+" roads"),D.landDiffMi>0&&O.push("difficult terrain"),D.hasWater&&O.push("water route")}$!=null&&O.push($>=1?`+${Math.round(($-1)*100)}% travel time`:`\u2212${Math.round((1-$)*100)}% travel time`),O.push("tap to use"),k=O.join(" \xB7 ")}}return{calibrated:!0,segs:v,spans:y,camps:_,stops:u,totalDays:s.totalDays,altSegs:w,altScale:$,altDayMarks:E,altTip:k,altStops:C}}function _dirMarchRailHtml(e,t,n){if(!e||!e.calibrated||!e.segs.length)return"";const a=$=>Math.max(0,Math.min(100,$)).toFixed(2),s=e.segs.map($=>`<span class="dir-rail-seg dir-rail-seg--${$.kind}${$.kind==="rough"&&n?" dir-rail-seg--enc":""}" style="left:${a($.leftPct)}%;width:${a($.widthPct)}%"></span>`).join(""),i=(e.spans||[]).map($=>`<span class="dir-rail-span" style="left:${a($.leftPct)}%;width:${a($.widthPct)}%" title="${escHtml($.tip)}"></span>`).join(""),o=e.stops.filter($=>!$.isEnd).map($=>`<span class="dir-rail-stop${$.hidden?" dir-rail-stop--hidden":""}" style="left:${a($.leftPct)}%" title="${escHtml($.name)}"></span>`).join(""),r=`<span class="dir-rail-end dir-rail-end--a" title="${escHtml(t.from.name)}">A</span><span class="dir-rail-end dir-rail-end--b" title="${escHtml(t.to.name)}">B</span>`,c=_dirIsDh(),l=c?"Camp":"Day",d=e.camps.map($=>`<div class="dir-rail-camp-wrap${$.atStop?" dir-rail-camp--town":""}" style="left:${a($.leftPct)}%" title="${l} ${$.day} Camp${$.placeName?": "+escHtml($.placeName)+" (inn)":$.atStop?"":": Wilderness Camp"}"></div>`).join(""),u=e.stops.filter($=>!$.isEnd),p=($,E)=>$.leftPct<14||$.leftPct>86||E%2?"lower":"upper",m=u.map(($,E)=>`<span class="dir-rail-leader dir-rail-leader--${p($,E)}" style="left:${a($.leftPct)}%"></span>`).join(""),f=`<span class="dir-rail-place dir-rail-place--a">${escHtml(t.from.name)}</span>`+u.map(($,E)=>`<span class="dir-rail-place dir-rail-place--mid dir-rail-place--${p($,E)}${$.hidden?" dir-rail-place--hidden":""}" style="left:${a($.leftPct)}%">${escHtml($.name)}</span>`).join("")+`<span class="dir-rail-place dir-rail-place--b">${escHtml(t.to.name)}</span>`,g=e.camps.length>7,h=e.camps.map(($,E)=>g&&E%2?"":`<span class="dir-rail-day" style="left:${a($.leftPct)}%" title="${escHtml($.atStop?l+" "+$.day+": camp at "+($.placeName||"a settlement")+" (inn)":l+" "+$.day+": wilderness camp")}"><span class="dir-rail-day-icon dir-rail-day-icon--${$.atStop?"inn":"wild"}">${_dirCampGlyph($.atStop)}</span>${l} ${$.day}</span>`).join(""),v=e.altScale!=null?Math.max(.3,Math.min(3,e.altScale)):null,b=v!=null&&v>1?a(100/v):"100.00",y=v!=null&&v<1?a(v*100):"100.00",_=b!=="100.00"?` style="width:${b}%"`:"";return`<div class="dir-rail" role="img" aria-label="${c?`Journey planner: ${escHtml(t.from.name)} to ${escHtml(t.to.name)}, ${e.camps.length} camp${e.camps.length===1?"":"s"} along the way`:`March planner: ${escHtml(t.from.name)} to ${escHtml(t.to.name)}, ${Math.max(1,Math.ceil(e.totalDays))} day march`}"><div class="dir-rail-places"${_}>${f}</div><div class="dir-rail-track-wrap"${_}><div class="dir-rail-track"><div class="dir-rail-groove"></div>${s}${i}${m}${d}${o}${r}</div></div><div class="dir-rail-days"${_}>${h}</div></div>`}function _dirAltSubrailHtml(e){if(!e||!e.altSegs||!e.altSegs.length||e.altScale==null)return"";const t=l=>Math.max(0,Math.min(100,l)).toFixed(2),n=Math.max(.3,Math.min(3,e.altScale)),a=n<1?t(n*100):"100.00",s=a!=="100.00"?` style="width:${a}%"`:"",i=e.altSegs.map(l=>`<span class="dir-subrail-seg dir-subrail-seg--${l.kind}" style="left:${t(l.leftPct)}%;width:${t(l.widthPct)}%"></span>
1413`).join(""),o=(e.altDayMarks||[]).map(l=>`<span class="dir-subrail-daymark" style="left:${t(l.leftPct)}%"></span>`).join(""),r=(e.altStops||[]).map(l=>`<span class="dir-subrail-stop" style="left:${t(l.leftPct)}%"></span>`).join(""),c=e.altTip||"Alternate route, tap to use";return`<button type="button" class="dir-subrail" data-dir-promote-alt title="${escHtml(c)}"><span class="dir-subrail-label">Alternate</span><span class="dir-subrail-track-wrap"${s}><span class="dir-subrail-track">${i}${o}${r}</span></span></button>`}function _dirCardHtml(e,t,n,a,s){const i=window.AtlasRoutes,o=t.scale_unit||"mi",r=t.map_travel||null,c=n.mode||"foot",l=n.pace||"normal",d=n.watermode||"ship",u=!!(n&&n.crow),p=(e.roadIds||[]).length,m=u?p?`${p===1?"1 road":p+" roads"} &middot; cross-country`:"as the crow flies":p===1?"1 road":p+" roads",f=`<span class="live-dir-route">${escHtml(n.from.name)} <span class="live-dir-arrow">&#8594;</span> ${escHtml(n.to.name)}</span>`,g=`<div class="live-dir-actions"><button type="button" class="btn-ghost btn-xs" data-${a}="reverse">&#8645; Reverse</button><button type="button" class="btn-ghost btn-xs" data-${a}="clear">&#10005; Clear</button></div>`,h=`<div class="live-dir-top">${f}${g}</div>`,v=_dirIsDh(),b=_liveDirTimeModel(e,t,c,l,d);if(!b)return h+`<span class="live-dir-headline"><span class="live-dir-big">Route</span> <span class="live-dir-unit">found</span></span><span class="live-dir-furniture">${m} &middot; set the map scale for ${v?"distance":"travel time"}</span>`;let y,_;if(b.hasWater){const M=Math.max(1,Math.ceil(b.totalDays));y=M<=10?{big:"~"+M,unit:M===1?"day":"days"}:{big:"~"+Math.ceil(M/7),unit:Math.ceil(M/7)===1?"week":"weeks"},_=b.hasLand?_liveDirModeWord(c):_liveDirWaterWord(d)}else y=_liveDirDuration(b.landHours,b.landDayH),_=_liveDirModeWord(c);const w=b.hasWater?b.hasLand?`${_liveDirLegStr(b.landHours,b.landDayH)} ${_liveDirModeWord(c)} &middot; ${_liveDirLegStr(b.waterHours,b.waterDayH)} ${_liveDirWaterWord(d)}`:`${_liveDirExact(b.waterHours,b.waterDayH)} ${_liveDirWaterWord(d)}`:_liveDirExact(b.landHours,b.landDayH);if(v){const M=_dhJourneyBand(b.totalMi,c,b.hasWater);y={big:M.big,unit:M.unit},_=""}const $=b.landDiffMi>0?` &middot; <span class="live-dir-warn">&#9888; ${v?"rough country":"difficult terrain"}</span>`:"",E=(v?"":`${w} &middot; `)+`${i.formatDistance(b.totalMi,o,r)} &middot; ${m}${$}`,k=(M,D,O)=>'<div class="tf-segctl">'+D.map(N=>`<button type="button" class="tf-segctl-btn${N.v===O?" active":""}" data-${a}-${M}="${N.v}"${N.t?` title="${N.t}"`:""}>${N.l}</button>`).join("")+"</div>",C=(M,D)=>`<div class="live-dir-segwrap"><span class="live-dir-seglabel">${M}</span>${D}</div>`,S=b.hasLand?C("Mode",k("mode",[{v:"foot",l:"Foot",t:v?"On foot: the party travels at the pace of the fiction":""},{v:"mounted",l:"Mounted",t:v?"Mounted: quicker, but conspicuous, and the animals need care":""}],c)):"",q=b.hasLand&&!v?C("Pace",k("pace",[{v:"slow",l:"Slow",t:"Slow pace: can use Stealth / forage"},{v:"normal",l:"Normal"},{v:"fast",l:"Fast",t:"Fast pace: \u22125 passive Perception"}],l)):"",I=b.hasWater?C("Water",k("watermode",[{v:"ship",l:"Ship",t:v?"By ship: open water, and whoever else is on it":"Ship: sails day & night (24h day)"},{v:"rowboat",l:"Rowboat",t:v?"By rowboat: close to shore, and slower going":"Rowboat: day-travels like land (8h day)"}],d)):"",T=s&&s.isGm&&s.hasHiddenRoads?`<label class="live-dir-hidden"><input type="checkbox" class="live-dir-hidden-cb" data-${a}-usehidden${n.useHidden?" checked":""}><span class="live-dir-hidden-lbl">Hidden Roads</span></label>`:"",P=S||I?`<div class="live-dir-ctrl-stack live-dir-ctrl-stack--left">${S}${I}</div>`:"",B=q||T?`<div class="live-dir-ctrl-stack live-dir-ctrl-stack--right">${q}${T}</div>`:"",H=_dirRailModel(e,t,n,s||{}),A=_dirMarchRailHtml(H,n,!!(s&&s.isGm)),x=_dirAltSubrailHtml(H);return h+'<div class="live-dir-card-body"><div class="live-dir-headline-row">'+P+`<span class="live-dir-headline"><span class="live-dir-big">${y.big}</span> <span class="live-dir-unit">${y.unit}</span> <span class="live-dir-mode">${_}</span></span>`+B+"</div>"+A+`<span class="live-dir-furniture">${E}</span>`+_dhFearNudgeHtml(v,b,a,s)+x+"</div>"}function _dhFearNudgeHtml(e,t,n,a){if(!e||!t||!(a&&a.isGm))return"";const s=t.landDiffMi>0,i=(t.totalMi||0)>40,o=s&&i?2:1;return`<div class="live-dir-fear"><span class="live-dir-fear-q">Worth marking ${o===2?"1 or 2":"a"} Fear for this leg?</span><button type="button" class="btn-ghost btn-xs" data-${n}-fear="1">+1 Fear</button>`+(o===2?`<button type="button" class="btn-ghost btn-xs" data-${n}-fear="2">+2 Fear</button>`:"")+"</div>"}
1413async function _dhDirAddFear(e){const t=currentCampaign&&currentCampaign.id;if(t==null)return;const n=Math.max(0,Math.min(12,currentCampaign&&currentCampaign.fear_pool||0)),a=Math.max(0,Math.min(12,n+e));if(a===n){showToast("Fear is already at 12.");return}try{const s=await api("PUT",`/api/campaigns/${t}/fear`,{fear:a});if(_campaignStale(t))return;currentCampaign.fear_pool=s.fear_pool,typeof _liveRenderFearBar=="function"&&_liveRenderFearBar(),showToast(`Fear ${s.fear_pool}/12.`)}catch{showToast("Could not mark Fear.")}}function _liveDirCardHtml(e,t){return _dirCardHtml(e,t,_liveDir,"livedir",_liveDirRailCtx())}function _liveDirRailCtx(){const e=(liveAllMaps[liveCurrentMapIdx]||{}).id??null,t=_liveEntsOnMap(e).map(({e:a,x:s,y:i,hidden:o})=>({x:s,y:i,name:a.name,hidden:!!(a.is_inactive||o),kind:_dirKindOf(a)})),n={};return(_liveDirRoads||[]).forEach(a=>{const s=a.label&&String(a.label).trim();s&&(n[a.id]=s)}),{locations:t,imgW:liveImgW,imgH:liveImgH,isGm:typeof _isGm=="function"?_isGm():!1,roadLabels:n,hasHiddenRoads:(_liveDirRoads||[]).some(a=>a.hidden)}}function _liveDirPortPins(){const e=(liveAllMaps[liveCurrentMapIdx]||{}).id??null;return _liveEntsOnMap(e).filter(t=>t.is_port).map(t=>({x:t.x,y:t.y}))}function _dirNoRouteHtml(e,t){const n=`<span class="live-dir-route">${escHtml(e.from.name)} <span class="live-dir-arrow">&#8594;</span> ${escHtml(e.to.name)}</span>`,a=`<div class="live-dir-actions"><button type="button" class="btn-ghost btn-xs" data-${t}="clear">&#10005; Clear</button></div>`;return`<div class="live-dir-top">${n}${a}</div><span class="live-dir-headline"><span class="live-dir-big">No route</span></span><span class="live-dir-furniture">No charted road connects these two places.</span>`}function _liveDirNoRouteHtml(){return _dirNoRouteHtml(_liveDir,"livedir")}function _liveDirSetBanner(e){const t=_liveDirEl("live-dir-banner");if(!t)return;const n=_liveDir&&_liveDir.from?`<span class="live-dir-chip" title="Start">${escHtml(_liveDir.from.name)}</span>`:"";t.innerHTML=`<span class="live-dir-banner-glyph">&#10547;</span><span class="live-dir-banner-text">${escHtml(e)}</span>${n}<button type="button" class="live-dir-banner-cancel" data-livedir-cancel>Cancel</button>`}function _liveDirShowCard(e){const t=_liveDirEl("live-dir-card");t&&(t.innerHTML=e,t.classList.remove("hidden"),_liveDirEl("live-nav-info")?.classList.add("dir-active"))}(function(){_liveDirEl("live-dir-pill")?.addEventListener("click",()=>_liveDirStart()),_liveDirEl("live-dir-banner")?.addEventListener("click",t=>{t.target.closest("[data-livedir-cancel]")&&_liveDirCancel()}),_liveDirEl("live-dir-card")?.addEventListener("change",t=>{const n=t.target.closest("[data-livedir-usehidden]");n&&_liveDir&&_liveDir.from&&_liveDir.to&&(_liveDir.useHidden=!!n.checked,_liveDir.result=null,_liveDirClearRouteLayers(),_liveDirCompute())}),_liveDirEl("live-dir-card")?.addEventListener("click",t=>{if(t.target.closest("[data-dir-promote-alt]")){_liveDirPromoteAlt();return}const n=t.target.closest("[data-livedir-fear]");if(n){_dhDirAddFear(parseInt(n.getAttribute("data-livedir-fear"),10));return}const a=t.target.closest("[data-livedir-mode],[data-livedir-pace],[data-livedir-watermode]");if(a){if(!_liveDir||!_liveDir.result)return;const o=a.getAttribute("data-livedir-mode"),r=a.getAttribute("data-livedir-pace"),c=a.getAttribute("data-livedir-watermode"),l=currentCampaign&&currentCampaign.id;if(o){_liveDir.mode=o;try{l&&localStorage.setItem("tf_dir_mode_"+l,o)}catch{}}if(c){_liveDir.watermode=c;try{l&&localStorage.setItem("tf_dir_watermode_"+l,c)}catch{}}r&&(_liveDir.pace=r),_liveDirShowCard(_liveDirCardHtml(_liveDir.result,liveAllMaps[liveCurrentMapIdx]||{})),_liveDirDrawCamps(),_liveDirDrawScrubber();return}const s=t.target.closest("[data-livedir]");if(!s)return;const i=s.getAttribute("data-livedir");i==="clear"?_liveDirCancel():i==="reverse"&&_liveDirReverse()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&_liveDir&&_liveDirCancel()})})();const _NB_EYE='<svg class="live-nb-eye" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" role="img" aria-label="Hidden from players"><title>Hidden from players</title><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="2.5"/><line x1="3" y1="21" x2="21" y2="3"/></svg>';
1413function _liveCompass(e,t){const n=t.x-e.x,a=e.y-t.y;if(Math.abs(n)<1e-6&&Math.abs(a)<1e-6)return"";const s=(Math.atan2(n,a)*180/Math.PI%360+360)%360;return["N","NE","E","SE","S","SW","W","NW"][Math.round(s/45)%8]}function _liveResolveOnMap(e,t){const n=typeof _isGm=="function"?_isGm():!1;if(t==null)return e.map_x==null||e.map_y==null||!n&&e.map_pin_hidden?null:{x:e.map_x,y:e.map_y,hidden:!!e.map_pin_hidden};const a=_livePlacements[e.id];if(a&&a[t]){const s=a[t];return!n&&s.hidden?null:{x:s.x,y:s.y,hidden:s.hidden,is_port:!!s.is_port}}return!a&&e.map_id===t&&e.map_x!=null&&e.map_y!=null?!n&&e.map_pin_hidden?null:{x:e.map_x,y:e.map_y,hidden:!!e.map_pin_hidden}:null}function _liveEntsOnMap(e){const t=[];return(allEntities||[]).forEach(n=>{if(n.type!=="location"||_hiddenFromViewer(n))return;const a=_liveResolveOnMap(n,e);a&&t.push({e:n,x:a.x,y:a.y,hidden:a.hidden,is_port:!!a.is_port})}),t}function _liveNearbyMajors(e){const t=window.MapStyle;return _liveEntsOnMap(e).filter(({e:n})=>{const a=t?t.bucketOf(n.ledger&&n.ledger.kind||null):"other";return a==="settlement"||a==="stronghold"||a==="sacred"||!!n.area_map_id}).map(({e:n,x:a,y:s})=>Object.assign({},n,{map_x:a,map_y:s}))}function _liveNearbyCompute(){const e=window.AtlasRoutes;if(!e||!liveMap||!livePartyPos)return[];const t=liveAllMaps[liveCurrentMapIdx]||{},n=t.id??null,a={x:livePartyPos.lng,y:liveImgH-livePartyPos.lat},s=_liveDirSnapToGraph(a),i=_liveDirPortPins(),o=_liveDirDefaultMode(t),r=_liveDirDefaultWaterMode(t),c=_liveNearbyPace,l=_liveDirSnapDist()*.5,d=[];return _liveNearbyMajors(n).forEach(u=>{const p={x:u.map_x,y:u.map_y};if(Math.hypot(p.x-a.x,p.y-a.y)<l)return;const f=_liveDirSnapToGraph(p),g=_liveCompass(a,p),h=e.planRoute(_liveDirRoads,[{key:"_p",x:s.x,y:s.y},{key:"_w",x:f.x,y:f.y}],"_p","_w",["land","water"],{portPins:i});if(h){const y=_liveDirTimeModel(h,t,o,c,r);d.push({e:u,bearing:g,tm:y,off:!1,sort:y?y.totalDays:h.lenPx/1e6});return}const v=e.planRouteFrom(_liveDirRoads,[{key:"_w",x:p.x,y:p.y}],{x:a.x,y:a.y},"_w",["land","water"],{portPins:i});if(!v)return;const b=_liveDirTimeModel(v,t,o,c,r);d.push({e:u,bearing:g,tm:b,off:!0,allCrow:(v.connectorLenPx||0)>=v.lenPx-.5,sort:b?b.totalDays:v.lenPx/1e6})}),d.sort((u,p)=>u.sort-p.sort),d.slice(0,6)}function _liveNearbyDur(e){if(!e)return"";if(e.hasWater){const n=Math.max(1,Math.ceil(e.totalDays));return n<=10?"~"+n+(n===1?" day":" days"):"~"+Math.ceil(n/7)+" wk"}const t=_liveDirDuration(e.landHours,e.landDayH);return t.big+" "+t.unit}function _liveRenderNearby(){const e=document.getElementById("live-nav-nearby");if(!e)return;if(!_isGm()){e.innerHTML="";return}const t=_liveNearbyCompute(),n=liveAllMaps[liveCurrentMapIdx]||{},a=n.scale_unit||"mi",s=n.map_travel||null,i=window.AtlasRoutes,o=_dirIsDh(),r=o?"":'<div class="tf-segctl">'+[["slow","Slow"],["normal","Normal"],["fast","Fast"]].map(([d,u])=>`<button type="button" class="tf-segctl-btn${d===_liveNearbyPace?" active":""}" data-nb-pace="${d}">${u}</button>`).join("")+"</div>",c='<button type="button" class="live-nb-gear" id="live-nav-travel-btn" title="Travel settings: units &amp; how fast each mode travels">&#9881;</button>',l=t.length?t.map(d=>{const u=d.e,p=u.map_pin_hidden?_NB_EYE:"",m=o?"":`<span class="live-nb-days">${_liveNearbyDur(d.tm)}</span>`,f=d.off?`<span class="live-nb-crow">${d.allCrow?"crow-flies":"cross-country"}</span>`:d.tm?`<span class="live-nb-mi">${i.formatDistance(d.tm.totalMi,a,s)}</span>`:"",g=`${m}${f}`;return`<button type="button" class="live-nb-row${d.off?" live-nb-row--crow":""}" data-nb-eid="${u.id}" title="Directions to ${escHtml(u.name)}
1413"><span class="live-nb-compass">${d.bearing}</span><span class="live-nb-name">${escHtml(u.name)}${p}</span><span class="live-nb-right">${g}</span></button>`}).join(""):'<p class="live-nb-empty">No major places within reach.</p>';e.innerHTML=`<div class="lv-pass live-nb"><div class="lv-pass-hdr"><span class="lv-pass-eyebrow">Nearby</span>${r}${c}</div><div class="live-nb-rows">${l}</div></div>`}function _liveNearbyGoTo(e){const t=(allEntities||[]).find(s=>s.id===e);if(!t||!livePartyPos)return;const n=liveAllMaps[liveCurrentMapIdx]||{},a={x:livePartyPos.lng,y:liveImgH-livePartyPos.lat};_liveDirResetState(),_liveDir={from:{key:"_p",x:a.x,y:a.y,name:"Party"},to:{key:t.id,x:t.map_x,y:t.map_y,name:t.name},fromEl:null,toEl:null,result:null,mode:_liveDirDefaultMode(n),watermode:_liveDirDefaultWaterMode(n),pace:_liveNearbyPace},document.getElementById("live-dir-pill")?.classList.add("hidden"),_liveDirCompute()}(function(){const t=document.getElementById("live-nav-nearby");t&&t.addEventListener("click",n=>{if(n.target.closest("#live-nav-travel-btn")){_liveOpenTravel();return}const a=n.target.closest("[data-nb-pace]");if(a){_liveNearbyPace=a.getAttribute("data-nb-pace"),_liveRenderNearby();return}const s=n.target.closest("[data-nb-eid]");s&&_liveNearbyGoTo(parseInt(s.getAttribute("data-nb-eid"),10))})})();function _renderLiveMapSwitcher(){const e=document.getElementById("live-map-prev-map-btn"),t=document.getElementById("live-map-next-map-btn");if(!e||!t)return;liveAllMaps.length>1?(e.classList.remove("hidden"),t.classList.remove("hidden"),e.disabled=!1,t.disabled=!1):(e.classList.add("hidden"),t.classList.add("hidden"))}document.getElementById("live-map-prev-map-btn")?.addEventListener("click",async()=>{liveAllMaps.length>1&&(liveCurrentMapIdx=(liveCurrentMapIdx-1+liveAllMaps.length)%liveAllMaps.length,await initLiveMap(!0))}),document.getElementById("live-map-next-map-btn")?.addEventListener("click",async()=>{liveAllMaps.length>1&&(liveCurrentMapIdx=(liveCurrentMapIdx+1)%liveAllMaps.length,await initLiveMap(!0))});function _renderLiveMapManagerDropdown(){const e=document.getElementById("live-map-manager-dropdown");if(e){if(!_liveAllCampaignMaps.length){e.innerHTML='<p class="placeholder placeholder--padded">No maps uploaded yet.</p>';return}e.innerHTML=_liveAllCampaignMaps.map(t=>`
1414    <label class="live-map-mgr-item">
1415      <input type="checkbox" ${t.visible_live?"checked":""} data-map-id="${t.id}">
1416      <span class="live-map-mgr-name">${escHtml(t.title||"Untitled")}</span>
1417    </label>
1418  `).join(""),e.querySelectorAll('input[type="checkbox"]').forEach(t=>{t.addEventListener("change",async()=>{const n=parseInt(t.dataset.mapId);try{const a=await api("PUT",`/api/campaigns/${currentCampaign.id}/maps/${n}`,{visible_live:t.checked});_liveAllCampaignMaps=_liveAllCampaignMaps.map(s=>s.id===n?{...s,...a}:s),await initLiveMap(),_renderLiveMapManagerDropdown()}catch(a){showToast("Update failed: "+a.message),t.checked=!t.checked}})})}}function _openLiveMapManager(){const e=document.getElementById("live-map-manage-btn"),t=document.getElementById("live-map-panel")?.querySelector(".live-map-panel-hdr");if(!e||!t)return;const n=document.getElementById("live-map-manager-dropdown");if(n){n.remove();return}const a=document.createElement("div");a.id="live-map-manager-dropdown",a.className="live-map-manager-dropdown",t.style.position="relative",t.appendChild(a),_renderLiveMapManagerDropdown(),setTimeout(()=>{document.addEventListener("click",function s(i){const o=document.getElementById("live-map-manager-dropdown");if(!o){document.removeEventListener("click",s);return}!o.contains(i.target)&&i.target!==e&&(o.remove(),document.removeEventListener("click",s))})},0)}document.getElementById("live-map-manage-btn")?.addEventListener("click",e=>{e.stopPropagation(),_openLiveMapManager()});function renderLiveEntryPins(){if(!liveMap)return;liveEntryMarkers.forEach(n=>n.remove()),liveEntryMarkers=[];const e=_isGm(),t=liveAllMaps[liveCurrentMapIdx]?.id??null;liveLogEntries.forEach(n=>{if(n.map_x==null||n.map_y==null||t!==null&&n.map_id!==t)return;const a=n.tag==="key_event",s=L.divIcon({className:"",html:a?'<div class="live-entry-pin key-event">\u2B50</div>':'<div class="live-entry-pin note"></div>',iconSize:[16,16],iconAnchor:[8,8]}),i=liveImgH-n.map_y,o=n.map_x,r=(n.content||"").length>60?n.content.slice(0,60)+"\u2026":n.content,c=L.marker([i,o],{icon:s,interactive:!0,draggable:e}).bindTooltip(r,{direction:"top",offset:[0,-10]});e&&(c.on("dragend",async()=>{const l=c.getLatLng(),d=l.lng,u=liveImgH-l.lat;try{await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${n.session_id}/log-entries/${n.id}`,{map_x:d,map_y:u}),n.map_x=d,n.map_y=u;const p=_pinnedLogEntries.find(m=>m.id===n.id);p&&(p.map_x=d,p.map_y=u)}
1418catch(p){showToast("Move failed: "+p.message)}}),c.on("click",()=>{const l=`lep-label-${n.id}`,d=`<div class="map-popup-inner">
1419          <div class="map-popup-label form-section-label">Edit label</div>
1420          <input id="${l}" class="map-popup-input" value="${escHtml(n.content)}" />
1421          <button class="btn-new map-popup-btn" onclick="window._saveLiveEntryPin(${n.id},${n.session_id},'${l}')">Save</button>
1422          <button class="btn-ghost btn-ghost--danger map-popup-btn" onclick="window._removeLiveEntryPin(${n.id},${n.session_id})">Remove from map</button>
1423        </div>`;c.unbindPopup(),c.bindPopup(d,{maxWidth:230,closeButton:!0}).openPopup()})),c.addTo(liveMap),liveEntryMarkers.push(c)})}window._saveLiveEntryPin=async function(e,t,n){const a=document.getElementById(n);if(!a)return;const s=a.value.trim();if(s)try{await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}/log-entries/${e}`,{content:s});const i=liveLogEntries.find(r=>r.id===e);i&&(i.content=s);const o=_pinnedLogEntries.find(r=>r.id===e);o&&(o.content=s),liveMap?.closePopup(),renderLiveEntryPins(),renderLiveLog(),showToast("Label updated.")}catch(i){showToast("Save failed: "+i.message)}},window._removeLiveEntryPin=async function(e,t){try{await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${t}/log-entries/${e}`,{map_x:null,map_y:null});const n=liveLogEntries.find(a=>a.id===e);n&&(n.map_x=null,n.map_y=null),_pinnedLogEntries=_pinnedLogEntries.filter(a=>a.id!==e),liveMap?.closePopup(),renderLiveEntryPins(),showToast("Pin removed from map.")}catch(n){showToast("Remove failed: "+n.message)}};function renderLiveLog(){const e=document.getElementById("live-log");{const n=new Set;liveLogEntries=liveLogEntries.filter(a=>a&&!n.has(a.id)&&(n.add(a.id),!0))}if(!liveLogEntries.length){e.innerHTML='<p class="placeholder placeholder--padded live-log-empty">No entries yet.</p>';return}const t=liveLogEntries.some(n=>n._remote);e.innerHTML="",[...liveLogEntries].sort((n,a)=>{const s=n.timestamp_seconds!=null,i=a.timestamp_seconds!=null;return s&&i?a.timestamp_seconds-n.timestamp_seconds:s!==i?s?-1:1:a.id-n.id}).forEach(n=>{if(n.tag==="flag"){const g=document.createElement("div");g.className="live-log-entry live-log-entry--flare"+(n._remote?" live-log-entry--remote":""),n._remote&&(n._remote=!1),g.dataset.tag="flag",g.style.setProperty("--tag-color",LIVE_TAG_CFG.flag.color);const h=n.timestamp_seconds!=null?fmtTime(n.timestamp_seconds):"--:--:--",v=_TF_REACT[n.flag_kind]||_TF_REACT.rule_of_cool,b=escHtml(n.author_display_name||n.author||"Someone"),y=(_TF_FLARE_DECL[n.flag_
1423kind]||_TF_FLARE_DECL.rule_of_cool)(b);g.innerHTML=`
1424        <div class="live-log-entry-hdr">
1425          <span class="live-log-time">${h}</span>
1426          <span class="live-log-tag">${LIVE_TAG_CFG.flag.label}</span>
1427          <button class="live-log-delete" data-id="${n.id}" title="Delete">&times;</button>
1428        </div>
1429        <p class="live-log-content live-log-flare-line"><svg class="live-log-flare-glyph" width="16" height="16" aria-hidden="true"><use href="#${v.glyph}"/></svg> ${y}</p>
1430      `,e.appendChild(g);return}const a=LIVE_TAG_CFG[n.tag]||LIVE_TAG_CFG.note,s=n.tag==="system_event"||n.tag==="combat_event"||n.tag==="context_event",i=n.timestamp_seconds!=null?fmtTime(n.timestamp_seconds):"--:--:--",o=document.createElement("div");let r="live-log-entry";n._remote&&(r+=s?" live-log-entry--scan":" live-log-entry--remote"),o.className=r,n._remote&&(n._remote=!1),o.dataset.tag=n.tag;const c=n.tag==="gif"?`<img class="live-log-gif" src="${escHtml(n.content)}" alt="gif" loading="lazy">`:linkifyText(escHtml(n.content).replace(/\n/g,"<br>"),allEntities,null),l=n.reactions||[],d=!s&&n.tag!=="gif"?`
1431      <div class="le-reactions" data-entry-id="${n.id}">
1432        ${l.map(g=>`<button class="le-reaction-btn${liveMyReactions[n.id]&&liveMyReactions[n.id].has(g.emoji)?" mine":""}" data-id="${n.id}" data-emoji="${escHtml(g.emoji)}">${escHtml(g.emoji)} ${g.count}</button>`).join("")}
1433        <button class="le-react-add" data-id="${n.id}" title="Add reaction">\u{1F60A}</button>
1434      </div>`:"",u=n.author_display_name||n.author||"",p=n.author_pc_name?` <span class="le-author-pc">(${escHtml(n.author_pc_name)})</span>`:"",m=!s&&u?`
1435      <div class="le-author-row">
1436        <span class="le-author-avatar">${_avatarHtml(n.author_avatar_url||null,u,20)}</span>
1437        <span class="le-author-name">${escHtml(u)}${p}</span>
1438      </div>`:"",f=s?"":`<button class="live-log-delete" data-id="${n.id}" title="Delete">&times;</button>`;o.style.setProperty("--tag-color",a.color),o.innerHTML=`
1439      <div class="live-log-entry-hdr">
1440        <span class="live-log-time">${i}</span>
1441        <span class="live-log-tag">${a.label}</span>
1442        ${f}
1443      </div>
1444      ${m}
1445      <p class="live-log-content">${c}</p>
1446      ${d}
1447    `,e.appendChild(o)}),t&&(e.classList.remove("live-log--ping"),e.offsetWidth,e.classList.add("live-log--ping"),e.addEventListener("animationend",()=>e.classList.remove("live-log--ping"),{once:!0})),typeof _tfFlareTallyUpdate=="function"&&_tfFlareTallyUpdate()}function renderLiveQuestPanel(){const e=document.getElementById("live-quest-panel-body");if(!e)return;const t=allQuests.filter(o=>o.status==="active"&&!o.is_background);if(!t.length){e.innerHTML='<p class="placeholder placeholder--padded">No active quests.</p>';return}const n=_isGm(),a=o=>{let r=Array.isArray(o.leads)?o.leads:[];if(typeof o.leads=="string")try{r=JSON.parse(o.leads)||[]}catch{r=[]}return r};e.innerHTML=t.map(o=>{const r=a(o),c=r.filter(u=>(u?.status||"open")==="open"),l=r.length-c.length,d=c.length||l?`<div class="lqo-leads">
1448        ${c.map(u=>`<div class="lqo-lead">
1449            ${n?`<button class="lqo-lead-tick" data-qid="${o.id}" data-lead-id="${escHtml(u.id||"")}" title="Mark step done" aria-label="Mark step done">\u2610</button>`:'<span class="lqo-lead-bullet">\u25C7</span>'}
1450            <span class="lqo-lead-text">${escHtml(u.text||"")}</span>
1451          </div>`).join("")}
1452        ${l?`<div class="lqo-leads-done">${l} step${l===1?"":"s"} done</div>`:""}
1453      </div>`:"";return`
1454    <div class="lqo-item" data-id="${o.id}">
1455      <div class="lqo-main">
1456        <span class="lqo-title">${escHtml(o.title)}</span>
1457        ${o.description?`<div class="lqo-desc">${escHtml(o.description)}</div>`:""}
1458        ${d}
1459      </div>
1460      ${n?`<div class="lqo-actions">
1461        <button class="lqo-btn lqo-complete" data-id="${o.id}" title="Mark complete">\u2713</button>
1462        <button class="lqo-btn lqo-fail" data-id="${o.id}" title="Mark failed">\u2717</button>
1463      </div>`:""}
1464    </div>`}).join("");const s=async(o,r,c)=>{const l=allQuests.find(p=>p.id===o);if(!l)return;const d=a(l).map(p=>({...p})),u=d.findIndex(p=>p.id===r);if(!(u<0)){d[u].status=c;try{const p=await api("PUT",`/api/campaigns/${currentCampaign.id}/quests/${o}`,{leads:d});allQuests=allQuests.map(m=>m.id===o?p:m),renderLiveQuestPanel(),renderQuests()}
1464catch(p){showToast("Update failed: "+p.message)}}};e.querySelectorAll(".lqo-lead-tick").forEach(o=>{o.addEventListener("click",async()=>{const r=+o.dataset.qid,c=o.dataset.leadId;await s(r,c,"resolved"),_liveQuestToast("Step done",()=>s(r,c,"open"))})});const i=async(o,r,c)=>{try{const l=await api("PUT",`/api/campaigns/${currentCampaign.id}/quests/${o}`,{status:r});allQuests=allQuests.map(d=>d.id===o?l:d),renderLiveQuestPanel(),renderQuests(),_liveQuestToast(c,async()=>{try{const d=await api("PUT",`/api/campaigns/${currentCampaign.id}/quests/${o}`,{status:"active"});allQuests=allQuests.map(u=>u.id===o?d:u),renderLiveQuestPanel(),renderQuests()}catch(d){showToast("Reopen failed: "+d.message)}})}catch(l){showToast("Update failed: "+l.message)}};e.querySelectorAll(".lqo-complete").forEach(o=>o.addEventListener("click",()=>i(+o.dataset.id,"complete","Quest completed"))),e.querySelectorAll(".lqo-fail").forEach(o=>o.addEventListener("click",()=>i(+o.dataset.id,"failed","Quest marked failed")))}function _liveQuestToast(e,t){typeof window.tfToast=="function"?window.tfToast({type:"success",text:e,action:{label:"Undo",onClick:t},duration:8e3}):showToast(e)}document.getElementById("live-quest-add-btn").addEventListener("click",()=>{openQuestModal(null)});async function _doAddEntry(){if(_isReadOnlyRole())return;if(!liveSession){showToast("Start recording first, click \u23FA Record to create a session.");return}const e=document.getElementById("live-entry-text").value.trim();if(!e){document.getElementById("live-entry-text").focus();return}const t=_isGm()?liveElapsedSeconds||null:_memberElapsedSeconds(),n={content:e,tag:"note",timestamp_seconds:t};try{const a=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries`,n);liveLogEntries.some(s=>s.id===a.id)||liveLogEntries.push(a),delete liveEntriesCache[liveSession.id],document.getElementById("live-entry-text").value="",renderLiveLog()}catch(a){showToast("Add failed: "+a.message)}}const _TF_FLAG_WINDOW_MS=45e3,_TF_FLAG_MAX_CHARS=1100;function _tfFlagWindowText(){if(typeof liveRecording<"u"&&liveRecording&&(liveTranscriptSegments||[]).length){const t=liveTranscriptSegments;let n=0;for(const s of t)s.committedAt!=null&&s.committedAt>n&&(n=s.committedAt);const a=t.filter(s=>s.committedAt==null||n-s.committedAt<=_TF_FLAG_WINDOW_MS).slice(-14).map(s=>`${_resolveSegmentName(s)||"Speaker"}: ${s.text}`);return typeof liveCurrentSegText=="string"&&liveCurrentSegText.trim()&&a.push(liveCurrentSegText.trim()),a.join(`
1465`).slice(-_TF_FLAG_MAX_CHARS)}return typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive&&typeof _tfDiscordChunkLines<"u"&&(_tfDiscordChunkLines||[]).length?_tfDiscordChunkLines.slice(-14).join(`
1466`).slice(-_TF_FLAG_MAX_CHARS):typeof _voicesPolledSrc=="string"&&_voicesPolledSrc.trim()?_voicesPolledSrc.slice(-_TF_FLAG_MAX_CHARS):[...document.querySelectorAll("#watcher-box .watcher-turn")].slice(-14).map(t=>{const n=t.querySelector(".watcher-speaker")?.textContent||"Speaker",a=t.querySelector(".watcher-text")?.textContent||"";return`${n}: ${a}`}).join(`
1467`).slice(-_TF_FLAG_MAX_CHARS)}const _TF_REACT={plot_twist:{label:"Plot Twist",glyph:"tf-glyph-react-twist"},rule_of_cool:{label:"Rule of Cool",glyph:"tf-glyph-react-cool"},clutch_save:{label:"Clutch Save",glyph:"tf-glyph-react-clutch"},epic_fail:{label:"Epic Fail",glyph:"tf-glyph-react-fail"}},_TF_FLARE_DECL={plot_twist:e=>`${e} shot up a flare, "Plot Twist!"`,rule_of_cool:e=>`${e} declares the Rule of Cool!`,clutch_save:e=>`${e} declares a Clutch Save!`,epic_fail:e=>`${e} shot up a flare, "Epic Fail!"`},_TF_REACT_DEBOUNCE_MS=1e4;let _tfLastReactAt={};async function _tfReact(e,t){if(!liveSession||!currentCampaign||!_TF_REACT[e]||_isReadOnlyRole())return;_tfFlagPulse(t);try{navigator.vibrate&&navigator.vibrate(20)}catch{}const n=Date.now();if(n-(_tfLastReactAt[e]||0)<_TF_REACT_DEBOUNCE_MS)return;_tfLastReactAt[e]=n;const a=_tfFlagWindowText(),s=_isGm()?liveElapsedSeconds||null:_memberElapsedSeconds();try{await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries`,{content:a,tag:"flag",flag_kind:e,timestamp_seconds:s})}catch(i){_tfLastReactAt[e]=0,showToast("Reaction failed: "+i.message)}}function _tfFlagPulse(e){e&&(e.classList.remove("tf-flag-pulse"),e.offsetWidth,e.classList.add("tf-flag-pulse"))}let _tfReactActive={};function _tfReactOverlay(){let e=document.getElementById("tf-react-overlay");return e||(e=document.createElement("div"),e.id="tf-react-overlay",e.setAttribute("aria-hidden","true"),document.body.appendChild(e)),e}const _TF_FLARE_COLORS={plot_twist:"#4fb8c0",rule_of_cool:"#7dd8de",clutch_save:"#d4a017",epic_fail:"#e07a3e"};function _tfReactionFlash(e){const t=_TF_REACT[e]||_TF_REACT.rule_of_cool,n=_tfReactOverlay(),a=_tfReactActive[e];if(a&&document.contains(a.el)){a.count+=1;const i=a.el.querySelector(".tf-flare-count");
1467i&&(i.textContent="\xD7"+a.count),clearTimeout(a.timer),a.timer=setTimeout(()=>{a.el.remove(),delete _tfReactActive[e]},3200);return}const s=document.createElement("div");s.className="tf-flare",s.style.left=(20+Math.random()*60).toFixed(1)+"%",s.style.setProperty("--flare-color",_TF_FLARE_COLORS[e]||_TF_FLARE_COLORS.rule_of_cool),s.innerHTML=`<span class="tf-flare-trail"></span><span class="tf-flare-ring"></span><span class="tf-flare-burst"></span><span class="tf-flare-orb"><svg class="tf-flare-glyph" width="22" height="22" aria-hidden="true"><use href="#${t.glyph}"/></svg></span><span class="tf-flare-count"></span>`,n.appendChild(s),_tfReactActive[e]={el:s,count:1,timer:setTimeout(()=>{s.remove(),delete _tfReactActive[e]},3200)}}document.addEventListener("click",e=>{const t=e.target.closest(".tf-react-btn");t&&_tfReact(t.dataset.react,t)});function _tfFlareTallyUpdate(){const e=!!(currentCampaign&&currentCampaign.flare_counter_visible===!0)&&!_isGm(),t={};if(e)for(const n of liveLogEntries||[])n&&n.tag==="flag"&&n.flag_kind&&(t[n.flag_kind]=(t[n.flag_kind]||0)+1);document.querySelectorAll(".tf-react-btn").forEach(n=>{let a=n.querySelector(".tf-react-btn-count");const s=t[n.dataset.react]||0;if(!e||!s){a&&a.remove();return}a||(a=document.createElement("span"),a.className="tf-react-btn-count",n.appendChild(a)),a.textContent="\xD7"+s})}function _tfReactUpdateEnabled(){const e=typeof liveRecording<"u"&&liveRecording||typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive||typeof _watcherIsLive<"u"&&_watcherIsLive;document.querySelectorAll(".tf-react-bar").forEach(t=>t.classList.toggle("is-idle",!e))}function _tfAlignReactBar(){const e=document.querySelector(".tf-react-bar--header"),t=document.getElementById("live-recorder"),n=document.querySelector(".live-header"),a=document.getElementById("live-session-screen");if(!e||!t||!n||!a||a.classList.contains("hidden"))return;const s=t.getBoundingClientRect();if(!s.width)return;const i=n.getBoundingClientRect();e.style.setProperty("--tf-react-left",Math.round(s.left+s.width/2-i.left)+"px")}(function(){const t=()=>{setTimeout(()=>{try{_tfAlignReactBar(),_tfReactUpdateEnabled()}catch{}},60),setTimeout(()=>{try{_tfAlignReactBar(),_tfReactUpdateEnabled()}catch{}},500)};document.addEventListener("click",a=>{a.target.closest("#live-ai-expander, #live-ai-drawer-collapse-btn, .live-ai-collapse-btn, .live-collapse-btn, .live-panel-toggle, #live-pc-sidebar-expander")&&t()});try{const a=new MutationObserver(()=>{try{_tfReactUpdateEnabled()}catch{}});["live-header-rec-dot","live-broadcast-banner"].forEach(s=>{const i=document.getElementById(s);i&&a.observe(i,{attributes:!0,attributeFilter:["class"]})})}catch{}try{const a=new ResizeObserver(t);["live-recorder","live-ai-drawer","live-pc-sidebar"].forEach(s=>{const i=document.getElementById(s);i&&a.observe(i)})}catch{}const n=document.getElementById("live-session-screen");if(n)try{new MutationObserver(t).observe(n,{attributes:!0,attributeFilter:["class"]})}catch{}window.addEventListener("resize",t)})(),document.getElementById("live-entry-text").addEventListener("focus",()=>{_liveLogTyping=!0}),document.getElementById("live-entry-text").addEventListener("blur",()=>{_liveLogTyping=!1});const _LIVE_LOG_EMOJIS=["\u{1F44D}","\u{1F44E}","\u2764\uFE0F","\u{1F602}","\u{1F62E}","\u{1F631}","\u{1F480}","\u{1F525}","\u2694\uFE0F","\u{1F6E1}\uFE0F","\u{1F3B2}","\u{1F409}","\u{1F9D9}","\u{1F4B0}","\u{1F31F}","\u{1F608}","\u{1F389}","\u{1F44F}","\u{1F914}","\u{1F923}"];document.getElementById("live-log").addEventListener("click",async e=>{if(_isReadOnlyRole())return;const t=e.target.closest(".live-log-delete");if(t){const s=parseInt(t.dataset.id);try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries/${s}`),liveLogEntries=liveLogEntries.filter(i=>i.id!==s),renderLiveLog()}catch(i){showToast("Delete failed: "+i.message)}return}const n=e.target.closest(".le-reaction-btn");if(n){if(!liveSession)return;const s=parseInt(n.dataset.id),i=n.dataset.emoji,o=liveMyReactions[s]=liveMyReactions[s]||new Set;try{let r;o.has(i)?(o.delete(i),r=await api("DELETE",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries/${s}/reactions/${encodeURIComponent(i)}`)):(o.add(i),r=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries/${s}/reactions`,{emoji:i}));const c=liveLogEntries.find(l=>l.id===s);c&&(c.reactions=r.reactions),renderLiveLog()}catch(r){showToast("Reaction failed: "+r.message)}return}const a=e.target.closest(".le-react-add");if(a){e.stopPropagation(),document.querySelectorAll(".le-emoji-picker").forEach(o=>o.remove());const s=parseInt(a.dataset.id),i=document.createElement("div");i.className="le-emoji-picker",i.style.position="absolute",_LIVE_LOG_EMOJIS.forEach(o=>{const r=document.createElement("button");r.type="button",r.textContent=o,r.addEventListener("click",async c=>{if(c.stopPropagation(),i.remove(),!liveSession)return;(liveMyReactions[s]=liveMyReactions[s]||new Set).add(o);try{const d=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries/${s}/reactions`,{emoji:o}),u=liveLogEntries.find(p=>p.id===s);u&&(u.reactions=d.reactions),renderLiveLog()}catch(d){showToast("Reaction failed: "+d.message)}}),i.appendChild(r)}),a.parentNode.style.position="relative",a.parentNode.appendChild(i),setTimeout(()=>{document.addEventListener("click",function o(){i.remove(),document.removeEventListener("click",o)},{once:!0})},0)}}),document.getElementById("live-entry-text").addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),_doAddEntry())});let _gifSearchTimer=null;function _closeGifPicker(){document.getElementById("live-gif-picker").classList.add("hidden")}document.getElementById("live-gif-btn").addEventListener("click",e=>{e.stopPropagation();const t=document.getElementById("live-gif-picker");if(!t.classList.contains("hidden")){_closeGifPicker();return}t.classList.remove("hidden"),document.getElementById("live-gif-search").focus(),document.getElementById("live-gif-results").children.length||_runGifSearch("fantasy dungeon")}),document.addEventListener("click",e=>{const t=document.getElementById("live-gif-picker");!t||t.classList.contains("hidden")||!t.contains(e.target)&&e.target.id!=="live-gif-btn"&&_closeGifPicker()}),document.getElementById("live-tab-transcript-btn")?.addEventListener("click",()=>_switchLiveTransTab("transcript")),document.getElementById("live-preamble-close")?.addEventListener("click",()=>{if(document.getElementById("live-preamble")?.classList.add("hidden"),liveSession&&liveSession.id)try{localStorage.setItem(_notebookRecapDismissKey(liveSession.id),"1")}catch{}typeof _renderNotebookRecap=="function"&&_renderNotebookRecap()}),document.getElementById("live-tab-speakers-btn")?.addEventListener("click",()=>_switchLiveTransTab("speakers")),document.getElementById("live-transcript-toggle")?.addEventListener("click",()=>{const e=document.getElementById("live-transcript-panel"),t=document.getElementById("live-transcript-toggle");if(!e)return;const n=e.classList.toggle("transcript-collapsed");t&&(t.title=n?"Expand transcript":"Collapse transcript")}),document.getElementById("live-transcript-focus-btn")?.addEventListener("click",()=>{const e=document.querySelector(".live-body");if(!e)return;const t=e.classList.toggle("live-focus"),n=document.getElementById("live-transcript-focus-btn");n&&(n.classList.toggle("live-collapse-btn--active",t),n.title=t?"Exit focus mode":"Focus mode: hide side panels, expand the transcript")}),document.getElementById("live-gif-search").addEventListener("input",e=>{clearTimeout(_gifSearchTimer);const t=e.target.value.trim();t&&(_gifSearchTimer=setTimeout(()=>_runGifSearch(t),400))});async function _runGifSearch(e){const t=document.getElementById("live-gif-results");t.innerHTML='<p class="placeholder">Searching\u2026</p>';try{const n=await api("GET",`/api/gif-search?q=${encodeURIComponent(e)}`);
1467if(!n.gifs.length){t.innerHTML='<p class="placeholder">No results.</p>';return}t.innerHTML="",n.gifs.forEach(a=>{const s=document.createElement("img");s.src=a.preview,s.alt=a.title,s.title=a.title,s.className="live-gif-thumb",s.loading="lazy",s.addEventListener("click",async()=>{if(!liveSession){showToast("No active session.");return}document.getElementById("live-gif-picker").classList.add("hidden");try{const i=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/log-entries`,{content:a.url,tag:"gif",timestamp_seconds:_isGm()?liveElapsedSeconds||null:_memberElapsedSeconds()});liveLogEntries.some(o=>o.id===i.id)||liveLogEntries.unshift(i),renderLiveLog()}catch(i){showToast("Add GIF failed: "+i.message)}}),t.appendChild(s)})}catch(n){t.innerHTML=`<p style="color:var(--accent-red);font-size:0.78rem;padding:0.5rem">${n.message}</p>`}}document.getElementById("live-back-btn").addEventListener("click",()=>{liveSession?_minimizeLiveSession():closeLiveSession()});const _LATE_NIGHT_CUTOFF_HOUR=5;function _isContinuationDefault(e){if(!e||!e.date)return!1;const t=e.date.slice(0,10),n=new Date,a=i=>`${i.getFullYear()}-${String(i.getMonth()+1).padStart(2,"0")}-${String(i.getDate()).padStart(2,"0")}`;if(t===a(n))return!0;const s=new Date(n);return s.setDate(s.getDate()-1),t===a(s)&&n.getHours()<_LATE_NIGHT_CUTOFF_HOUR}document.getElementById("live-nav-btn").addEventListener("click",()=>{if(currentUser&&currentUser.verified===!1){_showVerifyWall();return}const e=allSessions.find(a=>!a.is_session_zero)??allSessions[0]??null,t=_isContinuationDefault(e)?e:null;liveSession&&t&&liveSession.id===t.id&&(_liveMinimized||liveRecording)?_returnToLiveSession():openLiveSession(t),applyRoleUI()});function _getSpeakerLabelPool(){return["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]}function _aaiLetterToIdx(e,t){return e.charCodeAt(0)-65+(typeof t=="number"?t:_liveSpeakerIdxBase)}function _takeCurTurnOrders(){const e=_liveCurTurnOrders;return _liveCurTurnOrders=[],e}let liveSpeakerNameMap={},liveSpeakerQuotes={};const _SPK_MIN_WORDS=6,_SPK_MAX_CANDIDATES=5,_SPK_DISPLAY_CAP=120;let _liveSpkBarLastTokens="",_liveInterimLast="",liveWs=null,livePcmNode=null,liveWsSessionComplete=!1,_liveAaiBilledSeconds=0,_liveAaiOpenAtMs=0,_liveAaiEstimatedSeconds=0;function _liveAaiSettleUnreported(){_liveAaiOpenAtMs>0&&(_liveAaiEstimatedSeconds+=Math.max(0,(Date.now()-_liveAaiOpenAtMs)/1e3),_liveAaiOpenAtMs=0)}let _liveAaiVendorModel=null,_usingAAI=!1,_aaiConnectRetries=0;const _AAI_MAX_CONNECT_RETRIES=2,_AAI_CYCLE_AT_MS=175*60*1e3;let liveWsRetiring=null,_aaiCycleTimer=null,_aaiCycling=!1,_aaiCyclePendingWs=null,_aaiCycleBeginTimeout=null;function _aaiCycleAtMs(){let e=null;try{e=localStorage.getItem("tf_aai_cycle_ms")}catch{}if(e==null||e==="")return _AAI_CYCLE_AT_MS;const t=parseInt(e,10);return t===0?null:Number.isFinite(t)&&t>=3e4?t:_AAI_CYCLE_AT_MS}function _storeQuoteIfNew(e,t){const n=(t||"").trim();if((n?n.split(/\s+/).filter(Boolean):[]).length<_SPK_MIN_WORDS)return;const s=liveSpeakerQuotes[e]||[];s.some(i=>i===n)||(s.push(n),s.sort((i,o)=>o.split(/\s+/).length-i.split(/\s+/).length),liveSpeakerQuotes[e]=s.slice(0,_SPK_MAX_CANDIDATES))}function _resolveSegText(){const e=liveCurrentSegText.trim().split(/\s+/).filter(Boolean).length;return _liveInterimLast.trim().split(/\s+/).filter(Boolean).length>e+5?_punctuate(_liveInterimLast):liveCurrentSegText}function _punctuate(e){return e=e.trim(),e?(e=e[0].toUpperCase()+e.slice(1),/[.!?,;]$/.test(e)||(e+="."),e):""}function _downsampleToInt16(e,t,n){const a=t/n,s=Math.floor(e.length/a),i=new Int16Array(s);for(let o=0;o<s;o++){const r=o*a,c=Math.floor(r),l=Math.min(c+1,e.length-1),d=r-c,u=e[c]*(1-d)+e[l]*d;i[o]=Math.max(-32768,Math.min(32767,Math.round(u*32767)))}return i.buffer}function _startPcmCapture(){if(!liveAnalyser||livePcmNode)return;const e=liveAnalyser.context,t=e.sampleRate;livePcmNode=e.createScriptProcessor(4096,1,1),liveAnalyser.connect(livePcmNode),livePcmNode.connect(e.destination),livePcmNode.onaudioprocess=n=>{liveWs?.readyState===WebSocket.OPEN&&liveWs.send(_downsampleToInt16(n.inputBuffer.getChannelData(0),t,16e3))}}function _stopPcmCapture(){if(livePcmNode){try{liveAnalyser?.disconnect(livePcmNode)}catch{}try{livePcmNode.disconnect()}catch{}livePcmNode.onaudioprocess=null,livePcmNode=null}}function _splitTurnByWordSpeaker(e){const t=e.speaker_label||"A",n=[{speakerLetter:t,text:(e.transcript||"").trim()}];
1467if(!Array.isArray(e.words)||!e.words.length)return n;const a=[];let s=null,i=e.speaker_label&&e.speaker_label!=="UNKNOWN"?e.speaker_label:null;for(const r of e.words){if(!r||!r.word_is_final)continue;const c=(r.text||"").trim();if(!c)continue;let l=r.speaker;(!l||l==="UNKNOWN")&&(l=i),l||(l=t),i=l,s&&s.speakerLetter===l?s.text+=" "+c:(s={speakerLetter:l,text:c},a.push(s))}if(!a.length)return n;const o=a.map(r=>({...r,text:r.text.trim()})).filter(r=>r.text&&r.text.split(/\s+/).filter(Boolean).length>=3);return o.length<2?n:o}function _handleAaiMessage(e,t){switch(e.type){case"Begin":console.log("[AAI] Session ready id="+e.id);try{e.configuration&&e.configuration.model&&(_liveAaiVendorModel=String(e.configuration.model))}catch{}_liveAaiSettleUnreported(),_liveAaiOpenAtMs=Date.now(),_armAaiCycleTimer();break;case"Turn":{const n=(e.transcript||"").trim();if(!n)break;const a=t||liveWs;if(a&&!a._tfFirstTurnLogged&&(a._tfFirstTurnLogged=!0,_liveTel("aai_first_turn",{turn_order:e.turn_order,genBase:a._tfGenBase})),!e.end_of_turn){updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,n);break}const s=_splitTurnByWordSpeaker(e);if(s.length>1){if(liveCurrentSegHas){const r=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,r),liveTranscriptSegments.push({text:r,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime,turnOrders:_takeCurTurnOrders(),genBase:_liveSpeakerIdxBase}),_broadcastChunk(r,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast=""}clearTimeout(liveSegmentIdleTimer);for(const r of s){const c=r.speakerLetter==="UNKNOWN"?liveSpeakerIdx:_aaiLetterToIdx(r.speakerLetter);_storeQuoteIfNew(c,r.text),liveTranscriptSegments.push({text:r.text,speakerIdx:c,committedAt:Date.now()-liveRecordingStartTime,turnOrders:e.turn_order!=null?[e.turn_order]:[],genBase:_liveSpeakerIdxBase}),_broadcastChunk(r.text,_liveSpeakerLabel(c)),liveSpeakerIdx=c}_wdLastTranscriptMs=Date.now(),liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),_dhFearObserve(n,"aai"),updateTranscriptUI(liveTranscriptSegments,"","");break}const i=e.speaker_label||"A",o=i==="UNKNOWN"?liveSpeakerIdx:_aaiLetterToIdx(i);o!==liveSpeakerIdx&&liveCurrentSegHas?_commitSegAndSwitch(o):o!==liveSpeakerIdx&&(liveSpeakerIdx=o),liveCurrentSegText+=(liveCurrentSegText?" ":"")+n,liveCurrentSegHas=!0,e.turn_order!=null&&_liveCurTurnOrders.push(e.turn_order),_wdLastTranscriptMs=Date.now(),liveTranscriptAccum=_buildLiveTranscriptAccum()+(liveCurrentSegText?(liveTranscriptSegments.length?`
1468`:"")+liveCurrentSegText:""),scheduleSaveTranscript(),_dhFearObserve(n,"aai"),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""),clearTimeout(liveSegmentIdleTimer),liveSegmentIdleTimer=setTimeout(()=>{if(!liveCurrentSegHas)return;const r=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,r),liveTranscriptSegments.push({text:r,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime,turnOrders:_takeCurTurnOrders(),genBase:_liveSpeakerIdxBase}),_broadcastChunk(r,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast="",liveTranscriptAccum=_buildLiveTranscriptAccum(),updateTranscriptUI(liveTranscriptSegments,"","")},2500);break}case"SpeakerRevision":{const n=t&&typeof t._tfGenBase=="number"?t._tfGenBase:_liveSpeakerIdxBase,a=Array.isArray(e.revisions)?e.revisions.length:0,s=_applyAaiSpeakerRevision(e.revisions,n);_liveTel("aai_speaker_revision",{turns:a,applied:s,genBase:n}),s>0&&(liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""));break}case"Termination":if(console.log("[AAI] Terminated \u2014 audio:",e.audio_duration_seconds+"s"),_liveAaiBilledSeconds+=Number(e.session_duration_seconds)||0,_liveAaiOpenAtMs=0,liveWsSessionComplete=!0,liveCurrentSegHas){const n=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,n),liveTranscriptSegments.push({text:n,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime,turnOrders:_takeCurTurnOrders(),genBase:_liveSpeakerIdxB
1468ase}),_broadcastChunk(n,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast="",liveTranscriptAccum=_buildLiveTranscriptAccum(),updateTranscriptUI(liveTranscriptSegments,"","")}break}}function _applyAaiSpeakerRevision(e,t){const n=Array.isArray(e)?e:[],a=new Map;for(const i of n)!i||i.turn_order==null||!i.speaker_label||i.speaker_label==="UNKNOWN"||a.set(i.turn_order,i.speaker_label);if(!a.size)return 0;let s=0;for(const i of liveTranscriptSegments){if(!i||i.genBase!==t||!Array.isArray(i.turnOrders))continue;const o=[...new Set(i.turnOrders.filter(c=>a.has(c)).map(c=>a.get(c)))];if(o.length!==1||liveSpeakerNameMap[i.speakerIdx]!=null||i.nameOverride)continue;const r=_aaiLetterToIdx(o[0],t);r!==i.speakerIdx&&(i.speakerIdx=r,s++)}return s}function _waitForAaiTermination(){return new Promise(e=>{if(!liveWs||liveWs.readyState!==WebSocket.OPEN||liveWsSessionComplete){e();return}const t=()=>{clearTimeout(n),clearInterval(a),e()},n=setTimeout(t,3e3),a=setInterval(()=>{liveWsSessionComplete&&t()},100),s=liveWs.onclose;liveWs.onclose=i=>{t(),s&&s(i)};try{liveWs.send(JSON.stringify({type:"Terminate"}))}catch{}})}const _DH_KEYTERMS=["Duality Dice","roll with Hope","roll with Fear","with Fear","with Hope","spend a Fear","gain a Fear","Fear pool","Hope","Fear","Stress","Evasion","damage threshold","Armor Slot","Armor Score","domain card","Experience","adversary","spotlight","countdown","death move","Blaze of Glory","Avoid Death","Risk It All","Scar","Tend to Wounds","Repair Armor","Duality","Clank","Drakona","Faun","Firbolg","Fungril","Galapa","Infernis","Katari","Ribbet","Simiah","Highborne","Loreborne","Orderborne","Ridgeborne","Seaborne","Slyborne","Underborne","Wanderborne","Wildborne","Seraph","Splendor","Valor","Sage domain","Midnight domain"];function _buildKeyterms(){const n=new Set,a=[];function s(p){if(!p||typeof p!="string")return;const m=p.split(",")[0].trim();!m||m.length>50||n.has(m.toLowerCase())||(n.add(m.toLowerCase()),a.push(m))}function i(p){s(p.name),Array.isArray(p.aliases)&&p.aliases.forEach(m=>s(m))}function o(p){if(!p)return;(Array.isArray(p)?p.join(","):String(p)).split(",").forEach(f=>s(f.trim()))}const r=allEntities.filter(p=>p.type==="pc");r.forEach(p=>i(p)),r.forEach(p=>o(p.key_spells)),typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()&&r.forEach(p=>{const m=_dhSheet(p);m&&((m.domain_cards||[]).forEach(f=>s(String(f))),(m.experiences||[]).forEach(f=>s(f&&typeof f=="object"?f.name:String(f))))}),typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()&&_DH_KEYTERMS.forEach(s);const c=new Set(["npc","location","faction","companion"]);allEntities.filter(p=>c.has(p.type)).sort((p,m)=>(m.last_seen_session_id||0)-(p.last_seen_session_id||0)).forEach(p=>{a.length<1e3&&i(p)});const d=new Set(["item","deity","lore"]);return allEntities.filter(p=>d.has(p.type)).sort((p,m)=>(m.last_seen_session_id||0)-(p.last_seen_session_id||0)).forEach(p=>{a.length<1e3&&i(p)}),a.slice(0,1e3)}function _buildAaiWsUrl(e){const t=_buildKeyterms().slice(0,100),n=t.length?"&keyterms_prompt="+encodeURIComponent(JSON.stringify(t)):"",a=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()?"This is a recording of a Daggerheart tabletop roleplaying session. A game master and several players speak, often acting in character with dramatic, exaggerated voices. Expect fantasy names of characters, places, and adversaries, Duality Dice rolls with Hope and Fear, Stress and damage thresholds, domain cards, and rules discussion.":"This is a recording of a Dungeons and Dragons tabletop roleplaying session. A game master and several players speak, often acting in character with dramatic, exaggerated voices. Expect fantasy names of characters, places, monsters, and spells, along with dice rolls and rules discussion.",s="&prompt="+encodeURIComponent(a),i=allEntities.filter(r=>r.type==="pc"&&!r.is_dead&&!r.is_inactive).length,o=Math.max(2,Math.min(10,i+3));return`wss://streaming.assemblyai.com/v3/ws?token=${e}&speech_model=universal-3-5-pro&sample_rate=16000&speaker_labels=true&max_speakers=${o}&format_turns=true&min_turn_silence=400`+s+n}function _startAssemblyAI(e){_usingAAI=!0,liveWs=new WebSocket(_buildAaiWsUrl(e)),liveWs._tfGenBase=_liveSpeakerIdxB
1468ase,_attachPrimaryAaiHandlers(liveWs)}function _attachPrimaryAaiHandlers(e){e.onopen=()=>{_liveTel("aai_ws_open"),console.log("[AAI] Universal Streaming connected"),_aaiConnectRetries=0,_startPcmCapture()},e.onmessage=t=>{let n;try{n=JSON.parse(t.data)}catch{return}_handleAaiMessage(n,e)},e.onclose=t=>{if(_liveTel("aai_ws_closed",{code:t.code,reason:t.reason}),console.log("[AAI] Closed",t.code,t.reason),_aaiCycling){_liveTel("aai_ws_closed_during_cycle",{code:t.code});return}_stopPcmCapture(),_usingAAI&&liveMediaRecorder?.state==="recording"&&t.code!==1e3&&(_showCaptureAlert("Transcription connection lost, attempting to reconnect\u2026","warning","Code "+t.code),setTimeout(()=>{_usingAAI&&liveMediaRecorder?.state==="recording"&&_attemptAaiReconnect()},1500))},e.onerror=t=>{if(_liveTel("aai_ws_error"),console.warn("[AAI] WebSocket error",t?.message||""),_aaiCycling){_liveTel("aai_ws_error_during_cycle");return}if(_stopPcmCapture(),liveWs){try{liveWs.onclose=null,liveWs.close()}catch{}liveWs=null}if(_usingAAI&&liveMediaRecorder?.state==="recording"&&_aaiConnectRetries<_AAI_MAX_CONNECT_RETRIES){_aaiConnectRetries++,_liveTel("aai_connect_retry",{attempt:_aaiConnectRetries}),_showCaptureAlert("Reconnecting live transcription\u2026","warning","Attempt "+_aaiConnectRetries+" of "+_AAI_MAX_CONNECT_RETRIES+". Audio is still recording."),setTimeout(()=>{_usingAAI&&liveMediaRecorder?.state==="recording"&&_retryAaiConnect()},1200);return}_fallbackToWebSpeechWithNotice()}}function _armAaiCycleTimer(){clearTimeout(_aaiCycleTimer);const e=_aaiCycleAtMs();e&&(_aaiCycleTimer=setTimeout(_cycleAaiSession,e))}async function _cycleAaiSession(){if(!_usingAAI||liveMediaRecorder?.state!=="recording"||_aaiCycling||liveWsRetiring)return;_aaiCycling=!0,_liveTel("aai_session_cycle_begin",{fromBase:_liveSpeakerIdxBase});let t=null;try{const s=await Promise.race([api("GET",_liveTokenUrl()),new Promise((i,o)=>setTimeout(()=>o(new Error("token fetch timeout")),1e4))]);t=s&&s.token}catch(s){_aaiCycling=!1,_liveTel("aai_cycle_abandoned",{stage:"token",error:s&&s.message}),_tfLiveCapNotice(s),_aaiCycleRecoverIfDead();return}if(!t||!_usingAAI||liveMediaRecorder?.state!=="recording"){_aaiCycling=!1;return}let n=null;try{n=new WebSocket(_buildAaiWsUrl(t))}catch(s){_aaiCycling=!1,_liveTel("aai_cycle_abandoned",{stage:"ws_construct",error:s&&s.message}),_aaiCycleRecoverIfDead();return}_aaiCyclePendingWs=n;const a=(s,i)=>{if(_aaiCyclePendingWs===n){_aaiCyclePendingWs=null,clearTimeout(_aaiCycleBeginTimeout);try{n.onmessage=null,n.onerror=null,n.onclose=null,n.close()}catch{}_aaiCycling=!1,_liveTel("aai_cycle_abandoned",{stage:s,...i||{}}),_aaiCycleRecoverIfDead()}};n.onerror=()=>a("ws_error"),n.onclose=s=>a("ws_closed",{code:s.code}),clearTimeout(_aaiCycleBeginTimeout),_aaiCycleBeginTimeout=setTimeout(()=>a("begin_timeout"),8e3),n.onmessage=s=>{let i;try{i=JSON.parse(s.data)}catch{return}i.type==="Begin"&&_activateCycledAaiSession(n)}}function _aaiCycleRecoverIfDead(){_usingAAI&&liveMediaRecorder?.state==="recording"&&(!liveWs||liveWs.readyState!==WebSocket.OPEN)&&_attemptAaiReconnect()}function _activateCycledAaiSession(e){if(_aaiCyclePendingWs!==e)return;if(_aaiCyclePendingWs=null,clearTimeout(_aaiCycleBeginTimeout),!_usingAAI||liveMediaRecorder?.state!=="recording"){try{e.close()}catch{}_aaiCycling=!1;return}const t=_liveSpeakerIdxBase;if(clearTimeout(liveSegmentIdleTimer),liveCurrentSegHas){const a=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,a),liveTranscriptSegments.push({text:a,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime,turnOrders:_takeCurTurnOrders(),genBase:t}),_broadcastChunk(a,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast=""}else _takeCurTurnOrders();const n=liveWs;if(n&&(liveWsRetiring=n,n._tfInsertAt=liveTranscriptSegments.length,_attachRetiringAaiHandlers(n)),_liveSpeakerIdxBase+=_getSpeakerLabelPool().length,e._tfGenBase=_liveSpeakerIdxBase,liveWs=e,liveWsSessionComplete=!1,_liveAaiOpenAtMs=Date.now(),_attachPrimaryAaiHandlers(e),_startPcmCapture(),n){try{n.send(JSON.stringify({type:"Terminate"}))}catch{}n._tfForceCloseTimer=setTimeout(()=>_closeRetiringAai("drain_timeout"),5e3)}_aaiCycling=!1,_armAaiCycleTimer(),_liveTel("aai_session_cycle_done",{fromBase:t,toBase:_liveSpeakerIdxB
1468ase})}function _attachRetiringAaiHandlers(e){e.onerror=()=>{},e.onclose=()=>{clearTimeout(e._tfForceCloseTimer),liveWsRetiring===e&&(liveWsRetiring=null),_liveTel("aai_retiring_closed",{drained:!!e._tfDrained})},e.onmessage=t=>{let n;try{n=JSON.parse(t.data)}catch{return}_handleRetiringAaiMessage(n,e)}}function _insertRetiringSegment(e,t){const n=Math.min(typeof e._tfInsertAt=="number"?e._tfInsertAt:liveTranscriptSegments.length,liveTranscriptSegments.length);liveTranscriptSegments.splice(n,0,t),e._tfInsertAt=n+1}function _handleRetiringAaiMessage(e,t){switch(e.type){case"Turn":{if(!liveRecording||!e.end_of_turn||!(e.transcript||"").trim())break;const a=_splitTurnByWordSpeaker(e);for(const s of a){const i=s.speakerLetter==="UNKNOWN"?"A":s.speakerLetter,o=_aaiLetterToIdx(i,t._tfGenBase);_storeQuoteIfNew(o,s.text),_insertRetiringSegment(t,{text:s.text,speakerIdx:o,committedAt:Date.now()-liveRecordingStartTime,turnOrders:e.turn_order!=null?[e.turn_order]:[],genBase:t._tfGenBase}),_broadcastChunk(s.text,_liveSpeakerLabel(o))}document.querySelectorAll(".spk-reassign-pop").forEach(s=>s.remove()),liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,"");break}case"SpeakerRevision":{const n=typeof t._tfGenBase=="number"?t._tfGenBase:0,a=Array.isArray(e.revisions)?e.revisions.length:0,s=_applyAaiSpeakerRevision(e.revisions,n);_liveTel("aai_speaker_revision",{turns:a,applied:s,genBase:n,retiring:!0}),s>0&&liveRecording&&(liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""));break}case"Termination":_liveAaiBilledSeconds+=Number(e.session_duration_seconds)||0,t._tfDrained=!0,_liveTel("aai_retiring_drained"),_closeRetiringAai("drained");break}}function _closeRetiringAai(e){const t=liveWsRetiring;if(t){liveWsRetiring=null,clearTimeout(t._tfForceCloseTimer);try{t.onmessage=null,t.onerror=null,t.onclose=null,t.close()}catch{}e!=="drained"&&_liveTel("aai_retiring_force_closed",{reason:e})}}function _waitForRetiringDrain(){return new Promise(e=>{const t=liveWsRetiring;if(!t||t._tfDrained||t.readyState===WebSocket.CLOSED){e();return}const n=()=>{clearTimeout(a),clearInterval(s),e()},a=setTimeout(n,2e3),s=setInterval(()=>{(!liveWsRetiring||t._tfDrained)&&n()},100)})}function _abortAaiCycle(e){if(clearTimeout(_aaiCycleTimer),_aaiCycleTimer=null,clearTimeout(_aaiCycleBeginTimeout),_aaiCycleBeginTimeout=null,_aaiCyclePendingWs){const t=_aaiCyclePendingWs;_aaiCyclePendingWs=null;try{t.onmessage=null,t.onerror=null,t.onclose=null,t.close()}catch{}_liveTel("aai_cycle_abandoned",{stage:"abort",reason:e})}_aaiCycling=!1,_closeRetiringAai(e)}async function _retryAaiConnect(){try{const e=await api("GET",_liveTokenUrl());if(_stopPcmCapture(),liveWs){try{liveWs.onclose=null,liveWs.close()}catch{}liveWs=null}_startAssemblyAI(e.token)}catch(e){_liveTel("aai_connect_retry_failed",{error:e.message}),_tfLiveCapNotice(e),_fallbackToWebSpeechWithNotice()}}function _fallbackToWebSpeechWithNotice(){if(_usingAAI=!1,_abortAaiCycle("webspeech_fallback"),liveWs){try{liveWs.onclose=null,liveWs.close()}catch{}liveWs=null}_stopPcmCapture(),_startChromeWebSpeech(),_showCaptureAlert("Live transcription couldn't reach our service, usually a firewall, VPN, or network block.","warning","Switched to lower-quality browser transcription; your audio is still recording. For the full-quality transcript, try a different network or turn off any VPN, then start a new recording.")}function _startChromeWebSpeech(){const e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e){showToast("Live transcript preview unavailable in this browser, audio still recording for transcription.");return}liveSpeechRec=new e,liveSpeechRec.continuous=!0,liveSpeechRec.interimResults=!0,liveSpeechRec.lang="en-US",liveSpeechRec.profanityFilter=localStorage.getItem("tf_profanity_filter")==="true",liveSpeechRec.onresult=t=>{let n="";for(let a=t.resultIndex;a<t.results.length;a++)if(t.results[a].isFinal){const s=_punctuate(t.results[a][0].transcript);liveCurrentSegText+=(liveCurrentSegText?" ":"")+s,liveCurrentSegHas=!0,_wdLastTranscriptMs=Date.now(),liveTranscriptAccum=[...liveTranscriptSegments.map(i=>i.text),liveCurrentSegText].join(`
1469
1470`),scheduleSaveTranscript()}else n+=t.results[a][0].transcript;n.trim()&&(_liveInterimLast=n),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,n),clearTimeout(liveSegmentIdleTimer),liveCurrentSegHas&&(liveSegmentIdleTimer=setTimeout(()=>{if(!liveCurrentSegHas)return;const a=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,a),liveTranscriptSegments.push({text:a,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime}),_broadcastChunk(a,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast="",liveTranscriptAccum=_buildLiveTranscriptAccum(),updateTranscriptUI(liveTranscriptSegments,"","")},2500))},liveSpeechRec.onend=()=>
1470{if(liveRecording&&liveMediaRecorder&&liveMediaRecorder.state!=="inactive"){try{liveSpeechRec.start()}catch{}return}if(liveCurrentSegHas){const t=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,t),liveTranscriptSegments.push({text:t,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime}),_broadcastChunk(t,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast="",liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),updateTranscriptUI(liveTranscriptSegments,"","")}},liveSpeechRec.onerror=t=>{t.error==="not-allowed"?(showToast("Mic access denied: live preview unavailable; audio still recording for transcription."),liveRecording=!1):t.error==="audio-capture"?showToast("Microphone not accessible, check your mic and permissions, then restart recording."):t.error!=="no-speech"&&t.error!=="aborted"&&console.warn("[WebSpeech] error:",t.error)},liveSpeechRec.start()}function _spkChip(e){const t=_liveSpeakerLabel(e);return t.length<=4?t:t.substring(0,2).toUpperCase()}function updateTranscriptUI(e,t,n){const a=document.getElementById("live-transcript-box"),s=document.getElementById("live-transcript-wordcount");if(!a)return;const i=a.scrollHeight-a.scrollTop-a.clientHeight<60;let o="";_priorTranscriptBaseline&&_priorTranscriptBaseline.trim()&&(_priorTxHtmlCache.src!==_priorTranscriptBaseline&&(_priorTxHtmlCache={src:_priorTranscriptBaseline,html:_buildPriorTranscriptHTML(_priorTranscriptBaseline)+'<div class="transcript-divider transcript-divider--continued">\u2500\u2500 Recording continued \u2500\u2500</div>'}),o=_priorTxHtmlCache.html);let r=o;if(e.forEach((d,u)=>{const p=_resolveSegmentName(d),m=d.nameOverride?p.length<=4?p:p.substring(0,2).toUpperCase():_spkChip(d.speakerIdx),f=d.nameOverride?" transcript-speaker--override":"",g=/^gm$/i.test(p)?" transcript-speaker--gm":"";r+=`<div class="transcript-turn"><button class="transcript-speaker transcript-speaker--col transcript-speaker--clickable${g}${f}" data-seg-idx="${u}" title="${escHtml(p)}: click to reassign">${_speakerAvatar(p)}</button><span class="transcript-text">${d.text}</span></div>`}),t||n){const d=_liveSpeakerLabel(liveSpeakerIdx),u=_spkChip(liveSpeakerIdx),p=_punctuate(t)||"",m=n?`<span class="transcript-interim"> ${n}</span>`:"",f=/^gm$/i.test(d)?" transcript-speaker--gm":"";r+=`<div class="transcript-turn transcript-turn--active"><span class="transcript-speaker transcript-speaker--col${f}" title="${escHtml(d)}">${_speakerAvatar(d)}</span><span class="transcript-text">${p}${m}</span></div>`}a.innerHTML=r||'<span class="transcript-empty">Listening\u2026</span>',i&&(a.scrollTop=a.scrollHeight);let l=(e.map(d=>d.text).join(" ")+(t?" "+t:"")).trim().split(/\s+/).filter(Boolean).length;_priorTranscriptBaseline&&_priorTranscriptBaseline.trim()&&(l+=_priorTranscriptBaseline.trim().split(/\s+/).filter(Boolean).length),s&&(s.textContent=`${l} word${l!==1?"s":""}`),a.querySelectorAll(".transcript-speaker--clickable").forEach(d=>{d.addEventListener("click",u=>{u.stopPropagation();const p=parseInt(d.dataset.segIdx);Number.isFinite(p)&&_openSegmentReassignMenu(p,d)})}),_refreshLiveSpkBar()}function _openSegmentReassignMenu(e,t){document.querySelectorAll(".spk-reassign-pop").forEach(u=>u.remove());const n=liveTranscriptSegments[e];if(!n)return;const a=_resolveSegmentName(n),s=_liveSpkPcOptions(a),i=document.createElement("div");i.className="spk-reassign-pop",i.innerHTML=`
1471    <div class="spk-reassign-row"><label>Reassign to:</label>
1472      <select class="spk-reassign-select">${s}</select>
1473    </div>
1474    <label class="spk-reassign-row spk-reassign-checkbox">
1475      <input type="checkbox" class="spk-reassign-propagate" />
1476      <span>Apply to all subsequent segments by this speaker</span>
1477    </label>
1478    <div class="spk-reassign-actions">
1479      <button class="btn-new spk-reassign-cancel">Cancel</button>
1480      <button class="btn-new spk-reassign-save">Save</button>
1481    </div>`,document.body.appendChild(i);const o=t.getBoundingClientRect(),r=i.getBoundingClientRect(),c=Math.min(window.innerHeight-r.height-8,o.bottom+6),l=Math.min(window.innerWidth-r.width-8,Math.max(8,o.left));i.style.top=`${c+window.scrollY}px`,i.style.left=`${l+window.scrollX}px`;const d=()=>i.remove();i.querySelector(".spk-reassign-cancel").addEventListener("click",d),i.querySelector(".spk-reassign-save").addEventListener("click",()=>{const u=i.querySelector(".spk-reassign-select").value,p=i.querySelector(".spk-reassign-propagate").checked;_applySegmentReassign(e,u,p),d()}),setTimeout(()=>{const u=p=>{i.contains(p.target)||(d(),document.removeEventListener("click",u,!0))};document.addEventListener("click",u,!0)},0)}
1481function _applySegmentReassign(e,t,n){const a=liveTranscriptSegments[e];if(!a||!t)return;const s=a.speakerIdx;let i=0;if(a.nameOverride=t,i++,n)for(let o=e+1;o<liveTranscriptSegments.length;o++)liveTranscriptSegments[o].speakerIdx===s&&(liveTranscriptSegments[o].nameOverride=t,i++);liveTranscriptAccum=_buildLiveTranscriptAccum(),scheduleSaveTranscript(),updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""),showToast(n?`Reassigned to ${t} (${i} segment${i!==1?"s":""})`:`Reassigned to ${t}`,3e3)}function _spkIdxLetter(e){let t=e,n="";do n=String.fromCharCode(65+t%26)+n,t=Math.floor(t/26)-1;while(t>=0);return n}function _liveSpeakerLabel(e){return liveSpeakerNameMap[e]??_spkIdxLetter(e)}function _resolveSegmentName(e){return e&&e.nameOverride?e.nameOverride:_liveSpeakerLabel(e?e.speakerIdx:0)}function _buildLiveTranscriptAccum(){return liveTranscriptSegments.map(e=>`${_resolveSegmentName(e)}: ${e.text}`).join(`
1482`)}function _absorbSpeakerRelabel(e,t){if(!Array.isArray(liveTranscriptSegments)||!liveTranscriptSegments.length)return;let n=0;liveTranscriptSegments.forEach(a=>{_resolveSegmentName(a)===e&&(a.nameOverride=t,a.speakerIdx!=null&&liveSpeakerNameMap&&typeof liveSpeakerNameMap=="object"&&(liveSpeakerNameMap[a.speakerIdx]=t),n++)}),n&&(liveTranscriptAccum=_buildLiveTranscriptAccum(),liveRecording&&(updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""),scheduleSaveTranscript()))}function _absorbSpeakerRelabelLine(e,t,n){if(!Array.isArray(liveTranscriptSegments)||!liveTranscriptSegments.length)return;const a=(n||"").trim();if(!a)return;const s=liveTranscriptSegments.find(i=>_resolveSegmentName(i)===e&&(i.text||"").trim()===a);s&&(s.nameOverride=t,liveTranscriptAccum=_buildLiveTranscriptAccum(),liveRecording&&(updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,""),scheduleSaveTranscript()))}function _openSpeakerRelabelMenu(e,t){document.querySelectorAll(".spk-reassign-pop").forEach(u=>u.remove());const n=parseInt(t.dataset.lineIdx,10),a=(t.parentElement?.querySelector(".transcript-text")?.textContent||"").trim();if(!Number.isFinite(n))return;const s=_liveSpkPcOptions(e),i=document.createElement("div");i.className="spk-reassign-pop",i.innerHTML=`
1483    <div class="spk-reassign-row"><label>Reassign this line to:</label>
1484      <select class="spk-reassign-select">${s}</select>
1485    </div>
1486    <p class="spk-reassign-hint">Changes only this line, not every line by ${escHtml(e)}.</p>
1487    <div class="spk-reassign-actions">
1488      <button class="btn-new spk-reassign-cancel">Cancel</button>
1489      <button class="btn-new spk-reassign-save">Save</button>
1490    </div>`,document.body.appendChild(i);const o=t.getBoundingClientRect(),r=i.getBoundingClientRect(),c=Math.min(window.innerHeight-r.height-8,o.bottom+6),l=Math.min(window.innerWidth-r.width-8,Math.max(8,o.left));i.style.top=`${c+window.scrollY}px`,i.style.left=`${l+window.scrollX}px`;const d=()=>i.remove();i.querySelector(".spk-reassign-cancel").addEventListener("click",d),i.querySelector(".spk-reassign-save").addEventListener("click",async()=>{const u=i.querySelector(".spk-reassign-select").value;if(d(),!(!u||u===e)&&!(!currentCampaign||!liveSession))try{const p=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/relabel-speaker-line`,{line_idx:n,to_label:u,expected_label:e,excerpt:a});p&&p.raw_transcript&&(_liveTranscriptPollLastText=p.raw_transcript,_applyPolledTranscript(p.raw_transcript)),showToast(`This line reassigned to ${u}`,3e3)}catch(p){if(p&&p.status===409){showToast("That line just changed, refreshed; please try again",4500);try{await _pollLiveLog()}catch{}}else showToast(p?.message||"Could not reassign speaker",4e3)}}),setTimeout(()=>{const u=p=>{i.contains(p.target)||(d(),document.removeEventListener("click",u,!0))};document.addEventListener("click",u,!0)},0)}(function(){const t=document.getElementById("live-transcript-box");t&&t.addEventListener("click",n=>{const a=n.target.closest(".transcript-speaker--relabel");if(!a||!t.contains(a))return;n.stopPropagation();const s=a.dataset.spkLabel;s&&_openSpeakerRelabelMenu(s,a)})})();const _VOICES_K_WORDS=4,_VOICES_HALF_LIFE_MS=6e5,_VOICES_WINDOW_MS=6e5,_VOICES_MIN_WORDS=200,_VOICES_GLYPH='<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5 8 8l-4.5 4.5M8.5 3.5 13 8l-4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';let _voicesPolledTurns=[],_voicesPolledSrc=null,_voicesLastLabels="",_voicesLastKey="",_voicesOpen=(()=>{try{return localStorage.getItem("tf_voices_open")!=="0"}catch{return!0}})();function _voicesReset(){_voicesPolledTurns=[],_voicesPolledSrc=null,_voicesLastLabels="",_voicesLastKey=""}
1490function _voicesWords(e){return e?e.trim().split(/\s+/).filter(Boolean).length:0}function _voicesParseRaw(e){const t=[];for(const n of(e||"").split(`
1491`)){const a=n.trim();if(!a)continue;const s=a.match(/^([^:]{1,30}):\s+(.+)$/);s&&t.push({name:s[1].trim(),w:_voicesWords(s[2]),t:null})}return t}function _voicesAbsorbPolled(e){if(typeof e!="string"||e===_voicesPolledSrc)return;const t=_voicesPolledSrc===null,n=_voicesPolledTurns,a=_voicesParseRaw(e),s=Date.now();for(let i=0;i<a.length;i++)a[i].t=i<n.length?n[i].t:t?null:s;_voicesPolledSrc=e,_voicesPolledTurns=a}function _voicesCollectTurns(){if(typeof liveRecording<"u"&&liveRecording){const e=typeof liveRecordingStartTime=="number"?liveRecordingStartTime:0,t=(liveTranscriptSegments||[]).map(n=>({name:_resolveSegmentName(n),w:_voicesWords(n.text),t:e&&n.committedAt!=null?e+n.committedAt:null}));return _priorTranscriptBaseline&&_priorTranscriptBaseline.trim()&&t.push(..._voicesParseRaw(_priorTranscriptBaseline)),t}return _voicesPolledTurns}function _voicesMedian(e){if(!e.length)return 0;const t=[...e].sort((a,s)=>a-s),n=Math.floor(t.length/2);return t.length%2?t[n]:(t[n-1]+t[n])/2}function _voicesCompute(e){const t=new Map;let n=0;for(const a of _voicesCollectTurns()){if(!a.name||a.w<=0)continue;n+=a.w;let s=t.get(a.name);if(s||(s={name:a.name,turns:0,words:0,presence:0,lastT:null,recentN:0,wpt:[]},t.set(a.name,s)),s.turns++,s.words+=a.w,s.wpt.push(a.w),a.t!=null){const i=e-a.t;s.presence+=Math.min(1,a.w/_VOICES_K_WORDS)*Math.pow(.5,i/_VOICES_HALF_LIFE_MS),i<=_VOICES_WINDOW_MS&&s.recentN++,(s.lastT==null||a.t>s.lastT)&&(s.lastT=a.t)}}return{by:t,totalWords:n}}function _voicesSnippet(e,t){return!e||!e.turns?{text:"",stale:!1}:e.recentN>0?{text:`${e.recentN}\xD7 \xB7 10m`,stale:!1}:e.lastT!=null?{text:`quiet ${Math.max(1,Math.round((t-e.lastT)/6e4))}m`,stale:!0}:{text:"\u2014",stale:!1}}function _voicesRender(){const e=document.getElementById("live-voices-strip"),t=document.getElementById("live-voices-body");if(!e||!t||e.classList.contains("hidden")||!_voicesOpen)return;const n=document.getElementById("live-transcript-panel");if(!n||n.classList.contains("hidden"))return;const a=Date.now(),{by:s,totalWords:i}=_voicesCompute(a);if(i<_VOICES_MIN_WORDS){_voicesLastKey!=="gathering"&&(_voicesLastKey="gathering",_voicesLastLabels="",t.innerHTML='<p class="lv-placeholder placeholder">Gathering data\u2026</p>');return}const o=_splitPcsByLifecycle().active,r=w=>{if(s.has(w))return s.get(w);const $=w.toLowerCase();for(const[E,k]of s)if(E.toLowerCase()===$)return k;return null},c=o.map(w=>({label:w.name,s:r(w.name),unmapped:!1})),l=new Set(c.map(w=>w.s).filter(Boolean)),d=[...s.values()].filter(w=>!l.has(w)&&!/^gm$/i.test(w.name));if(d.slice(0,4).forEach(w=>c.push({label:w.name,s:w,unmapped:!0})),d.length>5){const w=d.slice(4),$={name:"Others",turns:0,words:0,presence:0,lastT:null,recentN:0,wpt:[]};w.forEach(E=>{$.turns+=E.turns,$.words+=E.words,$.presence+=E.presence,$.recentN+=E.recentN,E.lastT!=null&&($.lastT==null||E.lastT>$.lastT)&&($.lastT=E.lastT)}),c.push({label:"Others",s:$,unmapped:!0})}else d.length===5&&c.push({label:d[4].name,s:d[4],unmapped:!0});const u=c.filter(w=>!w.unmapped&&w.s&&w.s.turns>=5).map(w=>w.s);let p=0,m=0;u.length>=3&&(p=_voicesMedian(u.map(w=>_voicesMedian(w.wpt))),m=_voicesMedian(u.map(w=>w.turns)));const f=w=>!w.unmapped&&p>0&&w.s&&w.s.turns>=8&&w.s.turns>=m&&_voicesMedian(w.s.wpt)<.5*p,g=Math.max(0,...c.map(w=>w.s?w.s.presence:0)),h=c.map(w=>{const $=_voicesSnippet(w.s,a),E=w.s&&g>0?Math.round(w.s.presence/g*100):0,k=f(w),C=w.s?`${w.s.turns} turn${w.s.turns!==1?"s":""} \xB7 ${w.s.words} words`+(w.s.lastT!=null?` \xB7 last spoke ${Math.max(0,Math.round((a-w.s.lastT)/6e4))}m ago`:"")+(k?" \xB7 quick reactions":""):"no lines yet";return{label:w.label,unmapped:w.unmapped,spoke:!!(w.s&&w.s.turns),pct:E,snip:$.text,stale:$.stale,glyph:k,receipt:C}}),v=[...s.values()].find(w=>/^gm$/i.test(w.name)),b=v&&i>0?`GM \xB7 ${Math.round(v.words/i*100)}%`:"",y=h.map(w=>`${w.label}|${w.unmapped?1:0}|${w.spoke?1:0}`).join(",")+"#"+(b?1:0),_=y+"@"+h.map(w=>`${w.pct}|${w.snip}|${w.stale?1:0}|${w.glyph?1:0}`).join(",")+"@"+b;if(_!==_voicesLastKey){if(_voicesLastKey=_,y===_voicesLastLabels){const w=t.querySelectorAll(".lv-row");h.forEach((E,k)=>{const C=w[k];if(!C)return;C.classList.toggle("lv-row--stale",E.stale);const S=C.querySelector(".lv-bar");S&&(S.style.width=E.pct+"%");const q=C.querySelector(".lv-snip");q&&(q.textContent=E.snip);const I=C.querySelector(".lv-glyph");I&&(I.innerHTML=E.glyph?_VOICES_GLYPH:""),C.title=E.receipt});const $=t.querySelector(".lv-gm");$&&($.textContent=b);return}_voicesLastLabels=y,t.innerHTML=h.map(w=>{const $=w.spoke?`<span class="lv-track"><span class="lv-bar" style="width:${w.pct}%"></span></span>
1491`:'<span class="lv-none">hasn\u2019t spoken</span>',E=w.unmapped?"":` style="--spk-color:${_speakerColor(w.label)}"`;return`<div class="lv-row${w.stale?" lv-row--stale":""}${w.unmapped?" lv-row--unmapped":""}"${E} title="${escHtml(w.receipt)}"><span class="lv-dot"></span><span class="lv-name">${escHtml(w.label)}</span><span class="lv-glyph" title="quick reactions">${w.glyph?_VOICES_GLYPH:""}</span>`+$+`<span class="lv-snip">${escHtml(w.snip)}</span></div>`}).join("")+(b?`<div class="lv-gm">${escHtml(b)}</div>`:"")}}(function(){const t=document.getElementById("live-voices-toggle");if(!t)return;const n=()=>{document.getElementById("live-voices-body")?.classList.toggle("hidden",!_voicesOpen),t.textContent=_voicesOpen?"\u25BE":"\u25B8",t.title=_voicesOpen?"Collapse Voices":"Expand Voices"};n(),t.addEventListener("click",()=>{_voicesOpen=!_voicesOpen;try{localStorage.setItem("tf_voices_open",_voicesOpen?"1":"0")}catch{}n(),_voicesOpen&&(_voicesLastKey="",_voicesLastLabels="",_voicesRender())}),setInterval(()=>{try{_voicesRender()}catch{}},5e3)})();function _refreshLiveSpkBar(){const e=document.getElementById("live-spk-bar-wrap");if(!e)return;if(typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive){_refreshDiscordSpkBar(e);return}if(document.activeElement&&e.contains(document.activeElement)&&document.activeElement.classList?.contains("live-spk-inline-select"))return;const t=[...new Set(liveTranscriptSegments.map(r=>r.speakerIdx))].sort((r,c)=>r-c);if(!t.length){_liveSpkBarLastTokens!==""&&(_liveSpkBarLastTokens="",e.innerHTML='<p class="placeholder placeholder--padded">Listening for speakers\u2026</p>');return}const n=t.join(",")+"|"+JSON.stringify(liveSpeakerNameMap)+"|"+JSON.stringify(liveSpeakerQuotes)+"|"+[..._liveAwayIds()].join(",");if(n===_liveSpkBarLastTokens)return;_liveSpkBarLastTokens=n;const a=r=>_liveSpkPcOptions(liveSpeakerNameMap[r]??""),s=t.map(r=>{const c=liveSpeakerQuotes[r]||[],l=c[0],d=_spkIdxLetter(r),u=liveSpeakerNameMap[r]??"",p=l?`"${escHtml(l.length>_SPK_DISPLAY_CAP?l.slice(0,_SPK_DISPLAY_CAP)+"\u2026":l)}"`:`<em style="color:var(--text-3)">${d}, listening for a clear sample\u2026</em>`,m=c.length>1?` <span class="live-spk-quote-more">+${c.length-1}</span>`:"",f=l?`<button class="live-spk-dismiss-btn" data-idx="${r}" title="Not a clean sample: show the next candidate">\u2715</button>`:"";return`<div class="live-spk-quote-row">
1492      <span class="live-spk-quote-text">${p}${m}</span>
1493      ${f}
1494      <select class="live-spk-inline-select" data-idx="${r}">
1495        <option value="" ${u?"":"selected"}>(Who is this?)</option>
1496        ${a(r)}
1497      </select>
1498    </div>`}).join(""),i=`<div class="spk-mute-hdr">
1499    <button type="button" id="spk-automute-btn" class="spk-automute-btn ${_tfDiscordAutoMute?"is-on":"is-off"}"
1500      title="${_tfDiscordAutoMute?"Auto Mute is ON: bots are muted automatically and the mic column is locked. Click to manage mics manually.":"Auto Mute is OFF: click any mic to toggle it. Click to re-lock, your choices are remembered; new arrivals follow the default."}">Auto Mute</button>
1501  </div>`;e.innerHTML=`${i}<div class="live-spk-body">${s}</div>`,document.getElementById("spk-automute-btn")?.addEventListener("click",async()=>{_tfDiscordAutoMute=!_tfDiscordAutoMute;const r=currentCampaign?.id;try{if(await api("PUT",`/api/campaigns/${r}/discord/speakers`,{speakers:_tfDiscordSpeakersPayload(),auto_mute:_tfDiscordAutoMute}),_campaignStale(r))return;showToast(_tfDiscordAutoMute?"Auto Mute on: bots muted automatically; your manual choices are remembered.":"Auto Mute off: click any mic to toggle it.",5e3)}catch(c){_tfDiscordAutoMute=!_tfDiscordAutoMute,showToast(c.message||"Could not update Auto Mute.",5e3)}_tfDiscordSpkBarKey="",_refreshLiveSpkBar()});const o=e.querySelector(".live-spk-body");o&&(o.scrollTop=o.scrollHeight),e.querySelectorAll(".spk-mic-btn").forEach(r=>{r.addEventListener("click",async()=>{if(_tfDiscordAutoMute)return;const c=r.dataset.micDid,l=_tfDiscordSpeakers.find(m=>m.id===c),d=c in _tfDiscordMutedMap?_tfDiscordMutedMap[c]:!!l?.is_bot,u=_tfDiscordMutedMap[c];_tfDiscordMutedMap[c]=!d;const p=currentCampaign?.id;try{if(await api("PUT",`/api/campaigns/${p}/discord/speakers`,{speakers:_tfDiscordSpeakersPayload()}),_campaignStale(p))return;showToast(_tfDiscordMutedMap[c]?"Muted: the bot stops transcribing this source within a minute.":"Unmuted: transcription resumes within a minute.",5e3)}catch(m){u===void 0?delete _tfDiscordMutedMap[c]:_tfDiscordMutedMap[c]=u,showToast(m.message||"Could not update mute.",5e3)}_tfDiscordSpkBarKey="",_refreshLiveSpkBar()})}),e.querySelectorAll(".live-spk-inline-select").forEach(r=>{r.addEventListener("change",()=>{const c=parseInt(r.dataset.idx),l=r.value;l?liveSpeakerNameMap[c]=l:delete liveSpeakerNameMap[c],liveTranscriptAccum=_buildLiveTranscriptAccum(),_liveSpkBarLastTokens="",updateTranscriptUI(liveTranscriptSegments,liveCurrentSegText,"")})}),e.querySelectorAll(".live-spk-dismiss-btn").forEach(r=>{r.addEventListener("click",()=>{const c=parseInt(r.dataset.idx),l=liveSpeakerQuotes[c]||[];l.shift(),l.length?liveSpeakerQuotes[c]=l:delete liveSpeakerQuotes[c],_liveSpkBarLastTokens="",_refreshLiveSpkBar()})}),_updateSpeakersTabVisibility()}
1501function _updateSpeakersTabVisibility(){if(!_isGm())return;const e=document.getElementById("live-tab-speakers-btn");if(!e)return;const t=liveRecording||liveTranscriptSegments.length>0||typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive;e.classList.toggle("hidden",!t),!t&&_liveTransTab==="speakers"&&_switchLiveTransTab("transcript")}function _transcriptBackupKey(e,t){return`tf_transcript_backup_${e}_${t}`}function scheduleSaveTranscript(){clearTimeout(liveTranscriptSaveTimer),liveTranscriptSaveTimer=setTimeout(async()=>{if(!liveRecording||!liveSession||!liveTranscriptAccum.trim())return;const e=liveTranscriptAccum.trim(),t=_priorTranscriptBaseline?_priorTranscriptBaseline+`
1502
1503--- Recording continued ---
1504
1505`+e:e;try{const n=_transcriptBackupKey(currentCampaign.id,liveSession.id);localStorage.setItem(n,t)}catch{}try{await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}`,{raw_transcript:t,is_live_transcript:!0})}catch{}},4e3)}function _txSaveRetryable(e){if(!e||/session expired/i.test(e.message||""))return!1;const t=e.status;return t?t>=500||t===429:!0}async function _tfSaveLiveTranscript(e,t,n,a=4){let s;for(let i=1;i<=a;i++)try{return await api("PUT",`/api/campaigns/${e}/sessions/${t}`,n)}catch(o){if(s=o,!_txSaveRetryable(o)||i===a)throw o;const r=800*Math.pow(2,i-1);console.warn(`[save-transcript] attempt ${i} failed (${o.status||"transport"}), retrying in ${r}ms`,o),showToast(`Save failed, retrying (${i}/${a-1})\u2026`,r),await new Promise(c=>setTimeout(c,r))}throw s}async function _checkTranscriptBackupRecovery(e,t){if(!e||!t)return;const n=_transcriptBackupKey(e,t);let a=null;try{a=localStorage.getItem(n)}catch{return}if(!a)return;const s=allSessions.find(o=>o.id===t)?.transcript_chars||0;if(a.length<=s+100){try{localStorage.removeItem(n)}catch{}return}if(await confirmDialog({title:"Recover unsaved transcript?",message:`A local backup for this session is ${a.length.toLocaleString()} characters, the server has only ${s.toLocaleString()}. This usually means a previous recording stopped unexpectedly. Restore the backup?`,confirmLabel:"Restore Backup",cancelLabel:"Discard Backup",danger:!1}))try{await api("PUT",`/api/campaigns/${e}/sessions/${t}`,{raw_transcript:a,is_live_transcript:!0}),allSessions=allSessions.map(o=>o.id===t?{...o,raw_transcript:a,is_live_transcript:!0}:o),_priorTranscriptBaseline=a,showToast(`\u2713 Transcript backup restored (${a.length.toLocaleString()} chars).`,6e3)}catch(o){showToast("Failed to restore backup: "+o.message,6e3);return}try{localStorage.removeItem(n)}catch{}}function _commitSegAndSwitch(e){if(liveCurrentSegHas){const t=_resolveSegText();_storeQuoteIfNew(liveSpeakerIdx,t),liveTranscriptSegments.push({text:t,speakerIdx:liveSpeakerIdx,committedAt:Date.now()-liveRecordingStartTime,turnOrders:_takeCurTurnOrders(),genBase:_liveSpeakerIdxBase}),_broadcastChunk(t,_liveSpeakerLabel(liveSpeakerIdx)),liveCurrentSegText="",liveCurrentSegHas=!1,_liveInterimLast="",liveTranscriptAccum=_buildLiveTranscriptAccum(),updateTranscriptUI(liveTranscriptSegments,"","")}liveSpeakerIdx=e}function startSpeakerDetection(){if(!liveAnalyser)return;liveAnalyser.fftSize=2048,liveAnalyser.smoothingTimeConstant=.3;const e=liveAnalyser.frequencyBinCount,t=liveAnalyser.fftSize,n=new Uint8Array(e),a=new Uint8Array(t),s=liveAnalyser.context.sampleRate,i=s/(e*2),o=T=>Math.max(2,Math.min(e-1,Math.round(T/i))),r=[[o(80),o(430)],[o(430),o(1500)],[o(1500),o(4300)],[o(4300),o(8e3)]],c=(T,P)=>{let B=0;for(let H=T;H<=P;H++)B+=n[H];return B},l=.015,d=.01;function u(){liveAnalyser.getByteTimeDomainData(a);let T=0;for(let P=0;P<t;P++){const B=(a[P]-128)/128;T+=B*B}return Math.sqrt(T/t)}function p(){liveAnalyser.getByteFrequencyData(n);let T=0,P=0;for(let z=2;z<200;z++)T+=n[z],P+=z*n[z];if(T<1)return null;const B=P/T;let H=0,A=2;const x=T*.85;for(let z=2;z<200;z++)if(H+=n[z],H>=x){A=z;break}const M=r.map(([z,V])=>c(z,V)/(T||1));liveAnalyser.getByteTimeDomainData(a);const D=Math.floor(s/500),O=Math.min(Math.floor(s/80),t-513),N=512;let G=-1/0,F=D;for(let z=D;z<=O;z++){let V=0;for(let K=0;K<N;K++)V+=(a[K]-128)/128*((a[K+z]-128)/128);V>G&&(G=V,F=z)}const W=G/N>.08?s/F:0;return{centroid:B,rolloff:A,bandRatios:M,pitch:W}}function m(T,P){const B=Math.abs(T.centroid-P.centroid)/Math.max(T.centroid,P.centroid,1),H=Math.abs(T.rolloff-P.rolloff)/Math.max(T.rolloff,P.rolloff,1),A=T.bandRatios.reduce((M,D,O)=>M+Math.abs(D-P.bandRatios[O]),0)/4,x=T.pitch>0&&P.pitch>0?Math.abs(T.pitch-P.pitch)/Math.max(T.pitch,P.pitch):0;return .6*B+.5*H+1.8*A+2*x}function f(T){const P=T.length,B=T.filter(H=>H.pitch>0);return{centroid:T.reduce((H,A)=>H+A.centroid,0)/P,rolloff:T.reduce((H,A)=>H+A.rolloff,0)/P,bandRatios:r.map((H,A)=>T.reduce((x,M)=>x+M.bandRatios[A],0)/P),pitch:B.length>0?B.reduce((H,A)=>H+A.pitch,0)/B.length:0}}const g=(T,P,B=.15)=>({centroid:T.centroid*(1-B)+P.centroid*B,rolloff:T.rolloff*(1-B)+P.rolloff*B,bandRatios:T.bandRatios.map((H,A)=>H*(1-B)+P.bandRatios[A]*B),pitch:T.pitch>0&&P.pitch>0?T.pitch*(1-B)+P.pitch*B:T.pitch||P.pitch});function h(T){if(T.length<3)return 0;const P=T.slice(1).map((G,F)=>m(G,T[F])),B=P.reduce((G,F)=>
1505G+F,0)/P.length,H=Math.sqrt(P.reduce((G,F)=>G+(F-B)**2,0)/P.length),A=B>0?H/B:0;let x=0,M=0,D=0;liveAnalyser.getByteFrequencyData(n);for(let G=2;G<200;G++){const F=Math.max(n[G],1);x+=Math.log(F),M+=F,D++}const O=Math.exp(x/D)/(M/D),N=1-Math.min(1,Math.max(0,(A-.3)/.5))-Math.min(1,Math.max(0,(O-.1)/.35));return Math.max(0,N)}const v=[];let b=-1;const y=.4,_=8;function w(T){if(!v.length){v.push({profile:T,speakerIdx:0}),b=0,liveSpeakerIdx=0,console.log("[SPK] first speaker");return}let P=1/0,B=-1;for(let A=0;A<v.length;A++){const x=m(T,v[A].profile);x<P&&(P=x,B=A)}const H=v.length>=_;if(H||P<y){const A=v[B].speakerIdx;A!==liveSpeakerIdx&&(console.log("[SPK] \u2192",A,"dist",P.toFixed(3),H?"[capped]":""),_commitSegAndSwitch(A),b=B)}else{const A=v.reduce((x,M)=>Math.max(x,M.speakerIdx),-1)+1;console.log("[SPK] new",A,"dist",P.toFixed(3)),_commitSegAndSwitch(A),v.push({profile:T,speakerIdx:A}),b=v.length-1}}function $(T){if(T.length<10||b<0)return;const P=h(T);if(P<.45){console.log("[SPK] overlap/noisy segment skipped conf",P.toFixed(2));return}const B=f(T);v[b].profile=g(v[b].profile,B,.18),console.log("[SPK] profile updated spk",liveSpeakerIdx,"conf",P.toFixed(2))}let E=0,k=0,C=0,S=[];const q=2,I=5;liveSpeakerDetectInterval=setInterval(()=>{if(!liveRecording)return;const T=u(),P=E<=1?T>l:T>d,B=p();switch(E){case 0:P&&(E=1,k=1);break;case 1:P?++k>=q&&(E=2,S=B?[B]:[],B&&w(B)):(E=0,k=0);break;case 2:P?B&&S.push(B):(E=3,C=0);break;case 3:P?(E=2,B&&S.push(B)):++C>=I&&(E=0,$(S),S=[]);break}},200)}async function startLiveTranscription(e=!0){if(e){const n=liveTranscriptAccum.trim(),a=allSessions.find(o=>o.id===liveSession?.id),s=a&&a.is_live_transcript&&a.transcript_chars>0?await _tfSessionTranscript(a.id):"",i=typeof s=="string"?s.trim():"";_priorTranscriptBaseline=n.length>=i.length?n:i,liveTranscriptAccum="",liveTranscriptSegments=[],liveCurrentSegText="",liveCurrentSegHas=!1,liveSpeakerIdx=0,_liveSpeakerIdxBase=0,_liveCurTurnOrders=[],_abortAaiCycle("fresh_start"),liveSpeakerNameMap={},liveSpeakerQuotes={},_liveSpkBarLastTokens="",_voicesLastKey="",_voicesLastLabels="",_updateSpeakersTabVisibility(),_liveInterimLast="",liveWsSessionComplete=!1,_usingAAI=!1,updateTranscriptUI([],"",""),_startLiveScan(),_switchLiveTransTab("transcript"),document.getElementById("live-preamble")?.classList.add("hidden")}document.getElementById("live-transcript-panel").classList.remove("hidden"),liveRecording=!0,_liveTelDeferOutcome&&(_liveTelDeferOutcome=!1,_flushLiveTelemetry("stop")),clearTimeout(_liveTelDeferTimer),_liveTelDeferTimer=null,_liveTelDeferOutcome=!1,_liveTelOutcome=null,_liveEvents.length=0,_liveTelCarry&&(_liveEvents.push(_liveTelCarry),_liveTelCarry=null),_liveTelFlushed=!1,_updateSpeakersTabVisibility(),showToast("\u{1F534} Recording has started"),_isGm()&&_broadcastStart();let t=null;try{t=(await api("GET",_liveTokenUrl())).token}catch(n){console.log("[AAI] Not available, using Chrome WebSpeech:",n.message),_tfLiveCapNotice(n)}t?(_aaiConnectRetries=0,_startAssemblyAI(t),liveMicStream||setTimeout(()=>{if(_usingAAI&&liveTranscriptSegments.length===0&&!liveCurrentSegHas){if(console.warn("[AAI] No mic stream + no transcript after 12s \u2014 falling back to Chrome WebSpeech"),_usingAAI=!1,liveWs){try{liveWs.send(JSON.stringify({type:"Terminate"}))}catch{}liveWs=null}_stopPcmCapture(),_startChromeWebSpeech(),startSpeakerDetection(),showToast("Mic not detected in AudioContext, switched to browser transcription.",5e3)}},12e3)):(_startChromeWebSpeech(),startSpeakerDetection())}function stopLiveTranscription(){if(liveRecording=!1,_updateSpeakersTabVisibility(),_stopWatchdog(),_liveMinimized&&_updateLiveRecordBar(),clearTimeout(liveTranscriptSaveTimer),clearInterval(liveSpeakerDetectInterval),clearTimeout(liveSegmentIdleTimer),liveSpeakerDetectInterval=null,liveSegmentIdleTimer=null,liveAnalyser=null,_broadcastStop(),_broadcastActive=!1,_flushLiveTelemetry("stop"),_abortAaiCycle("stop"),liveWs){try{liveWs.close()}catch{}liveWs=null}if(_stopPcmCapture(),liveSpeechRec){try{liveSpeechRec.stop()}catch{}liveSpeechRec=null}document.getElementById("live-transcript-panel").classList.add("hidden"),_liveInterimLast=""}async function _broadcastStart(){try{const e=!!window._tfNextBroadcastAnnounce;window._tfNextBroadcastAnnounce=!1;const t=new URLSearchParams;e&&t.set("announce","1"),typeof liveSession<"u"&&liveSession?.id&&t.set("session_id",String(liveSession.id)),(_tfRecModeSel==="inpers
1505on"||_tfRecModeSel==="virtual")&&t.set("capture_mode",_tfRecModeSel);const n=t.toString()?`?${t.toString()}`:"",a=await api("POST",`/api/campaigns/${currentCampaign.id}/broadcast/start${n}`);_broadcastActive=!0,e&&a&&a.discord_announce==="failed"&&showToast("Discord announce failed, check bot permissions in your recap channel.",6e3)}catch(e){if(e.status===402){_showUpgradeModal({scenario:"cap_exhausted"});return}console.warn("[broadcast] start failed:",e.message),showToast(e.message||"Could not start live broadcast")}}async function _broadcastStop(){if(_broadcastActive){_broadcastActive=!1;try{await api("POST",`/api/campaigns/${currentCampaign.id}/broadcast/stop`)}catch{}}}async function _broadcastChunk(e,t){if(!(!_broadcastActive||!e?.trim()))try{await api("POST",`/api/campaigns/${currentCampaign.id}/broadcast/chunk`,{text:e.trim(),speaker:t||"A"})}catch(n){n&&n.status===409&&_broadcastEndedByInactivity()}}let _broadcastEndedNoticeOpen=!1;async function _broadcastEndedByInactivity(){if(!_broadcastEndedNoticeOpen){_broadcastEndedNoticeOpen=!0,_broadcastActive=!1;try{await confirmDialog({title:"Live feed ended due to inactivity",message:`Your players\u2019 live view paused because nothing was heard for a long stretch.
1506
1507Your recording never stopped and the transcript is safe \u2014 it saves when you press Stop, exactly as normal. Resume only puts your players back on the feed.`,confirmLabel:"Resume live feed",cancelLabel:"Leave it off",danger:!1})&&(await _broadcastStart(),showToast("Live feed resumed."))}catch{}finally{_broadcastEndedNoticeOpen=!1}}}function _startWatcherPoll(){if(_watcherPollTimer)return;_watcherPollTimer=setInterval(_pollBroadcast,3e3);const e=_watcherPollTimer;setTimeout(()=>{_watcherPollTimer===e&&_pollBroadcast()},_TF_POLL_FIRST_TICK_MS)}function _stopWatcherPoll(){clearInterval(_watcherPollTimer),_watcherPollTimer=null}async function _pollBroadcast(){if(currentCampaign)try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/broadcast?after=${_watcherLastSeq}`),t=_watcherIsLive;_watcherIsLive=!!e.is_live,_watcherIsLive&&!t&&_enforceLiveConsent();const n=document.getElementById("live-broadcast-banner"),a=document.getElementById("live-watcher-panel");if(!n)return;if(_watcherIsLive&&(!_isGm()||_tfDiscordRecActive)){if(n.classList.remove("hidden"),!t){_watcherLastSeq=0;const s=document.getElementById("watcher-box");s&&(s.innerHTML='<span class="watcher-empty">Listening\u2026</span>');const i=document.getElementById("watcher-wordcount");i&&(i.textContent="0 words")}}else _watcherIsLive||(n.classList.add("hidden"),t&&(showToast("Live session has ended."),a&&a.classList.add("hidden"),_watcherPanelOpen=!1));if(e.chunks&&e.chunks.length&&typeof _tfDiscordRecActive<"u"&&_tfDiscordRecActive&&(e.chunks.forEach(s=>{_watcherLastSeq=Math.max(_watcherLastSeq,s.seq),_tfDiscordChunkLines.push(`${s.speaker||"A"}: ${s.text}`)}),_renderDiscordLiveTranscript(),e.chunks.forEach(s=>_dhFearObserve(s.text,"discord"))),e.chunks&&e.chunks.length){const s=document.getElementById("watcher-box");if(s){const i=s.querySelector(".watcher-empty");i&&i.remove();const o=s.scrollHeight-s.scrollTop-s.clientHeight<60;e.chunks.forEach(c=>{_watcherLastSeq=Math.max(_watcherLastSeq,c.seq);const l=document.createElement("div");l.className="watcher-turn",l.innerHTML=`<span class="watcher-speaker">${escHtml(c.speaker||"A")}</span><span class="watcher-text">${escHtml(c.text)}</span>`,s.appendChild(l)}),o&&(s.scrollTop=s.scrollHeight);const r=document.getElementById("watcher-wordcount");if(r){const c=[...s.querySelectorAll(".watcher-text")].map(l=>l.textContent).join(" ").trim().split(/\s+/).filter(Boolean).length;r.textContent=`${c} word${c!==1?"s":""}`}}}}catch{}}function _toggleWatcherPanel(){const e=document.getElementById("live-watcher-panel");if(!e)return;_watcherPanelOpen=!_watcherPanelOpen,e.classList.toggle("hidden",!_watcherPanelOpen);const t=document.querySelector(".broadcast-watch-btn");t&&(t.textContent=_watcherPanelOpen?"Hide \u25B4":"Watch live \u25BE")}const _TF_FEED_GLYPHS={person:'<circle cx="8" cy="5" r="2.6"/><path d="M2.8 14a5.2 5.2 0 0 1 10.4 0"/>',sword:'<path d="M3 13l7.5-7.5M10.5 2.5l3 3-1.5 1.5-3-3zM2 14l1.5-1.5M5 9l2 2"/>',pin:'<path d="M8 14.5s-4.5-4.4-4.5-7.7a4.5 4.5 0 0 1 9 0C12.5 10.1 8 14.5 8 14.5Z"/><circle cx="8" cy="6.8" r="1.6"/>',gem:'<path d="M4 2.5h8l2.5 3.5L8 14 1.5 6zM1.5 6h13M5.5 6 8 14l2.5-8"/>',banner:'<path d="M3.5 2.5h9v11l-4.5-3-4.5 3z"/>',star:'<path d="M8 1.8l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.6l-3.8 2 .7-4.3-3.1-3 4.3-.6z"/>',book:'<path d="M2.5 3h4a2 2 0 0 1 1.5.8A2 2 0 0 1 9.5 3h4v10h-4a1.5 1.5 0 0 0-1.5 1 1.5 1.5 0 0 0-1.5-1h-4zM8 3.8V14"/>',wand:'<path d="M2.5 13.5 10 6M11.5 2.5l.6 1.4 1.4.6-1.4.6-.6 1.4-.6-1.4-1.4-.6 1.4-.6zM4 4l.4 1 1 .4
1507-1 .4-.4 1-.4-1-1-.4 1-.4z"/>',list:'<path d="M3 4.5h1M6 4.5h7M3 8h1M6 8h7M3 11.5h1M6 11.5h7"/>',pencil:'<path d="M3 13l.8-3.2L11.5 2l2.5 2.5-7.8 7.8L3 13zM9.8 3.7l2.5 2.5"/>',skull:'<path d="M8 1.8a5 5 0 0 0-5 5c0 1.8.9 3.1 2 3.9V13h6v-2.3c1.1-.8 2-2.1 2-3.9a5 5 0 0 0-5-5z"/><circle cx="6" cy="7" r="1"/><circle cx="10" cy="7" r="1"/><path d="M7 13v1.2M9 13v1.2"/>',out:'<path d="M6 2.5H3v11h3M9.5 5l3 3-3 3M12.5 8H6"/>',back:'<path d="M13 3.5v4a3 3 0 0 1-3 3H3.5M6 7.5 3.5 10.5 6 13.5"/>',eye:'<path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8z"/><circle cx="8" cy="8" r="2"/>',mask:'<path d="M2.5 3.5c3.5 1.5 7.5 1.5 11 0v4.5c0 3.5-2.5 6-5.5 6S2.5 11.5 2.5 8zM5.5 7.5h1.5M9 7.5h1.5M6 10.5c1.2.8 2.8.8 4 0"/>',scroll:'<path d="M4.5 2.5h8a1.5 1.5 0 0 1 0 3h-1M4.5 2.5a1.5 1.5 0 0 0-1.5 1.5v8a1.5 1.5 0 0 0 1.5 1.5h7a1.5 1.5 0 0 0 1.5-1.5V5.5M5.5 6.5h4M5.5 9h4"/>',check:'<path d="M3 8.5 6.5 12 13 4.5"/>',cross:'<path d="M4 4l8 8M12 4l-8 8"/>',play:'<path d="M4 2.5v11l9-5.5z"/>',send:'<path d="M14 2 2 7l5 2 2 5zM7 9l7-7"/>',dice:'<rect x="2.5" y="2.5" width="11" height="11" rx="2.5"/><circle cx="5.5" cy="5.5" r=".9"/><circle cx="10.5" cy="10.5" r=".9"/><circle cx="8" cy="8" r=".9"/>',bell:'<path d="M4 11V7.5a4 4 0 0 1 8 0V11l1.2 1.5H2.8L4 11zM6.5 14a1.5 1.5 0 0 0 3 0"/>',image:'<rect x="2" y="3" width="12" height="10" rx="1.5"/><circle cx="5.5" cy="6.5" r="1.2"/><path d="M14 11 10 7l-5 5.5"/>',refresh:'<path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.9L13.5 5.5M13.5 2v3.5H10M13.5 8a5.5 5.5 0 0 1-9.4 3.9L2.5 10.5M2.5 14v-3.5H6"/>',dot:'<circle cx="8" cy="8" r="1.8"/>',key:'<circle cx="5.5" cy="10.5" r="3"/><path d="M7.8 8.2 14 2M11.5 4.5l2 2M9.5 6.5l2 2"/>'};function _tfFeedSvg(e){return`<svg class="tf-glyph feed-item-svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${_TF_FEED_GLYPHS[e]||_TF_FEED_GLYPHS.dot}</svg>`}const _FEED_ICONS={entity_created:{npc:"person",pc:"sword",location:"pin",item:"gem",faction:"banner",deity:"star",lore:"book",spell:"wand",_default:"list"},entity_updated:"pencil",entity_died:"skull",entity_departed:"out",entity_returned:"back",entity_revealed:"eye",entities_staged:"mask",quest_created:"scroll",quest_completed:"check",quest_failed:"cross",quest_updated:"list",session_started:"play",key_event:"star",discord_recap_published:"send",discord_live_announce_posted:"dice",discord_last_time_on_posted:"bell",discord_reveal_saved:"image",discord_reveal_posted:"eye",discord_entity_shared:"book",discord_event_synced:"refresh",ownership_transferred:"key"};function _feedIcon(e,t){const n=_FEED_ICONS[e];return _tfFeedSvg(n?typeof n=="object"?n[t?.type]||n._default:n:"dot")}function _feedProvenance(e){const t=[];return e.via==="crosslink"?t.push(e.source_name?`via crosslink from <strong>${escHtml(e.source_name)}</strong>`:"via crosslink"):e.via==="recap"?t.push("from the session recap"):e.via==="doc_sync"?t.push("from a journal sync"):e.via==="sync"?t.push("from an entity sync"):e.via==="ai_refresh"&&t.push("chronicle regenerated"),e.actor_name&&t.push(`by ${escHtml(e.actor_name)}`),t.length?` <span class="feed-prov">\xB7 ${t.join(" \xB7 ")}</span>`:""}function _feedItemHtml(e){const t=e.payload||{},n=new Date(e.created_at),a=n.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}),s=n.toLocaleDateString([],{month:"short",day:"numeric"}),i=_feedIcon(e.type,t);let o="",r="",c="feed-item";switch(e.type){case"entity_created":o=`<strong>${escHtml(t.name||"")}</strong> added to Codex <span class="feed-type-pill">${escHtml(t.type||"")}</span>${_feedProvenance(t)}`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--entity feed-item--link";break;case"entity_updated":{const l=t.change||"updated";l==="renamed"?o=`<strong>${escHtml(t.name||"")}</strong> was renamed`:l==="reclassified"?o=`<strong>${escHtml(t.name||"")}</strong> reclassified as <span class="feed-type-pill">${escHtml(t.new_type||t.type||"")}</span>`:o=`<strong>${escHtml(t.name||"")}</strong> codex entry updated`,o+=_feedProvenance(t),t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--entity feed-item--link";break}case"entity_died":o=`<strong>${escHtml(t.name||"")}</strong> has fallen`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--death feed-item--link";break;case"entity_departed":o=`<strong>${escHtml(t.name||"")}</strong> has left the party`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--departure feed-item--link";break;case"entity_returned":o=`<strong>${escHtml(t.name||"")}</strong> has rejoined`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--entity feed-item--link";break;case"entity_revealed":o=`<strong>${escHtml(t.name||"")}</strong> revealed to players${_feedProvenance(t)}`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--entity feed-item--link";break;case"entities_staged":{const l=Number(t.count)||0;o=`${l} new Codex ${l===1?"entry":"entries"} staged for reveal${_feedProvenance(t)}`,c+=" feed-item--entity";break}case"quest_created":o=`New quest: <strong>${escHtml(t.title||"")}</strong>`,t.quest_id&&(r=`data-feed-quest-id="${Number(t.quest_id)}"`),c+=" feed-item--quest feed-item--link";break;
1507case"quest_completed":o=`<strong>${escHtml(t.title||"")}</strong>: completed`,t.quest_id&&(r=`data-feed-quest-id="${Number(t.quest_id)}"`),c+=" feed-item--complete feed-item--link";break;case"quest_failed":o=`<strong>${escHtml(t.title||"")}</strong>: failed`,t.quest_id&&(r=`data-feed-quest-id="${Number(t.quest_id)}"`),c+=" feed-item--failed feed-item--link";break;case"quest_updated":o=`Quest updated: <strong>${escHtml(t.title||"")}</strong>`,c+=" feed-item--quest";break;case"session_started":o=`<strong>${escHtml(t.title||"New session")}</strong> has begun`,c+=" feed-item--session";break;case"key_event":o=`<em>${escHtml((t.content||"").slice(0,160))}${t.content?.length>160?"\u2026":""}</em>`,c+=" feed-item--key";break;case"discord_recap_published":{const l=(t.session_title||"").trim(),d=t.session_num,u=t.beats_posted??0,p=t.tally_included?' <span class="feed-type-pill">+tally</span>':"";o=`Recap published to Discord: ${l&&d?`<strong>Session ${escHtml(d)}: ${escHtml(l)}</strong>`:l?`<strong>${escHtml(l)}</strong>`:`<strong>Session ${escHtml(d||"?")}</strong>`} (${u} ${u===1?"beat":"beats"} threaded)${p}`,c+=" feed-item--discord";break}case"discord_live_announce_posted":{const l=(t.gm_name||"").trim(),d=t.next_session_num;o=l&&d?`Live announce posted to Discord: <strong>${escHtml(l)}</strong> just started Session ${escHtml(d)}`:"Live announce posted to Discord",c+=" feed-item--discord";break}case"discord_last_time_on_posted":{const l=t.prior_session_num;o=l?`"Last time on\u2026" ping posted to Discord: recapping <strong>Session ${escHtml(l)}</strong>`:'"Last time on\u2026" ping posted to Discord',c+=" feed-item--discord";break}case"discord_reveal_saved":{const l=(t.entity_name||"").trim(),d=(t.entity_type||"").trim(),u=d?` <span class="feed-type-pill">${escHtml(d)}</span>`:"";o=`<strong>${escHtml(l)}</strong> saved via <code>/tf-reveal</code>${u}`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--discord feed-item--link";break}case"discord_reveal_posted":{const l=(t.entity_name||"").trim();o=`<strong>${escHtml(l)}</strong> revealed to players on Discord`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--discord feed-item--link";break}case"discord_entity_shared":{const l=(t.entity_name||"").trim();o=`<strong>${escHtml(l)}</strong>&rsquo;s codex card posted to Discord`,t.entity_id&&(r=`data-feed-entity-id="${Number(t.entity_id)}"`),c+=" feed-item--discord feed-item--link";break}case"discord_event_synced":{const l=(t.event_name||"").trim();o=l?`Next session synced from Discord event: <strong>${escHtml(l)}</strong>`:"Next session synced from a Discord event",c+=" feed-item--discord";break}case"speaker_relabel":o=`Speaker fixed on the transcript: <strong>${escHtml(t.from||"")}</strong> \u2192 <strong>${escHtml(t.to||"")}</strong>`,c+=" feed-item--session";break;case"ownership_transferred":o=`Campaign ownership handed to <strong>${escHtml(t.to_name||"another GM")}</strong>`+(t.by_name?` \xB7 by ${escHtml(t.by_name)}
1507`:"");break;default:o=escHtml(e.type)}return`<div class="${c}" ${r}>
1508    <span class="feed-item-icon">${i}</span>
1509    <span class="feed-item-body">${o}</span>
1510    <span class="feed-item-time" title="${escHtml(n.toLocaleString())}">${s} ${a}</span>
1511  </div>`}function renderFeed(){const e=document.getElementById("feed-list");if(e){if(!allFeedItems.length){e.innerHTML='<p class="feed-empty placeholder placeholder--padded">No events yet. Events appear here as sessions progress.</p>';return}e.innerHTML=[...allFeedItems].reverse().map(_feedItemHtml).join("")}}async function loadFeed(){if(currentCampaign)try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/events?limit=100`);allFeedItems=[...e].reverse(),_feedLatestId=e.length?e[0].id:0,_feedLoaded=!0,renderFeed()}catch(e){document.getElementById("feed-list").innerHTML=`<p class="feed-empty placeholder--padded error">${escHtml(e.message)}</p>`}}document.getElementById("feed-list").addEventListener("click",e=>{const t=e.target.closest("[data-feed-entity-id],[data-feed-quest-id]");t&&(t.dataset.feedEntityId?openEntityById(Number(t.dataset.feedEntityId)):t.dataset.feedQuestId&&openQuestById(Number(t.dataset.feedQuestId)))});function _startFeedPoll(){_feedPollTimer||(_feedPollTimer=setInterval(_pollFeed,5e3))}function _stopFeedPoll(){clearInterval(_feedPollTimer),_feedPollTimer=null}function buildLiveSessionPreamble(){const t=allSessions.filter(d=>d.id!==liveSession?.id&&d.summary&&d.summary.trim()).sort((d,u)=>(u.number??u.id)-(d.number??d.id))[0];if(!t)return null;const n=_preambleParseBullets(t.summary),a=new Set(allEntities.filter(d=>d.last_seen_session_id===t.id).map(d=>d.id)),s=allQuests.filter(d=>d.status==="active").map(d=>d.title.toLowerCase()),i=allEntities.filter(d=>d.type==="location").map(d=>d.name.toLowerCase()),o=/\b(decided|agreed|chose|accepted|refused|rejected|pledged|promised|revealed|discovered|uncovered|confronted|arrested|killed|escaped|betrayed)\b/i,c=n.map((d,u)=>{let p=10;const m=d.toLowerCase();return allEntities.some(f=>a.has(f.id)&&m.includes(f.name.toLowerCase()))&&(p+=50),s.some(f=>m.includes(f))&&(p+=40),o.test(d)&&(p+=30),i.some(f=>f.length>3&&m.includes(f))&&(p+=20),{text:d,score:p,idx:u}}).sort((d,u)=>u.score-d.score).slice(0,5).filter(d=>d.text.length>0).sort((d,u)=>d.idx-u.idx),l=_preambleBuildNext(t);return{lastTimeOn:c,whatComesNext:l,sessionTitle:t.title}}function _preambleParseBullets(e){const t=e.split(/^##\s*/m).filter(Boolean),n=[];for(const a of t){const i=a.split(`
1512`).map(c=>c.trim()).filter(Boolean).slice(1).join(" ");if(!i)continue;const o=i.match(/^[^.!?]+[.!?]/),r=(o?o[0]:i.substring(0,180)).trim();r.length>10&&n.push(r)}return n.length?n:e.split(`
1513`).map(a=>a.replace(/^[-*•◆]\s*/,"").trim()).filter(a=>a.length>10).slice(0,8)}function _preambleBuildNext(e){const t=[],n=allQuests.filter(s=>s.status==="active")[0];n&&t.push(`<em>${escHtml(n.title)}</em> still demands your attention.`);const a=_preambleLooseThreads(e);return a.length&&t.push(`${linkifyText(escHtml(a[0]),allEntities,null)} remains unresolved\u2026`),t.length&&t.push("What you choose next may change everything."),t}function _preambleLooseThreads(e){const t=allEntities.filter(a=>a.last_seen_session_id===e.id&&a.type==="npc"&&!a.is_dead&&!_hiddenFromViewer(a)),n=allQuests.filter(a=>a.status==="active").map(a=>`${a.title} ${a.description||""}`.toLowerCase()).join(" ");return t.filter(a=>n.includes(a.name.toLowerCase())).map(a=>a.name).slice(0,2)}function renderLiveSessionPreamble(){const e=document.getElementById("live-preamble");if(!e)return;const t=buildLiveSessionPreamble();if(!t||!t.lastTimeOn.length){e.classList.add("hidden");return}const n=t.lastTimeOn.map(i=>`<li class="live-preamble-bullet">${linkifyText(escHtml(i.text),allEntities,null)}</li>`).join(""),a=t.whatComesNext.map((i,o)=>`<p class="live-preamble-next-line${o===t.whatComesNext.length-1?" live-preamble-next-coda":""}">${i}</p>`).join("");document.getElementById("live-preamble-bullets").innerHTML=n,document.getElementById("live-preamble-next").innerHTML=a||'<p class="live-preamble-next-line live-preamble-next-coda">Nothing pressing\u2026 for now.</p>';const s=document.getElementById("live-preamble-session-label");s&&t.sessionTitle&&(s.textContent=`\u21A9 ${t.sessionTitle}`),typeof _isNotebookRecapDismissed=="function"&&_isNotebookRecapDismissed()?e.classList.add("hidden"):e.classList.remove("hidden"),typeof _renderNotebookRecap=="function"&&_renderNotebookRecap()}function _startLiveScan(){_stopLiveScan(),_liveScanOffset=0,_liveScanTimer=setInterval(_doLiveScan,LIVE_SCAN_INTERVAL_MS)}function _stopLiveScan(){_liveScanTimer&&(clearInterval(_liveScanTimer),_liveScanTimer=null)}async function _doLiveScan(){if(_testRecordingMode||!liveRecording||!liveSession||!currentCampaign||!_isGm())return;const e=Math.max(0,_liveScanOffset-200),t=liveTranscriptAccum.slice(e).trim();if(console.log("[live-scan] timer fired \u2014 accum:",liveTranscriptAccum.length,"offset:",_liveScanOffset,"chunk:",t.length),!(t.length<150))try{const n=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${liveSession.id}/live-scan`,{transcript_chunk:t,timestamp_seconds:liveElapsedSeconds??null});_liveScanOffset=liveTranscriptAccum.length;const a=n?.entry,s=n?.combat_entry,i=n?.context_entry;console.log("[live-scan] result \u2014 story:",!!a,"combat:",!!s,"context:",!!i,"entities:",n?.entities_created,"quests:",n?.quests_created);const o=new Set(liveLogEntries.map(l=>l.id));let r=!1;a&&!o.has(a.id)&&(a._remote=!0,liveLogEntries=[a,...liveLogEntries],a.id>_liveLogMaxId&&(_liveLogMaxId=a.id),o.add(a.id),r=!0),s&&!o.has(s.id)&&(s._remote=!0,liveLogEntries=[s,...liveLogEntries],s.id>_liveLogMaxId&&(_liveLogMaxId=s.id),o.add(s.id),r=!0),i&&!o.has(i.id)&&(i._remote=!0,liveLogEntries=[i,...liveLogEntries],i.id>_liveLogMaxId&&(_liveLogMaxId=i.id),r=!0),r&&renderLiveLog();const c=[];n.entities_created>0&&c.push(`${n.entities_created} codex entr${n.entities_created===1?"y":"ies"} added`),n.quests_created>0&&c.push(`${n.quests_created} quest${n.quests_created===1?"":"s"} found`),c.length&&showToast("Live scan: "+c.join(", "))}catch(n){console.warn("[live-scan] error:",n.message),showToast("\u26A0 Live scan failed: "+n.message,5e3)}}async function _pollFeed(){if(currentCampaign){if(!_feedLoaded){await loadFeed();return}try{const e=await api("GET",`/api/campaigns/${currentCampaign.id}/events?after=${_feedLatestId}`);if(!e.length)return;_feedLatestId=e.reduce((s,i)=>Math.max(s,i.id),_feedLatestId),allFeedItems=[...allFeedItems,...e];const t=document.getElementById("tab-manage");if(t&&!t.classList.contains("hidden")){const s=document.getElementById("feed-list");s&&(s.querySelector(".feed-empty")&&(s.innerHTML=""),[...e].reverse().forEac
1513h(o=>{const r=_feedItemHtml(o);s.insertAdjacentHTML("afterbegin",r),s.firstElementChild?.classList.add("feed-item--new")}))}const a=document.getElementById("feed-live-indicator");a&&a.classList.toggle("hidden",!_watcherIsLive)}catch{}}}function openEntityById(e){navigateToEntity(e)}function openQuestById(e){_partyOpenQuests(currentCampaign&&currentCampaign.id,e)}let _csAllCampaigns=[],_csRecordingIds=new Set,_csAdminRecordingExtra=[],_csSortKey="next",_csPreviewBuilders=null,_csViewMode=(()=>{try{return localStorage.getItem("tf_cs_view")==="cards"?"cards":"binders"}catch{return"binders"}})();const _CS_SKEL_HTML=(()=>{const e={};for(const t of["binders","cards"]){const n=document.querySelector("#campaign-list-screen > .cs-skel--"+t);n&&(e[t]=n.outerHTML.replace('class="cs-skel ','class="cs-skel is-on '))}return e})();let _csShelfOpen=(()=>{try{const e=localStorage.getItem("tf_shelf_open");return e===null?!0:e==="1"}catch{return!0}})();const _SPINE_JEWELS=["--jewel-amethyst","--jewel-teal","--jewel-crimson","--jewel-forest","--jewel-navy","--jewel-oxblood","--jewel-ink","--jewel-bronze"],_spineJewelVar=e=>`var(${_SPINE_JEWELS[(e%8+8)%8]})`,_CS_SESSION_GRACE_MS=8*60*60*1e3;function _csUpcomingTs(e){const t=e.next_session_at?Date.parse(e.next_session_at):NaN;return Number.isFinite(t)&&t>Date.now()-_CS_SESSION_GRACE_MS?t:null}function _sortCampaigns(e,t){const n=[...e];switch(t){case"name":n.sort((a,s)=>a.name.localeCompare(s.name));break;case"gm":{const a=s=>(s.gm_display_name||s.gm_email||"").toLowerCase();n.sort((s,i)=>a(s).localeCompare(a(i)));break}case"session":{n.sort((a,s)=>{const i=a.last_session_date,o=s.last_session_date;return i&&o?new Date(o)-new Date(i):i?-1:o?1:new Date(s.created_at||0)-new Date(a.created_at||0)});break}case"next":{n.sort((a,s)=>{const i=_csUpcomingTs(a),o=_csUpcomingTs(s);if(i!=null&&o!=null)return i-o;if(i!=null)return-1;if(o!=null)return 1;const r=a.last_activity_at||a.last_session_date||a.created_at||0,c=s.last_activity_at||s.last_session_date||s.created_at||0;return new Date(c)-new Date(r)});break}case"recent":{const a=s=>s.last_activity_at||s.last_session_date||s.created_at||"1970-01-01";n.sort((s,i)=>new Date(a(i))-new Date(a(s)));break}case"created":n.sort((a,s)=>new Date(s.created_at)-new Date(a.created_at));break;case"role":{const a={gm:0,co_gm:1,member:2,observer:3};n.sort((s,i)=>{const o=(a[s.user_role]??3)-(a[i.user_role]??3);return o!==0?o:s.name.localeCompare(i.name)});break}}return n}const _COVER_FILES={"Curse of Strahd.webp":"curse of strahd","Out of the Abyss.webp":"out of the abyss","Phandelver and Below - The Shattered Obelisk.webp":"phandelver and below","Tyranny of Dragons.webp":"tyranny of dragons"},_COVER_DEFAULTS=(()=>{const e={};for(const[t,n]of Object.entries(_COVER_FILES))e[n]="/static/covers/"+encodeURIComponent(t);return e})(),_COVER_FALLBACK_PREFIXES=["ethos","astarions book of hungers","baldurs gate descent into avernus","borderlands quest goblin trouble","candlekeep mysteries","critical role call of the netherdeep","descent into the lost caverns of tsojcanth","divine contention","dragon delves","dragon of icespire peak","dragonlance shadow of the dragon queen","dragons of stormwreck isle","dungeon masters ravenloft play along pack","fated flight of the recluse","forgotten realms adventures in faerun","forgotten realms the tenebrous stone","frozen sick","ghosts of saltmarsh","giants of the star forge","heroes of the borderlands","heroes feast saving the childrens menu","hold back the dead","icewind dale rime of the frostmaiden","infernal machine rebuild","intro to stormwreck isle","journeys through the radiant citadel","keys from the golden vault prisoner 13","keys from the golden vault","lightning keep","locathah rising","lost laboratory of kwalish","lost mine of phandelver","netherils fall","princes of the apocalypse","quests from the infinite staircase","rrakkma","scions of elemental evil","sleeping dragons wake","spelljammer academy","storm kings thunder","tales from the yawning portal","the radiant citadel","the wild beyond the witchlight","tomb of annihilation","uni and the hunt for the lost horn","vecna eve of ruin","vecna nest of the eldritch eye","waterdeep dragon heist","waterdeep dungeon of the mad mage","welcome to the hellfire club"],_COVER_KNOWN_PREFIXES=[...Object.keys(_COVER_DEFAULTS),..._COVER_FALLBACK_PREFIXES].sort((e,t)=>
1513t.length-e.length);function _normForCover(e){return(e||"").toLowerCase().replace(/&/g," and ").replace(/['‘’ʼ]/g,"").replace(/[\-:–—]+/g," ").replace(/\s+/g," ").trim()}function _matchCoverPrefix(e){const t=_normForCover(e);for(const n of _COVER_KNOWN_PREFIXES)if(t.startsWith(n))return n;if(t.startsWith("the ")){const n=t.slice(4);for(const a of _COVER_KNOWN_PREFIXES)if(n.startsWith(a))return a}return null}function _csDayDiff(e){const t=_csShelfDate(e);if(!t)return null;const n=new Date(t.getFullYear(),t.getMonth(),t.getDate()),a=new Date,s=new Date(a.getFullYear(),a.getMonth(),a.getDate());return Math.round((s-n)/864e5)}function _csFutureSessionZero(e){if(!e||!e.last_session_is_zero||!e.last_session_date)return!1;const t=_csDayDiff(e.last_session_date);return t!==null&&t<0}function _csRelDay(e){if(!e)return"";const t=_csDayDiff(e);if(t===null)return"";const n=_csShelfDate(e);return t<=0?"today":t===1?"yesterday":t<7?"last "+n.toLocaleDateString("en-US",{weekday:"short"}):n.getFullYear()===new Date().getFullYear()?n.toLocaleDateString("en-US",{month:"short",day:"numeric"}):n.toLocaleDateString("en-US",{month:"short",year:"numeric"})}function _csNextSession(e){const t=e.next_session_at,n=t?new Date(t):null;if(!n||isNaN(n.getTime())||n.getTime()<=Date.now())return null;const s=n.getTime()-Date.now()<=48*3600*1e3,i=n.toLocaleDateString(void 0,{weekday:"short"}),o=n.getMinutes()===0?{hour:"numeric"}:{hour:"numeric",minute:"2-digit"},r=n.toLocaleTimeString(void 0,o).replace(/\s*([AaPp])\.?\s*[Mm]\.?$/,(y,_)=>_.toUpperCase()),c=y=>{const _=new Date(y);return _.setHours(0,0,0,0),_.getTime()},l=c(new Date),d=Math.round((c(n)-l)/864e5),u=l+(7-new Date().getDay())*864e5,p=c(n)<u,m=n.toLocaleDateString(void 0,{month:"numeric",day:"numeric"}),f=d===0?"Today":d===1?"Tomorrow":p?`This ${i} ${m}`:`${i} ${m}`,g=`${f}, ${r}`;let h="",v="";const b=e.recurrence_rule||"none";if(b==="monthly")h="monthly",v="mthly";else if(b==="weekly"){const y=Number(e.recurrence_interval)||1;h=y===1?"weekly":`every ${y} wks`,v=y===1?"wkly":`${y} wks`}return{when:g,date:f,time:r,cadence:h,cadenceShort:v,soon:s,near:d<=1||p}}function _newCampaignCardHtml(){return`
1514    <div class="cc-card cc-new-card" id="cs-new-campaign-card" role="button" tabindex="0" aria-label="Start a new campaign">
1515      <div class="cc-new-rest">
1516        <span class="cc-new-eyebrow cc-new-eyebrow--gm">For GMs</span>
1517        <span class="cc-new-plus" aria-hidden="true">+</span>
1518        <span class="cc-new-title">Start a new campaign</span>
1519        <span class="cc-new-sub">Your next session is the first one you won't have to write up.</span>
1520      </div>
1521      <form class="cc-new-edit hidden" autocomplete="off" novalidate>
1522        <div class="cc-new-edit-top">
1523          <span class="form-section-label">Campaign name</span>
1524          <button type="button" class="cc-new-cancel" aria-label="Cancel">\u2715</button>
1525        </div>
1526        <input type="text" class="cc-new-input" maxlength="60" placeholder="The Ember Accord\u2026" aria-label="Campaign name" aria-describedby="cs-new-hint" enterkeyhint="go" autocomplete="off" autocorrect="off" spellcheck="false" />
1527        <!-- SCHEMA 118 (owner 2026-08-02): the ruleset choice IS the create
1528             action \u2014 two equal-weight buttons instead of a picker + generic
1529             Start. This inline card is the PRIMARY create path (the modal is
1530             secondary); both must ask, or a Daggerheart table silently lands
1531             on the 5e defaults. D&D first by reading-order convention only;
1532             neither is visually primary (GM agent: a secondary Daggerheart
1533             button reads as an afterthought). Stacked, not side by side, so
1534             the full labels fit the narrow card column without wrapping. -->
1535        <div class="cc-new-edit-foot cc-new-foot--sys">
1536          <button type="button" class="btn-new cc-new-start" data-camp-system="dnd5e" aria-label="Create a D&amp;D campaign" disabled>D&amp;D</button>
1537          <button type="button" class="btn-new cc-new-start" data-camp-system="daggerheart" aria-label="Create a Daggerheart campaign" disabled>Daggerheart</button>
1538        </div>
1539        <span class="cc-new-hint" id="cs-new-hint">Esc to cancel</span>
1540        <p class="cc-new-error placeholder--padded error hidden" role="alert"></p>
1541      </form>
1542    </div>`}function _joinCampaignCardHtml(){return`
1543    <div class="cc-card cc-new-card cc-join-card" id="cs-join-campaign-card" role="button" tabindex="0" aria-label="Join a campaign with an invite code">
1544      <div class="cc-new-rest">
1545        <span class="cc-new-eyebrow cc-new-eyebrow--player">For players</span>
1546        <span class="cc-new-plus" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="22" height="22"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="M10 17l5-5-5-5"/><path d="M15 12H3"/></svg></span>
1547        <span class="cc-new-title">Join a campaign</span>
1548        <span class="cc-new-sub">Your GM has the code, paste it here and you're in.</span>
1549      </div>
1550      <form class="cc-new-edit hidden" autocomplete="off" novalidate>
1551        <div class="cc-new-edit-top">
1552          <span class="form-section-label">Invite code</span>
1553          <button type="button" class="cc-new-cancel" aria-label="Cancel">\u2715</button>
1554        </div>
1555        <input type="text" class="cc-new-input cc-join-input" maxlength="64" placeholder="e.g. RAVENS-4F2K" aria-label="Invite code" aria-describedby="cs-join-hint" enterkeyhint="go" autocomplete="off" autocorrect="off" spellcheck="false" />
1556        <div class="cc-new-edit-foot">
1557          <button type="submit" class="btn-new cc-new-start" disabled>Join \u2192</button>
1558        </div>
1559        <span class="cc-new-hint" id="cs-join-hint">\u21B5 Enter \xB7 Esc to cancel</span>
1560        <p class="cc-new-error placeholder--padded error hidden" role="alert"></p>
1561      </form>
1562    </div>`}function _importCampaignCardHtml(){return`
1563    <div class="cc-card cc-new-card cc-import-card" id="cs-import-campaign-card" role="button" tabindex="0" aria-label="Import a campaign from Archivist">
1564      <div class="cc-new-rest">
1565        <span class="cc-new-eyebrow cc-new-eyebrow--gm">For GMs</span>
1566        <span class="cc-new-plus" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="22" height="22"><path d="M12 3v13"/><path d="m7 12 5 4 5-4"/><path d="M5 21h14"/></svg></span>
1567        <span class="cc-new-title">Import a campaign</span>
1568        <span class="cc-new-sub">Coming from Archivist? Bring it all across.</span>
1569      </div>
1570    </div>`}function _wireImportCampaignCard(){const e=document.getElementById("cs-import-campaign-card");if(!e)return;const t=()=>{typeof _aimportOpen=="function"&&_aimportOpen()};e.addEventListener("click",t),e.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),t())})}function _wireJoinCampaignCard(){const e=document.getElementById("cs-join-campaign-card");if(!e)return;const t=e.querySelector(".cc-new-rest"),n=e.querySelector(".cc-new-edit"),a=e.querySelector(".cc-new-input"),s=e.querySelector(".cc-new-start"),i=e.querySelector(".cc-new-hint"),o=e.querySelector(".cc-new-error");let r=!1,c=null;const l="\u21B5 Enter \xB7 Esc to cancel",d="Join a campaign with an invite code";function u(){c&&(document.removeEventListener("pointerdown",c,!0),c=null)}function p(){if(e.classList.contains("cc-new-card--editing"))return;e.classList.add("cc-new-card--editing"),e.removeAttribute("role"),e.removeAttribute("tabindex"),e.removeAttribute("aria-label"),t.classList.add("hidden"),n.classList.remove("hidden"),o.classList.add("hidden"),a.value="",s.disabled=!0,i&&(i.textContent=l);const g=Date.now();requestAnimationFrame(()=>{a.focus();try{e.scrollIntoView({behavior:"smooth",block:"center"})}catch{}}),setTimeout(()=>{e.classList.contains("cc-new-card--editing")&&(c=h=>{Date.now()-g<500||h.composedPath().includes(e)||m()},document.addEventListener("pointerdown",c,!0))},0)}function m(){r||(u(),e.classList.remove("cc-new-card--editing"),n.classList.add("hidden"),t.classList.remove("hidden"),a.value="",s.disabled=!0,o.classList.add("hidden"),i&&(i.textContent=l),e.setAttribute("role","button"),e.setAttribute("tabindex","0"),e.setAttribute("aria-label",d))}async function f(){let g=a.value.trim();const h=g.match(/\/join\/([A-Za-z0-9_-]{3,64})/)||g.match(/[?&](?:code|invite|invite_code)=([A-Za-z0-9_-]{3,64})/);
1570if(h&&(g=h[1]),g=g.replace(/\s+/g,""),!g){a.classList.remove("cc-new-shake"),a.offsetWidth,a.classList.add("cc-new-shake"),i&&(i.textContent="Enter the code your GM sent."),a.focus();return}if(!r){r=!0,e.dataset.saving="1",a.disabled=!0,s.disabled=!0,s.textContent="Joining\u2026",o.classList.add("hidden");try{const v=await api("POST","/api/campaigns/join",{invite_code:g}),b=v&&v.campaign&&v.campaign.name||"the campaign";showToast(v&&v.already_member?`You're already in "${b}".`:`Joined "${b}"!`),u();const y=v&&v.campaign&&v.campaign.id;y?await bootWithCampaignId(y):await loadCampaignSelector()}catch(v){r=!1,delete e.dataset.saving,a.disabled=!1,s.disabled=!1,s.textContent="Join \u2192";const b=v&&v.message||"";o.textContent=/invalid|not found|expired|no longer/i.test(b)?"That code doesn\u2019t look right, double-check it or ask your GM.":b||"Couldn\u2019t join, try again.",o.classList.remove("hidden"),a.focus()}}}t.addEventListener("click",p),e.addEventListener("keydown",g=>{!e.classList.contains("cc-new-card--editing")&&(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),p())}),a.addEventListener("input",()=>{s.disabled=a.value.trim().length===0,i&&(i.textContent=l)}),a.addEventListener("keydown",g=>{g.key==="Escape"&&(g.preventDefault(),m())}),n.addEventListener("submit",g=>{g.preventDefault(),a.value.trim()&&startBtns[0]&&startBtns[0].focus()}),e.querySelector(".cc-new-cancel")?.addEventListener("click",g=>{g.preventDefault(),m()})}function _wireNewCampaignCard(){const e=document.getElementById("cs-new-campaign-card");if(!e)return;const t=e.querySelector(".cc-new-rest"),n=e.querySelector(".cc-new-edit"),a=e.querySelector(".cc-new-input"),s=[...e.querySelectorAll(".cc-new-start")],i=g=>s.forEach(h=>{h.disabled=g}),o=e.querySelector(".cc-new-hint"),r=e.querySelector(".cc-new-error");let c=!1,l=null;const d="Esc to cancel";function u(){l&&(document.removeEventListener("pointerdown",l,!0),l=null)}function p(){if(e.classList.contains("cc-new-card--editing"))return;e.classList.add("cc-new-card--editing"),e.removeAttribute("role"),e.removeAttribute("tabindex"),e.removeAttribute("aria-label"),t.classList.add("hidden"),n.classList.remove("hidden"),r.classList.add("hidden"),a.value="",i(!0),s.forEach(h=>{h.textContent=h.dataset.origLabel||h.textContent}),o&&(o.textContent=d);const g=Date.now();requestAnimationFrame(()=>{a.focus();try{e.scrollIntoView({behavior:"smooth",block:"center"})}catch{}}),setTimeout(()=>{e.classList.contains("cc-new-card--editing")&&(l=h=>{Date.now()-g<500||h.composedPath().includes(e)||m()},document.addEventListener("pointerdown",l,!0))},0)}function m(){c||(u(),e.classList.remove("cc-new-card--editing"),n.classList.add("hidden"),t.classList.remove("hidden"),a.value="",i(!0),r.classList.add("hidden"),o&&(o.textContent=d),e.setAttribute("role","button"),e.setAttribute("tabindex","0"),e.setAttribute("aria-label","Start a new campaign"))}async function f(g){const h=a.value.trim();if(!h){a.classList.remove("cc-new-shake"),a.offsetWidth,a.classList.add("cc-new-shake"),o&&(o.textContent="Give it a name first"),a.focus();return}if(c)return;c=!0,e.dataset.saving="1",a.disabled=!0,i(!0);const v=s.find(b=>b.dataset.campSystem===g);v&&(v.dataset.origLabel=v.dataset.origLabel||v.textContent,v.textContent="Creating\u2026"),r.classList.add("hidden");try{if(currentCampaign=await api("POST","/api/campaigns",{name:h,use_extended:!0,game_system:g}),currentRole=currentCampaign.user_role||"gm",currentCampaign.cap_warning&&currentCampaign.cap_warning.message)try{showToast(currentCampaign.cap_warning.message,7e3)}catch{}try{const b="tf_campaigns_"+(currentUser&&currentUser.id||"anon"),y=Array.isArray(_csAllCampaigns)?_csAllCampaigns.slice():[];y.unshift(currentCampaign),_csAllCampaigns=y,localStorage.setItem(b,JSON.stringify(y))}catch{}u(),applyRoleUI(),await showMain(),typeof _tfRoll20MaybeContinueImport=="function"&&_tfRoll20MaybeContinueImport()}catch(b){c=!1,delete e.dataset.saving,a.disabled=!1,i(!1),s.forEach(y=>{y.dataset.origLabel&&(y.textContent=y.dataset.origLabel)}),r.textContent=b&&b.message?b.message:"Couldn\u2019t create, try again.",r.classList.remove("hidden"),a.focus()}}
1570t.addEventListener("click",p),s.forEach(g=>{g.dataset.origLabel=g.textContent,g.addEventListener("click",h=>{h.preventDefault(),h.stopPropagation(),g.disabled||f(g.dataset.campSystem)})}),e.addEventListener("keydown",g=>{!e.classList.contains("cc-new-card--editing")&&(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),p())}),a.addEventListener("input",()=>{i(a.value.trim().length===0),o&&(o.textContent=d)}),a.addEventListener("keydown",g=>{g.key==="Escape"&&(g.preventDefault(),m())}),n.addEventListener("submit",g=>{g.preventDefault(),f()}),e.querySelector(".cc-new-cancel")?.addEventListener("click",g=>{g.preventDefault(),m()})}const _BND_PRESETS=["#1D2733","#16333A","#2E1B18","#2A2318","#241E33"];function _bndMix(e,t,n){const a=parseInt(e.replace("#",""),16);return"#"+[a>>16&255,a>>8&255,a&255].map(i=>Math.round(i+(n-i)*t)).map(i=>i.toString(16).padStart(2,"0")).join("")}function _bndColorVars(e){return/^#[0-9a-fA-F]{6}$/.test(e||"")?`--bnd-1:${e};--bnd-2:${_bndMix(e,.3,0)};--bnd-3:${_bndMix(e,.48,0)};--bnd-deep:${_bndMix(e,.72,0)};--bnd-spine-1:${_bndMix(e,.5,0)};--bnd-spine-2:${_bndMix(e,.24,0)};`:""}function _openCampCardMenu(e){document.querySelectorAll(".cc-card-menu").forEach(m=>m.remove());const t=parseInt(e.dataset.id),n=e.dataset.name||"this campaign",a=e.dataset.canEdit==="1",s=e.dataset.canLeave==="1",i=document.createElement("div");i.className="cc-card-menu",i.setAttribute("role","menu");const o=Array.isArray(_csAllCampaigns)&&_csAllCampaigns.find(m=>m.id===t)?.binder_color||_BND_PRESETS[0],r=a?'<div class="ccm-swatch-row" role="group" aria-label="Binder color"><span class="ccm-swatch-label">Binder color</span><span class="ccm-swabs">'+_BND_PRESETS.map(m=>`<button type="button" class="ccm-swab${m===o?" ccm-swab--on":""}" data-color="${m}" style="--swab:${m}" title="Binder color" aria-label="Binder color ${m}" aria-pressed="${m===o}"></button>`).join("")+"</span></div>":"";i.innerHTML=(a?'<button type="button" class="cc-card-menu-item" data-act="manage">\u2699 Manage campaign</button>':"")+r+(s?'<button type="button" class="cc-card-menu-item cc-card-menu-item--danger" data-act="leave">Leave campaign</button>':""),document.body.appendChild(i);const c=e.getBoundingClientRect(),l=i.offsetWidth||180;i.style.top=window.scrollY+c.bottom+4+"px",i.style.left=window.scrollX+Math.max(8,Math.min(c.right-l,window.innerWidth-l-8))+"px";const d=()=>{i.remove(),document.removeEventListener("click",u,!0),document.removeEventListener("keydown",p,!0)},u=m=>{i.contains(m.target)||d()},p=m=>{m.key==="Escape"&&d()};setTimeout(()=>{document.addEventListener("click",u,!0),document.addEventListener("keydown",p,!0)},0),i.querySelectorAll(".ccm-swab").forEach(m=>{m.addEventListener("click",async f=>{f.stopPropagation();const g=m.dataset.color;try{await api("PUT",`/api/campaigns/${t}`,{binder_color:g});const h=Array.isArray(_csAllCampaigns)&&_csAllCampaigns.find(v=>v.id===t);h&&(h.binder_color=g),Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns),i.querySelectorAll(".ccm-swab").forEach(v=>{v.classList.toggle("ccm-swab--on",v===m),v.setAttribute("aria-pressed",String(v===m))})}catch(h){showToast(h&&h.message||"Could not save the binder color.")}})}),i.querySelector('[data-act="manage"]')?.addEventListener("click",async()=>{d(),_tfPendingLanding="manage";try{await bootWithCampaignId(t),applyRoleUI(),document.getElementById("nav-manage-btn")?.click()}finally{_tfPendingLanding=null}}),i.querySelector('[data-act="leave"]')?.addEventListener("click",async()=>{if(d(),!!await confirmDialog({title:"Leave campaign?",message:`Leave \u201C${n}\u201D? You'll lose access to its Live, recaps, party, and codex. The GM can re-invite you with the campaign code.`,confirmLabel:"Leave",cancelLabel:"Stay",danger:!0}))try{if(await api("POST",`/api/campaigns/${t}/leave`),document.querySelector(`#campaign-selector .cc-card[data-id="${t}"]`)?.remove(),Array.isArray(_csAllCampaigns)){const f=_csAllCampaigns.findIndex(g=>g.id===t);f>=0&&_csAllCampaigns.splice(f,1)}showToast(`You left \u201C${n}\u201D.`)}catch(f){showToast(f&&f.message||"Could not leave the campaign.")}})}function _cc2CaptureKey(e){const t=e.game_type_default||null,n=e.last_session_capture||null;
1570if(t==="manual"&&(n==="discord"||n==="inperson"||n==="virtual"))return{key:n,declared:!1};const a=t||n;return a?{key:a,declared:!!t}:null}const _CC2_CAPTURE_GLYPHS={manual:{last:"Last session: written recap",gt:"Game type: notes / transcript",svg:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><rect x="4" y="1.5" width="8" height="13" rx="1"/><path d="M6 5.5h4M6 8h4M6 10.5h2.5"/></svg>'},inperson:{last:"Last session: recorded in person",gt:"Game type: live play in person",svg:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><rect x="6" y="1.5" width="4" height="8" rx="2"/><path d="M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12v2.5M2.5 14.5h11"/></svg>'},virtual:{last:"Last session: recorded online",gt:"Game type: live play online",svg:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="8.5" rx="1.2"/><path d="M6 14h4M8 11v3"/></svg>'},discord:{last:"Last session: recorded on Discord",gt:"Game type: live play on Discord",svg:DISCORD_GLYPH}};function _applyCoverFramingVars(e,t,n=1){if(!e)return;["bnd","card"].forEach(i=>{e.style.removeProperty(`--${i}-crop`),e.style.removeProperty(`--${i}-crop-pos`)});const a=t&&t.cover_framing;if(!a||a.src!==t.cover_image_url)return;const s={binder:280*n,card:158*n};for(const[i,o]of[["binder","bnd"],["card","card"]]){const r=a[i];if(!r||!(r.h>0&&r.h<=1&&r.w>0&&r.w<=1))continue;const c=(100/r.w).toFixed(2),l=r.a>0?`max(${c}%, ${(s[i]*r.a/r.h).toFixed(1)}px) auto`:`max(${c}%, 100%) auto`;e.style.setProperty(`--${o}-crop`,l);const d=r.w<.999?r.x/(1-r.w)*100:50,u=r.h<.999?r.y/(1-r.h)*100:50;e.style.setProperty(`--${o}-crop-pos`,`${d.toFixed(2)}% ${u.toFixed(2)}%`)}}function _tfFitFaceoutTitle(e){const t=e&&e.querySelector(".cs-faceout-title");if(!t||t.clientWidth<=20)return;const n=parseFloat(e.style.getPropertyValue("--spine-h"))||224,a=n*.55,s=n*.075;let i=n*.115;t.style.setProperty("--fo-title",i.toFixed(1)+"px");for(let o=0;o<40&&i>s&&!(t.scrollHeight<=a&&t.scrollWidth<=t.clientWidth+1);o++)i-=.5,t.style.setProperty("--fo-title",i.toFixed(1)+"px")}function _tfSpineCoverUrl(e){return e=String(e),e.includes("/image/upload/")?e.replace("/image/upload/","/image/upload/c_fill,g_auto,ar_2:5,w_360,q_auto:good/"):e}function _renderCampaignCards(e){const t=document.getElementById("campaign-list-screen");if(t&&t.querySelector(".cc-new-card--editing"))return;t&&t.classList.toggle("cs-view-binders",_csViewMode==="binders");const n=currentUser?.is_admin?e:e.filter(I=>!!I.user_role),a=n.filter(I=>!!I.completed_at),s=currentUser?.is_admin?[]:(e||[]).filter(I=>I.anthology_ghost),i=[...a,...s],o=n.filter(I=>!I.completed_at),r=_sortCampaigns(o,_csSortKey),c=r.filter(I=>_csRecordingIds.has(I.id));if(currentUser?.is_admin&&_csAdminRecordingExtra.length){const I=new Set(r.map(T=>T.id));for(const T of _csAdminRecordingExtra)I.has(T.id)||c.push(T)}const l=c.length?r.filter(I=>!_csRecordingIds.has(I.id)):r,d={};for(const I of e){if(!I.cover_image_url)continue;const T=_matchCoverPrefix(I.name);T&&!d[T]&&(d[T]=I.cover_image_url)}const u=I=>_COVER_DEFAULTS[I]||d[I]||null,p=(I,T=!1)=>{T=T===!0;const P=(I.gm_credit_name||"").trim()||(I.gm_display_name||"").trim()||(I.gm_discord_username||"").trim()||((I.gm_email||"").split("@")[0]||"").trim(),B=I.last_session_date?(_csShelfDate(I.last_session_date)||new Date(I.last_session_date)).toLocaleDateString():"",H=I.last_activity_at?new Date(I.last_activity_at).toLocaleDateString():"",A=B?"Last session: "+B:"",x=H&&H!==B?"Last active: "+H:"",M=!B&&!H&&I.created_at?"Created: "+new Date(I.created_at).toLocaleDateString():"",D=!!I.cover_image_url,O=I.user_role==="gm"||I.user_role==="co_gm"||!!currentUser?.is_admin,N=I.user_role==="member"||I.user_role==="co_gm"||I.user_role==="observer";let G="";{const ne=_csNextSession(I);ne&&(G=`<div class="cc2-next${ne.soon?" cc2-next--soon":""}">${escHtml(ne.when+(ne.cadence?" \xB7 "+ne.cadence:""))}</div>`)}const F=Number(I.session_count)||0,W=F===0?"No sessions yet":F===1?"1 session":`${F}
1570 sessions`,z=Array.isArray(I.pc_names)?I.pc_names.filter(Boolean):[],V=6;let K=[];z.length>V?(K=z.slice(0,V).map(ne=>`<span class="cc2-pc">${escHtml(ne)}</span>`),K.push(`<span class="cc2-pc cc2-pc--more" title="${escHtml(z.slice(V).join(", "))}">+${z.length-V}</span>`)):K=z.map(ne=>`<span class="cc2-pc">${escHtml(ne)}</span>`);const se=K.length?`<div class="cc2-pcs">${K.join("")}</div>`:"";let Q="";if(I.pending_recap&&I.pending_recap.session_id){const ne=I.pending_recap,we=ne.session_title&&ne.session_title.trim()?ne.session_title.trim():ne.session_number?`Session ${ne.session_number}`:"Last session";Q=`<div class="cc-pending" data-pending-recap="${ne.session_id}"><span class="cc-pending-dot">\u25CF</span>Ready for Recap: ${escHtml(we)}</div>`}let X="";if(_csSortKey==="anthology"&&I.anthology_id){const ne=I.anthology_active_campaign_id===I.id,we=(I.anthology_era_label||"").trim(),Me=(I.anthology_world_label||"").trim();X=`<div class="campaign-card-anthology"><div class="cca-name-line"><span class="cca-anthology-name">${escHtml(I.anthology_title||"")}</span><span class="cca-pill ${ne?"cca-pill--active":"cca-pill--legacy"}">${ne?"Active":"Legacy"}</span></div><div class="cca-detail-line">${we?escHtml(we):'<span class="cca-empty">No era set</span>'}</div><div class="cca-detail-line">${Me?escHtml(Me):'<span class="cca-empty">No world set</span>'}</div></div>`}const te=(I.anthology_world_label||I.region_label||"").trim();let le=(I.party_name||"").trim();/^the (company|party)$/i.test(le)&&(le="");const ue=_csRelDay(I.last_session_date),me=_csFutureSessionZero(I)?`${W} \xB7 Session 0 Scheduled`:ue?`${W} \xB7 ${ue}`:W,de='<svg class="cc2-ridge" viewBox="0 0 360 158" preserveAspectRatio="none" aria-hidden="true"><polygon points="0,158 70,96 120,118 190,70 250,110 320,72 360,104 360,158" fill="none" stroke="currentColor" stroke-width="1.5"></polygon><polygon points="0,158 90,120 170,140 250,108 360,138 360,158" fill="currentColor" opacity="0.16"></polygon></svg>',ye=_csShelfDate(I.last_session_date)||_csShelfDate(I.completed_at),Le=T&&ye?`<span class="cc-shelved-stamp">Completed ${escHtml(ye.toLocaleDateString())}</span>`:"",Se=_cc2CaptureKey(I),he=!T&&Se&&_CC2_CAPTURE_GLYPHS[Se.key],be=he?`<div class="cc2-capture${P?"":" cc2-capture--solo"}" title="${Se.declared?he.gt:he.last}">${he.svg}</div>`:"";return`
1571      <div class="cc-card cc-card--v2 ${T?"cc-card--shelved ":""}${D?"has-cover":"no-cover"}${X?" cc-card--anthology":""}" data-id="${I.id}" role="button" tabindex="0">
1572        <div class="cc2-cover">
1573          ${D?"":de}
1574          <div class="cc2-grad"></div>
1575          ${I.gm_brand_logo_url||I.gm_brand?.logo_url?`<img class="cc2-partner" src="${escHtml(I.gm_brand_logo_url||I.gm_brand.logo_url)}" alt="${escHtml(I.gm_brand?.name||"")}" title="${escHtml(I.gm_brand?.name||"")}" loading="lazy"/>`:""}
1576          <span id="camp-live-${I.id}" class="cc-live cc2-live hidden" aria-live="polite"><span class="cc-live-dot"></span><span class="cc-live-label">Live Now</span></span>
1577          ${O||N?`<button class="cc-gear camp-settings-btn cc2-gear" data-id="${I.id}" data-name="${escHtml(I.name)}" data-can-edit="${O?1:0}" data-can-leave="${N?1:0}" title="Campaign options" aria-haspopup="menu">\u2699</button>`:""}
1578          ${P?`<div class="cc2-gm-name" title="${escHtml(P)}">${escHtml(P)}</div>`:""}
1579          ${be}
1580          <div class="cc2-cover-text">
1581            ${te?`<div class="cc2-region">${escHtml(te)}</div>`:""}
1582            <h3 class="cc2-title">${escHtml(I.name)}</h3>
1583          </div>
1584        </div>
1585        <div class="cc2-body">
1586          ${le?`<div class="cc2-party">${escHtml(le)}</div>`:""}
1587          ${se}
1588          ${T?"":G}
1589          <div class="cc2-foot">
1590            <span class="cc2-sessions">${escHtml(me)}</span>
1591            ${Le}
1592          </div>
1593          ${X}
1594        </div>
1595        ${T?"":Q}
1596      </div>`},m=(I,T=!1)=>{if(T===!0)return p(I,!0);const P=(I.gm_credit_name||"").trim()||(I.gm_display_name||"").trim()||(I.gm_discord_username||"").trim()||((I.gm_email||"").split("@")[0]||"").trim(),B=!!I.cover_image_url,H=I.user_role==="gm"||I.user_role==="co_gm"||!!currentUser?.is_admin,A=I.user_role==="member"||I.user_role==="co_gm"||I.user_role==="observer",x=(I.anthology_world_label||I.region_label||"").trim();let M=(I.party_name||"").trim();/^the (company|party)$/i.test(M)&&(M="");const D=Number(I.session_count)||0,O=D===0?"No sessions yet":D===1?"1 session":`${D} sessions`,N=_csRelDay(I.last_session_date),G=_csFutureSessionZero(I)?"Session 0 Scheduled":N?`Last played ${N}`:I.created_at?"Created "+new Date(I.created_at).toLocaleDateString():"",F=["-1.3","1.1","-0.6","0.8","-1.6"][Math.abs(Number(I.id)||0)%5],W=Array.isArray(I.pc_names)?I.pc_names.filter(
1596Boolean):[],z=6;let V=W.slice(0,z).map(ne=>`<span class="bnd-tab">${escHtml(ne)}</span>`).join("");W.length>z&&(V+=`<span class="bnd-tab bnd-tab--more" title="${escHtml(W.slice(z).join(", "))}">+${W.length-z}</span>`);const K=I.gm_brand_logo_url||I.gm_brand?.logo_url,se=I.gm_brand?.name||"",Q=_cc2CaptureKey(I),X=Q&&_CC2_CAPTURE_GLYPHS[Q.key],te=X?`<div class="cc2-capture bnd-capture" title="${Q.declared?X.gt:X.last}">${X.svg}</div>`:"",le='<span class="bnd-id">'+(K?`<img class="bnd-partner" src="${escHtml(K)}" alt="${escHtml(se)}"${se?` title="${escHtml(se)}"`:""} loading="lazy"/>`:"")+(P?`<span class="bnd-gm" title="${escHtml(P)}">GM ${escHtml(P)}</span>`:"")+"</span>";let ue="";if(I.pending_recap&&I.pending_recap.session_id){const ne=I.pending_recap,we=ne.session_title&&ne.session_title.trim()?ne.session_title.trim():ne.session_number?`Session ${ne.session_number}`:"Last session";ue=`<span class="cc-pending bnd-note bnd-note--recap" data-pending-recap="${ne.session_id}"><svg class="bnd-recap-doodle" viewBox="0 0 60 52" aria-hidden="true"><path d="M7 20c1.8 10 2.8 17.4 3 22" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round"/><circle cx="10.8" cy="49.6" r="3.2" fill="currentColor"/><path d="M19 38C26 29 35 20 46 14" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/><path d="M37 13.4c4.6-.3 7.4 0 8.9.6-1.3 1.6-2.3 4-2.8 7.4" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="bnd-note-main"><span class="bnd-underline">Ready for recap</span></span><span class="bnd-note-sub">${escHtml(we)}</span></span>`}let me="";const de=_csNextSession(I);if(de){const ne=de.near?"yellow":"blue";let we=escHtml(de.date);const Me=de.date.match(/^(This|Today|Tomorrow)\b/);Me&&(we=`<span class="bnd-underline">${escHtml(Me[0])}</span>${escHtml(de.date.slice(Me[0].length))}`),me=`<span class="bnd-note bnd-note--next bnd-note--${ne}"><span class="bnd-note-main">${we}</span><span class="bnd-note-sub">${escHtml(de.time)}${de.cadenceShort?`&nbsp;<svg class="bnd-note-rpt" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.9L13.5 5.5"/><path d="M13.5 2v3.5H10"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.9L2.5 10.5"/><path d="M2.5 14v-3.5H6"/></svg>&nbsp;${escHtml(de.cadenceShort)}`:""}</span></span>`}let ye="";if(_csSortKey==="anthology"&&I.anthology_id){const ne=I.anthology_active_campaign_id===I.id,we=(I.anthology_era_label||"").trim();ye=`<div class="bnd-antho"><span class="cca-pill ${ne?"cca-pill--active":"cca-pill--legacy"}">${ne?"Active":"Legacy"}</span><span class="bnd-antho-txt">${escHtml(I.anthology_title||"")}${we?" \xB7 "+escHtml(we):""}</span></div>`}const Le=H||A?`<button class="cc-gear camp-settings-btn bnd-gear" data-id="${I.id}" data-name="${escHtml(I.name)}" data-can-edit="${H?1:0}" data-can-leave="${A?1:0}" title="Campaign options" aria-haspopup="menu"><span class="bnd-gear-ear" aria-hidden="true"></span>\u2699</button>`:"",Se=B?'<span class="bnd-face-text">'+(x?`<span class="bnd-region">${escHtml(x)}</span>`:"")+`<span class="bnd-title">${escHtml(I.name)}</span>`+(M?`<span class="bnd-party">${escHtml(M)}</span>`:"")+"</span>":'<span class="bnd-tape">'+(x?`<span class="bnd-tape-region">${escHtml(x)}</span>`:"")+`<span class="bnd-tape-title">${escHtml(I.name)}</span>`+(M?`<span class="bnd-tape-party">${escHtml(M)}</span>`:"")+"</span>",he=I.game_system==="daggerheart"?'<svg class="bnd-sys bnd-sys--dagger" viewBox="0 0 30 28" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="3" r="1.5"/><path d="M8 4.5V8"/><path d="M3.5 8.3h9"/><path d="M6.3 9.4 8 25.5 9.7 9.4"/><path d="M8 11v11"/><path d="M23 17.5c-2.3-1.8-3.8-3.1-3.8-4.9a2.05 2.05 0 0 1 3.8-1.1 2.05 2.05 0 0 1 3.8 1.1c0 1.8-1.5 3.1-3.8 4.9Z"/></svg>':'<span class="bnd-sys bnd-sys--amp">&amp;</span>',be=escHtml(`${I.name}${M?", "+M:""}${P?", GM "+P:""}. Press Enter to open.`);return`
1597      <div class="cc-card bnd-cell ${B?"has-cover":"no-cover"}" data-id="${I.id}" role="button" tabindex="0" aria-label="${be}" style="--bnd-rz:${F}deg;${_bndColorVars(I.binder_color)}">
1598        <div class="bnd-tilt">
1599          <span class="bnd-back" aria-hidden="true"></span>
1600          <span class="bnd-spine-out" aria-hidden="true"></span>
1601          <span class="bnd-pages" aria-hidden="true"></span>
1602          ${V?`<div class="bnd-tabs">${V}</div>`:""}
1603          ${Le}
1604          <div class="bnd-cover">
1605            <span class="bnd-spine" aria-hidden="true">${he}</span>
1606            <span class="bnd-hinge" aria-hidden="true"></span>
1607            <span class="bnd-art">
1608              ${B?'<span class="bnd-art-grad" aria-hidden="true"></span>':'<svg class="bnd-mark" viewBox="0 0 556 330" aria-hidden="true"><use href="#tf-mark"/></svg>'}
1609              <span class="bnd-top">
1610                <span class="bnd-meta"><span class="bnd-meta-line">${escHtml(O)}</span>${G?`<span class="bnd-meta-line bnd-meta-line--dim">${escHtml(G)}</span>`:""}${te}</span>
1611                ${le}
1612              </span>
1613              ${Se}
1614            </span>
1615            <span id="camp-live-${I.id}" class="cc-live bnd-ribbon hidden" aria-live="polite"><span class="cc-live-dot"></span><span class="cc-live-label">Live</span></span>
1616            ${ue}
1617            ${me}
1618          </div>
1619        </div>
1620        ${ye}
1621      </div>`},f=(I,T=!1)=>_csViewMode==="binders"&&T!==!0?m(I):p(I,T),g=I=>{const T=_csShelfDate(I);return T?String(T.getFullYear()):""},h=I=>I>=30?[80,275]:I>=15?[68,250]:[56,224];let v=new Set,b={};const y=I=>{if(!(I>0))return"";const T=[[1e3,"M"],[900,"CM"],[500,"D"],[400,"CD"],[100,"C"],[90,"XC"],[50,"L"],[40,"XL"],[10,"X"],[9,"IX"],[5,"V"],[4,"IV"],[1,"I"]];let P="";for(const[B,H]of T)for(;I>=B;)P+=H,I-=B;return P},_='<svg class="cs-spine-imprint" viewBox="0 0 556 330" aria-hidden="true"><use href="#tf-mark"/></svg>',w=(I,T)=>{const P=_spineJewelVar(I.id),B=I.cover_image_url?" has-cover":"",H=escHtml(I.name);let A=(I.party_name||"").trim();/^the (company|party)$/i.test(A)&&(A="");const x=g(I.last_session_date||I.completed_at),M=(I.gm_credit_name||"").trim()||(I.gm_display_name||"").trim().split(/\s+/)[0]||"",D=M?`GM ${M}`:"",O=I.gm_brand&&I.gm_brand.logo_url?I.gm_brand.logo_url:"",N=!!I.anthology_ghost,G=N?" cs-spine--ghost":"",F=escHtml(N?`${I.name}${A?", "+A:""}${D?", "+D:""}, shared from this series, read-only. Press Enter to open.`:`${I.name}${A?", "+A:""}${D?", "+D:""}, completed ${x}. Press Enter to open.`),W=v.has((I.name||"").toLowerCase())&&x?x:"";if(T)return`<div class="cs-spine cs-spine--flat${G}${B}" data-id="${I.id}" role="button" tabindex="0" aria-label="${F}" style="--spine-fallback:${P}"><span class="cs-spine-title">${H}</span>`+(W?`<span class="cs-spine-year">${escHtml(W)}</span>`:"")+"</div>";const[z,V]=h(Number(I.session_count)||0),K=b[I.id]||"";return`<div class="cs-spine cs-spine--w${z}${G}${B}" data-id="${I.id}" role="button" tabindex="0" aria-label="${F}" style="--spine-fallback:${P};--spine-w:${z}px;--spine-h:${V}px"><div class="cs-spine-head">`+(O?`<img class="cs-spine-gm-logo" src="${escHtml(O)}" alt="" aria-hidden="true" loading="lazy">`:"")+(D?`<span class="cs-spine-gm">${escHtml(D)}</span>`:"")+(K?`<span class="cs-spine-vol">${K}</span>`:"")+`</div><div class="cs-spine-type"><span class="cs-spine-title">${H}</span>`+(A?`<span class="cs-spine-party">${escHtml(A)}</span>`:"")+'</div><div class="cs-spine-foot">'+(W?`<span class="cs-spine-year">${escHtml(W)}</span>`:"")+'<span class="cs-spine-pubby">Published by</span>'+_+"</div></div>"};_csPreviewBuilders={binder:m,card:p,spine:w,faceout:null};let $;if(_csSortKey==="session"){const I=Date.now()-1296e6,T=[],P=[];for(const B of l){const H=B.last_session_date?_csShelfDate(B.last_session_date):null,A=H?H.getTime():NaN;Number.isFinite(A)&&A>=I?T.push(B):P.push(B)}T.length&&P.length?$='<span class="form-section-label cs-group-eyebrow">Active (last 15 days)</span>'+T.map(B=>f(B)).join("")+'<span class="form-section-label cs-group-eyebrow">Older</span>'+P.map(B=>f(B)).join(""):$=l.map(B=>f(B)).join("")}else if(_csSortKey==="next"){const I=[],T=[];for(const P of l)_csUpcomingTs(P)!=null?I.push(P):T.push(P);I.length&&T.length?$='<span class="form-section-label cs-group-eyebrow">Scheduled</span>'+I.map(P=>f(P)).join("")+'<span class="form-section-label cs-group-eyebrow">No session scheduled</span>'+T.map(P=>f(P)).join(""):$=l.map(P=>f(P)).join("")}else if(_csSortKey==="anthology"){const I=new Map,T=[];for(const H of l)H.anthology_id&&H.anthology_title?(I.has(H.anthology_title)||I.set(H.anthology_title,[]),I.get(H.anthology_title).push(H)):T.push(H);const P=(H,A)=>{const x=H.anthology_era_order,M=A.anthology_era_order;return x==null&&M==null?(H.name||"").localeCompare(A.name||""):x==null?1:M==null?-1:x-M||(H.name||"").localeCompare(A.name||"")},B=[];for(const H of[...I.keys()].sort((A,x)=>A.localeCompare(x)))B.push('<span class="form-section-label cs-group-eyebrow">'+escHtml(H)+"</span>"),B.push(I.get(H).sort(P).map(A=>f(A)).join(""));T.length&&(I.size&&B.push('<span class="form-section-label cs-group-eyebrow">Not in an anthology</span>'),B.push([...T].sort((H,A)=>(H.name||"").localeCompare(A.name||"")).map(H=>f(H)).join(""))),$=B.join("")}else $=l.map(I=>f(I)).join("");if(c.length&&($='<span class="form-section-label cs-group-eyebrow">Live now</span>'+c.map(I=>f(I)).join("")+$),i.length||_csShelfMode==="public"){b={};const I={};{const Q={};
1621n.concat(s).forEach(te=>{te.anthology_id&&(Q[te.anthology_id]=Q[te.anthology_id]||[]).push(te)});const X=(te,le)=>{const ue=te.anthology_era_order,me=le.anthology_era_order;return ue!=null&&me!=null&&ue!==me?ue-me:ue!=null&&me==null?-1:ue==null&&me!=null?1:(te.id||0)-(le.id||0)};Object.values(Q).forEach(te=>{te.sort(X).forEach((le,ue)=>{b[le.id]=y(ue+1),I[le.id]=ue+1})})}const T={};i.forEach(Q=>{const X=(Q.name||"").toLowerCase();T[X]=(T[X]||0)+1}),v=new Set(Object.keys(T).filter(Q=>T[Q]>1));const P=Q=>{const X=_csShelfDate(Q.last_session_date)||_csShelfDate(Q.completed_at);return X?X.getTime():0},B={};i.forEach(Q=>{if(Q.anthology_id){const X=P(Q);B[Q.anthology_id]=Math.min(B[Q.anthology_id]==null?1/0:B[Q.anthology_id],X)}});const H=Q=>Q.anthology_id?B[Q.anthology_id]||0:P(Q),A=[...i].sort((Q,X)=>H(Q)-H(X)||(Q.anthology_id||0)-(X.anthology_id||0)||(I[Q.id]||0)-(I[X.id]||0)||P(Q)-P(X)||(Q.name||"").localeCompare(X.name||"")),x=A.filter(Q=>Q.anthology_id||(Number(Q.session_count)||0)>5),M=A.filter(Q=>!Q.anthology_id&&(Number(Q.session_count)||0)<=5);let D="";for(let Q=0;Q<M.length;Q+=4)D+=`<div class="cs-flat-stack">${M.slice(Q,Q+4).map(X=>w(X,!0)).join("")}</div>`;const O=new Set(["a","an","the","and","or","nor","but","&","of","in","at","on","to","for","from","with","as","into","onto","upon","near","under","over","beyond"]),N=Q=>{const X=String(Q||"").trim().split(/\s+/).filter(Boolean),te=X.some(de=>!O.has(de.toLowerCase())),le=(de,ye)=>'<span class="fo-word '+(ye?"fo-minor":"fo-major")+'">'+escHtml(de)+"</span>";let ue=0,me="";if(te&&O.has((X[0]||"").toLowerCase())){const de=[];for(;ue<X.length&&O.has(X[ue].toLowerCase());)de.push(X[ue++]);me='<span class="fo-lead">'+de.map(ye=>le(ye,!0)).join(" ")+"</span>"}return me+X.slice(ue).map(de=>le(de,te&&O.has(de.toLowerCase()))).join(" ")},G=Q=>{const[,X]=h(Number(Q.session_count)||0),te=(Q.gm_credit_name||"").trim()||(Q.gm_display_name||"").trim().split(/\s+/)[0]||"";return`<div class="cs-faceout" data-id="${Q.id}" aria-hidden="true" style="--spine-h:${X}px"><span class="cs-faceout-title">${N(Q.name)}</span>`+(te?`<span class="cs-faceout-author">GM ${escHtml(te)}</span>`:"")+"</div>"},F=_csShelfMode==="public";_csPreviewBuilders&&(_csPreviewBuilders.faceout=G);let W;{const Q=x[x.length-1];W=!F&&Q&&Q.cover_image_url&&(window.innerWidth||0)>=480?x.slice(0,-1).map(te=>w(te,!1)).join("")+'<div class="cs-book">'+w(Q,!1)+G(Q)+"</div>":x.map(te=>w(te,!1)).join("")}const z=F?(_csStacks||[]).map(_publicSpineHtml).join("")||'<p class="cs-shelf-empty">The stacks are bare, nothing published yet</p>':W+D,V=F?"Public Library":"On the Shelf",K=F?(_csStacks||[]).length:i.length,se=F||_csShelfOpen||r.length===0?" open":"";$+=`<details class="cs-shelf-group"${se}><summary class="form-section-label cs-group-eyebrow cs-shelf-summary">${V} <span class="cs-shelf-count">${K}</span></summary><div class="cs-shelf-body"><div class="cs-shelf-rail${F?" cs-shelf-rail--public":""}">${z}</div>${_csShelfMetaHtml()}</div></details>`}else $+=`<div class="cs-shelf-meta-bare">${_csShelfMetaHtml()}</div>`;const E=!0;$+=`<div class="cc-ghost-pair${E?" cc-ghost-pair--tri":""}">${_newCampaignCardHtml()}${E?_importCampaignCardHtml():""}${_joinCampaignCardHtml()}</div>`,t.innerHTML=$,_wireNewCampaignCard(),E&&_wireImportCampaignCard(),_wireJoinCampaignCard();const k=t.querySelector(".cs-shelf-group");k&&k.addEventListener("toggle",()=>{_csShelfOpen=k.open;try{localStorage.setItem("tf_shelf_open",_csShelfOpen?"1":"0")}catch{}}),_wireShelfSpines(t,i),_wirePublicSpines(t),_csWireShelfMeta(t);const C=new Map;t.querySelectorAll(".cc-card[data-id]").forEach(I=>{C.set(parseInt(I.dataset.id),I)}),[...r,...a].forEach(I=>{if(!I.cover_image_url)return;const T=C.get(I.id);T&&(T.style.setProperty("--cover-img",`url("${I.cover_image_url.replace(/"/g,"%22")}")`),_applyCoverFramingVars(T,I))}),t.querySelectorAll(".cc-card[data-id]").forEach(I=>{I.addEventListener("click",async T=>{T.target.closest(".camp-settings-btn, .cc-gear, .campaign-enter-btn")||(await bootWithCampaignId(parseInt(I.dataset.id)),applyRoleUI())}),I.tabIndex=0,I.addEventListener("keydown",async T=>{T.key!=="Enter"&&T.key!==" "||T.target.closest(".camp-settings-btn, .cc-gear, .campaign-enter-btn")||(T.preventDefault(),await bootWithCampaignId(parseInt(I.dataset.id)),applyRoleUI())})}),t.querySelectorAll(".camp-settings-btn").forEach(I=>{I.addEventListener("click",T=>{T.stopPropagation(),T.preventDefault(),_openCampCardMenu(I)})});const S={session:"Last Session",next:"Next Session",recent:"Last Active",name:"A\u2013Z",created:"Created",role:"My Role",anthology:"By Anthology"},q=document.getElementById("campaign-sort-btn");q&&(q.textContent=S[_csSortKey]||"Last Active"),_renderAnthologiesSection()}function _csShelfDate(e){if(!e)return null;const t=new Date(e.length===10?e+"T12:00:00":e);return isNaN(t.getTime())?null:t}function _wireShelfSpines(e,t){const n=e.querySelector(".cs-shelf-rail");if(!n)return;const a=new Map((t||[]).map(h=>[h.id,h])),s=_tfSpineCoverUrl;n.querySelectorAll(".cs-spine.has-cover").forEach(h=>{const v=a.get(parseInt(h.dataset.id));v&&v.cover_image_url&&h.style.setProperty("--spine-cover",`url("${s(v.cover_image_url).replace(/"/g,"%22")}")`)}),n.querySelectorAll(".cs-faceout").forEach(h=>{const v=a.get(parseInt(h.dataset.id));if(!v||!v.cover_image_url)return;const b=String(v.cover_image_url),y=b.includes("/image/upload/")?b.replace("/image/upload/","/image/upload/c_fill,g_auto,ar_2:3,w_400,q_auto:good/"):b;h.style.setProperty("--faceout-cover",`url("${y.replace(/"/g,"%22")}")`)});{const h=n.querySelector(".cs-faceout");if(h){const y=parseFloat(getComputedStyle(h).getPropertyValue("--spine-h"))||224,w=600/(600+y*.68*Math.sin(62*Math.PI/180)
1621);h.style.setProperty("--fo-angle","62deg"),h.style.setProperty("--fo-persp","600px"),n.style.setProperty("--shelf-back-y",(10+(y-10)/2*(1-w)).toFixed(1)+"px")}}{const h=n.querySelector(".cs-book");if(h){const v=h.querySelector(".cs-spine"),b=h.querySelector(".cs-faceout"),y=_=>{v&&v.classList.toggle("is-book-hover",_),b&&b.classList.toggle("is-book-hover",_)};h.addEventListener("pointerenter",_=>{y(!0),_.pointerType==="mouse"&&v&&m(v)}),h.addEventListener("pointerleave",_=>{y(!1),_.pointerType==="mouse"&&(d=setTimeout(f,140))})}}n.querySelectorAll(".cs-spine.has-cover, .cs-faceout").forEach(h=>{const v=a.get(parseInt(h.dataset.id));!v||!v.cover_image_url||_spineEdgeFor(v.cover_image_url).then(b=>{b&&h.style.setProperty("--spine-edge",b)})});const i=()=>{n.querySelectorAll(".cs-spine:not(.cs-spine--flat) .cs-spine-title").forEach(v=>{const b=v.parentElement;if(!b||b.clientHeight<20)return;v.style.fontSize="";const y=b.clientHeight,_=b.querySelector(".cs-spine-party"),w=_?_.getBoundingClientRect().width+8:0,$=b.clientWidth-8-w;let E=parseFloat(getComputedStyle(v).fontSize)||14;for(let k=0;k<60&&E>9&&!(v.scrollWidth<=$-2&&v.scrollHeight<=y);k++)E-=.5,v.style.fontSize=E+"px"});const h=n.querySelector(".cs-faceout");h&&_tfFitFaceoutTitle(h)};i(),requestAnimationFrame(i),document.fonts&&document.fonts.ready&&document.fonts.ready.then(i);const o=n.closest(".cs-shelf-group");o&&o.addEventListener("toggle",()=>{o.open&&requestAnimationFrame(i)});const r=!window.matchMedia||window.matchMedia("(hover: hover)").matches;let c=n.querySelector(".cs-shelf-popover");c||(c=document.createElement("div"),c.className="cs-shelf-popover hidden",c.id="cs-shelf-pop",c.setAttribute("role","tooltip"),n.appendChild(c));let l=null,d=null;const u=h=>h&&!isNaN(h.getTime())?h.toLocaleDateString(void 0,{month:"short",year:"numeric"}):"",p=h=>{const v=escHtml((h.region_label||h.anthology_world_label||"").trim());let b=(h.party_name||"").trim();/^the (company|party)$/i.test(b)&&(b="");const y=Array.isArray(h.pc_names)?h.pc_names.filter(Boolean):[],_=y.slice(0,6).map(S=>`<span class="cs-pop-pc">${escHtml(S)}</span>`).join("")+(y.length>6?`<span class="cs-pop-pc cs-pop-pc--more">+${y.length-6}</span>`:""),w=Number(h.session_count)||0,$=_csShelfDate(h.last_session_date)||_csShelfDate(h.completed_at),E=_csShelfDate(h.first_session_date)||_csShelfDate(h.created_at),k=[u(E),u($)].filter(Boolean).join(" \u2192 "),C=h.anthology_ghost&&!h.completed_at?"":$?`Completed ${u($)}`:"Completed";return`<div class="cs-pop-huebar" style="--spine-jewel:${_spineJewelVar(h.id)}"></div>`+(v?`<div class="cs-pop-region">${v}</div>`:"")+`<div class="cs-pop-title">${escHtml(h.name)}</div>`+(b?`<div class="cs-pop-party">${escHtml(b)}</div>`:"")+(h.gm_display_name?`<div class="cs-pop-gm">Run by ${escHtml(h.gm_display_name)}</div>`:"")+(C?`<span class="cc-shelved-stamp cs-pop-stamp">${escHtml(C)}</span>`:"")+(_?`<div class="cs-pop-pcs">${_}</div>`:"")+`<div class="cs-pop-meta">${w} session${w===1?"":"s"}${k?" \xB7 "+k:""}</div>`+(h.anthology_ghost?'<div class="cs-pop-ghost-note">Shared from this series \xB7 read-only</div>':"")+`<button class="cs-pop-open btn-ghost btn-xs" data-id="${h.id}">Open \u2192</button>`},m=h=>{const v=a.get(parseInt(h.dataset.id));if(!v||(clearTimeout(d),l===h&&!c.classList.contains("hidden")))return;c.innerHTML=p(v),c.classList.remove("hidden"),l=h,h.setAttribute("aria-describedby","cs-shelf-pop");const b=n.getBoundingClientRect(),y=h.getBoundingClientRect(),_=c.offsetWidth,w=c.offsetHeight;let $=y.left-b.left+y.width/2-_/2;$=Math.max(4,Math.min($,n.clientWidth-_-4));let E=y.top-b.top-w-8;E<0&&(E=y.bottom-b.top+8),c.style.left=$+"px",c.style.top=E+"px"},f=()=>{c.classList.add("hidden"),l&&l.removeAttribute("aria-describedby"),l=null},g=async h=>{await bootWithCampaignId(h),applyRoleUI()};n.querySelectorAll(".cs-spine:not(.cs-spine--public), .cs-faceout").forEach(h=>{const v=parseInt(h.dataset.id);let b="";h.addEventListener("pointerdown",w=>{b=w.pointerType||""});const y=!!h.closest(".cs-book"),_=h.classList.contains("cs-faceout")&&n.querySelector(".cs-book .cs-spine")||h;y||(h.addEventListener("pointerenter",w=>{w.pointerType==="mouse"&&m(_)}),h.addEventListener("pointerleave",w=>{w.pointerType==="mouse"&&(d=setTimeout(f,140))})),h.addEventListener("focus",()=>m(_)),h.addEventListener("blur",()=>{d=setTimeout(f,140)}),h.addEventListener("click",w=>{if(w.target.closest(".cs-pop-open"))return;(w.pointerType||b||(r?"mouse":"touch"))==="mouse"?g(v):c.classList.contains("hidden")||l!==h?m(h):g(v)}
1621),h.addEventListener("keydown",w=>{w.key==="Enter"||w.key===" "?(w.preventDefault(),g(v)):w.key==="Escape"&&(f(),h.blur())})}),c.addEventListener("pointerenter",()=>clearTimeout(d)),c.addEventListener("pointerleave",h=>{h.pointerType==="mouse"&&(d=setTimeout(f,140))}),c.addEventListener("click",h=>{const v=h.target.closest(".cs-pop-open");v&&g(parseInt(v.dataset.id))}),window._shelfOutsideWired||(window._shelfOutsideWired=!0,document.addEventListener("click",h=>{h.target.closest(".cs-spine, .cs-shelf-popover")||document.querySelectorAll(".cs-shelf-popover:not(.hidden)").forEach(v=>v.classList.add("hidden"))}))}let _csShelfMode="mine";function _csShelfMetaHtml(){return'<div class="cs-shelf-meta hidden" id="cs-shelf-meta"><span id="cs-shelf-antho"></span><button type="button" id="cs-shelf-toggle" class="cs-shelf-toggle hidden" aria-pressed="false"><svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><rect x="1.5" y="2.5" width="3" height="11"/><rect x="6" y="2.5" width="3" height="11"/><path d="M10.6 3.4l2.8-.7 2.1 10.4-2.8.7z"/></svg></button></div>'}function _csSyncShelfMeta(){const e=document.getElementById("cs-shelf-meta");if(!e)return;const t=document.getElementById("cs-shelf-toggle"),n=_csShelfMode==="public";t&&(t.classList.toggle("hidden",!(_csStacks||[]).length&&!n),t.setAttribute("aria-pressed",n?"true":"false"),t.title=n?"Return to your own campaign shelf":"Browse the Public Library: campaigns published by other tables",t.setAttribute("aria-label",n?"Back to my shelf":"Browse the Public Library"));const a=document.getElementById("cs-shelf-antho"),s=!!(a&&a.innerHTML),i=!!(t&&!t.classList.contains("hidden"));e.classList.toggle("hidden",!s&&!i)}function _csWireShelfMeta(e){e.querySelector("#cs-shelf-toggle")?.addEventListener("click",()=>{_csShelfMode=_csShelfMode==="public"?"mine":"public",Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns)}),_csSyncShelfMeta()}const _spineEdgeCache=new Map;function _spineEdgeFor(e){return _spineEdgeCache.has(e)?Promise.resolve(_spineEdgeCache.get(e)):new Promise(t=>{const n=String(e).includes("/image/upload/")?String(e).replace("/image/upload/","/image/upload/w_24,q_auto:low/"):e;_loadImagePixels(n,24,a=>{let s=null;try{if(a&&a.data&&a.cw&&a.ch){const i=Math.max(0,a.cw-Math.max(1,Math.round(a.cw*.25))),o=[],r=[],c=[];for(let l=0;l<a.ch;l++)for(let d=i;d<a.cw;d++){const u=(l*a.cw+d)*4;o.push(a.data[u]),r.push(a.data[u+1]),c.push(a.data[u+2])}if(o.length){const l=y=>(y.sort((_,w)=>_-w),y[y.length>>1]),d=l(o)/255,u=l(r)/255,p=l(c)/255,m=Math.max(d,u,p),f=Math.min(d,u,p);let g=0,h=0,v=(m+f)/2;const b=m-f;b>0&&(h=v===0||v===1?0:b/(1-Math.abs(2*v-1)),m===d?g=(u-p)/b%6:m===u?g=(p-d)/b+2:g=(d-u)/b+4,g=(g*60+360)%360),h>=.04&&(v=Math.min(.6,Math.max(.18,v)),s="hsl("+Math.round(g)+" "+Math.round(h*100)+"% "+Math.round(v*100)+"%)")}}}catch{}try{console.debug("[spine-edge]",s||"fallback:gold",e)}catch{}_spineEdgeCache.set(e,s),t(s)})})}function _publicSpineHtml(e){const t=escHtml(e.title||e.name||"A chronicle"),n=(e.gm_credit_name||"").trim(),a=_csShelfDate(e.published_at),s=a?String(a.getFullYear()):"";let i=0;for(const c of String(e.slug||""))i=(i*31+c.charCodeAt(0))%997;const o=e.cover_image_url?" has-cover":"",r=escHtml(`${e.title||e.name||"A chronicle"}${n?", GM "+n:""}, published to the Public Library. Press Enter to read.`);return`<div class="cs-spine cs-spine--w56 cs-spine--public${o}" data-slug="${escHtml(e.slug||"")}" role="link" tabindex="0" aria-label="${r}" title="Read ${t}" style="--spine-fallback:${_spineJewelVar(i)};--spine-w:56px;--spine-h:224px"><div class="cs-spine-head">`+(n?`<span class="cs-spine-gm">GM ${escHtml(n)}</span>`:"")+`</div><div class="cs-spine-type"><span class="cs-spine-title">${t}</span></div><div class="cs-spine-foot">`+(s?`<span class="cs-spine-year">${escHtml(s)}</span>`:"")+'<span class="cs-spine-pubby">Published by</span><svg class="cs-spine-imprint" viewBox="0 0 556 330" aria-hidden="true"><use href="#tf-mark"/></svg></div></div>'}function _wirePublicSpines(e){const t=n=>{n&&window.open("/c/"+encodeURIComponent(n),"_blank","noopener")};e.querySelectorAll(".cs-spine--public").forEach(n=>{const a=n.dataset.slug,s=(_csStacks||[]).find(i=>i.slug===a);if(s&&s.cover_image_url){const i=String(s.cover_image_url),o=i.includes("/image/upload/")?i.replace("/image/upload/","/image/upload/c_fill,g_auto,ar_2:5,w_360,q_auto:good/"):i;
1621n.style.setProperty("--spine-cover",`url("${o.replace(/"/g,"%22")}")`),_spineEdgeFor(s.cover_image_url).then(r=>{r&&n.style.setProperty("--spine-edge",r)})}n.addEventListener("click",()=>t(a)),n.addEventListener("keydown",i=>{(i.key==="Enter"||i.key===" ")&&(i.preventDefault(),t(a))})})}function _tfMarkInternalBrowser(){if(currentUser?.is_admin)try{const e=localStorage.getItem("tf_vid")||"";if(e.startsWith("internal-"))return;const t=e||(window.crypto&&crypto.randomUUID?crypto.randomUUID():Date.now()+"-"+Math.random().toString(36).slice(2));localStorage.setItem("tf_vid","internal-"+t)}catch{}}function _pingLandingHit(){try{if(sessionStorage.getItem("tf_landing_pinged"))return;sessionStorage.setItem("tf_landing_pinged","1")}catch{}let e="";try{e=localStorage.getItem("tf_vid")||""}catch{}if(!e){e=window.crypto&&crypto.randomUUID?crypto.randomUUID():Date.now()+"-"+Math.random().toString(36).slice(2);try{localStorage.setItem("tf_vid",e)}catch{}}try{fetch("/api/track/landing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({visitor_id:e,referrer:document.referrer||"",path:(location.pathname||"/")+(location.search||"")+(location.hash||""),qc_code:window._tfQcLanding||""}),keepalive:!0}).catch(()=>{})}catch{}}let _tfLandingEvSent=null;function _tfLandingEvent(e,t,n){try{if(!_tfLandingEvSent)return;const a=n?e+"|"+(t||""):e;if(e!=="dwell"){if(_tfLandingEvSent.has(a))return;_tfLandingEvSent.add(a)}let s="";try{s=localStorage.getItem("tf_vid")||""}catch{}if(!s)return;const i=JSON.stringify({visitor_id:s,event:e,detail:t||""});navigator.sendBeacon?navigator.sendBeacon("/api/track/landing-event",new Blob([i],{type:"application/json"})):fetch("/api/track/landing-event",{method:"POST",headers:{"Content-Type":"application/json"},body:i,keepalive:!0}).catch(()=>{})}catch{}}function _tfArmLandingEvents(){try{if(_tfLandingEvSent)return;_tfLandingEvSent=new Set;const e=document.getElementById("auth-screen");if(!e)return;e.addEventListener("click",s=>{const i=s.target;if(!i||!i.closest)return;const o=i.closest("[data-tfa-tab]");if(o)return _tfLandingEvent("carousel_tab",o.dataset.k||"",!0);if(i.closest(".tfa-frame-link")){const l=i.closest("[data-tfa-slide]");return _tfLandingEvent("showcase_click",l&&l.dataset.k||"frame",!0)}if(i.closest(".tfa-cobrand-link"))return _tfLandingEvent("showcase_click","cobrand",!0);if(i.closest(".tfa-plans-link"))return _tfLandingEvent("pricing_view");if(i.closest(".js-privacy-info"))return _tfLandingEvent("privacy_open");if(i.closest("#auth-register-btn"))return _tfLandingEvent("signup_submit");const c=i.closest(".tfa-footer a");if(c)return _tfLandingEvent("footer_link",c.getAttribute("href")||"",!0)},!0),e.addEventListener("input",s=>{const i=s.target&&s.target.id;i==="reg-email"||i==="reg-password"?_tfLandingEvent("signup_start"):(i==="auth-email"||i==="auth-password")&&_tfLandingEvent("login_start")},!0);const t=Date.now();let n=!1;const a=()=>{n||(n=!0,_tfLandingEvent("dwell",String(Math.max(0,Math.round((Date.now()-t)/1e3)))))};window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&a()})}catch{}}function _tfLiveBusy(){try{if(typeof liveRecording<"u"&&liveRecording||typeof _tfHeldRecording<"u"&&_tfHeldRecording||typeof _broadcastActive<"u"&&_broadcastActive)return!0;const e=document.getElementById("live-session-screen");return!!(e&&!e.classList.contains("hidden"))}catch{return!0}}window._tfLiveBusy=_tfLiveBusy;function _tfSafeToReload(){try{if(!window.__tfUpdatePending||_tfLiveBusy()||document.querySelector(".modal-overlay:not(.hidden)")||document.querySelector(".editor:not(.hidden)")||Array.from(document.querySelectorAll("textarea")).some(t=>t.offsetParent!==null&&t.value.trim()))return!1;const e=document.activeElement;return!(e&&/^(input|textarea|select)$/i.test(e.tagName||""))}catch{return!1}}document.addEventListener("visibilitychange",()=>{document.hidden&&_tfSafeToReload()&&location.reload()});let _tfaCarTimer=null,_tfaCarHover=!1,_tfaCarIdx=0,_tfaQuoteTimer=null,_tfaQuoteIdx=0;const _TFA_QUOTES=[{q:"I copied in notes from a three-year campaign and it sorted everything into quests. It knows which ones failed, knows the party\u2019s priorities. It\u2019s genius.",a:"a GM, 47 sessions into the campaign he imported"},{q:"I love a good quote section. It\u2019s like D&D trading cards.",a:"a player"},{q:"It makes me look like I know what I\u2019m doing!",a:"a GM running 10 campaigns, still convinced he\u2019s winging it"},{q:"I might not post notes moving forward. Threadfall documents everything.",a:"a GM"},{q:"It even gives us advice on how to use our characters and what we\u2019re weak against.",a:"a player, a Warlock who fails every Wisdom save anyway"}];function _tfaShowSlide(e){const t=document.querySelectorAll("#auth-screen [data-tfa-slide]"),n=document.querySelectorAll("#auth-screen [data-tfa-tab]");if(!t.length)return;_tfaCarIdx=(e+t.length)%t.length,t.forEach((i,o)=>
1621i.classList.toggle("tfa-slide--on",o===_tfaCarIdx)),n.forEach((i,o)=>i.classList.toggle("tfa-tab--on",o===_tfaCarIdx));const a=n[_tfaCarIdx],s=a&&a.parentElement;s&&s.scrollWidth>s.clientWidth+4&&s.scrollTo({left:Math.max(0,a.offsetLeft-(s.clientWidth-a.offsetWidth)/2),behavior:"smooth"})}function _tfaRestartCarTimer(){_tfaCarTimer||_tfaCarHover||window.matchMedia("(max-width: 980px)").matches||(_tfaCarTimer=setInterval(()=>_tfaShowSlide(_tfaCarIdx+1),5200))}function _tfaShowQuote(){const e=document.querySelector("#auth-screen [data-tfa-quote]");if(!e)return;const t=_TFA_QUOTES[_tfaQuoteIdx%_TFA_QUOTES.length],n=e.querySelector("[data-q]"),a=e.querySelector("[data-a]");n&&(n.textContent="\u201C"+t.q+"\u201D"),a&&(a.textContent="\xB7 "+t.a),e.style.animation="none",e.offsetWidth,e.style.animation=""}function _tfaInitAuthShowcase(){const e=document.getElementById("auth-screen");if(!e)return;const t=e.querySelectorAll("[data-tfa-tab]");if(t.length){if(e.dataset.tfaBound!=="1"){t.forEach((a,s)=>{a.addEventListener("click",()=>{clearInterval(_tfaCarTimer),_tfaCarTimer=null,_tfaShowSlide(s),_tfaRestartCarTimer()})});const n=e.querySelector(".tfa-carousel");n&&(n.addEventListener("mouseenter",()=>{_tfaCarHover=!0,_tfaCarTimer&&(clearInterval(_tfaCarTimer),_tfaCarTimer=null)}),n.addEventListener("mouseleave",()=>{_tfaCarHover=!1,_tfaRestartCarTimer()})),e.dataset.tfaBound="1",_tfaShowSlide(0)}_tfaRestartCarTimer(),_tfaQuoteTimer||(_tfaShowQuote(),_tfaQuoteTimer=setInterval(()=>{_tfaQuoteIdx++,_tfaShowQuote()},6500))}}const _tfaAuthObserver=new MutationObserver(()=>{const e=document.getElementById("auth-screen");e&&!e.classList.contains("hidden")&&!_tfaCarTimer&&_tfaInitAuthShowcase()});_tfaAuthObserver.observe(document.getElementById("auth-screen")||document.body,{attributes:!0,attributeFilter:["class"]}),document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("auth-screen");e&&!e.classList.contains("hidden")&&_tfaInitAuthShowcase()}),function(){const t=document.getElementById("tfa-privacy-modal");if(!t)return;const n=()=>t.classList.add("hidden");document.addEventListener("click",a=>{if(a.target.closest(".js-privacy-info")){a.preventDefault(),t.classList.remove("hidden");return}if(a.target.closest(".js-privacy-close")){n();return}a.target===t&&n()}),document.addEventListener("keydown",a=>{_tfModalEsc(a,t)&&n()})}();async function loadCampaignSelector(){if(location.pathname==="/"&&/^#(chronicle|codex|party|quests|atlas)\//.test(location.hash||""))try{history.replaceState({type:"selector"},"","/")}catch{}_presStopPoll(),_presStopMainHb(),_presStartSelPoll(),updateNavAvatar(),updateViewAsBar();const e=document.getElementById("campaign-list-screen"),t=document.getElementById("campaign-selector-empty"),n=document.getElementById("campaign-sort-btn"),a="tf_campaigns_"+(currentUser&&currentUser.id||"anon");let s=!1;try{const i=localStorage.getItem(a);if(i){const o=JSON.parse(i);Array.isArray(o)&&o.length&&(_csAllCampaigns=o,t.classList.add("hidden"),n&&n.classList.remove("hidden"),_renderCampaignCards(_csAllCampaigns),s=!0)}}catch{}!s&&!e.querySelector(".cc-new-card--editing")&&(e.innerHTML=_CS_SKEL_HTML[_csViewMode]||Array.from({length:4},()=>'<div class="cc-card cc-card--skeleton" aria-hidden="true"><span class="skel-line skel-line--name"></span><span class="skel-line skel-line--gm"></span><span class="skel-line skel-line--meta"></span></div>').join(""),t.classList.add("hidden"),n&&n.classList.add("hidden"));try{const i=await api("GET","/api/campaigns");_csAllCampaigns=i;try{localStorage.setItem(a,JSON.stringify(i))}catch{}i.length===0?(n&&n.classList.add("hidden"),_renderCampaignCards([])):(n&&n.classList.remove("hidden"),_renderCampaignCards(_csAllCampaigns),_presPollSelector().catch(()=>{})),t?.classList.add("hidden"),_applyCsCommunitySection(!0)}catch(i){e.innerHTML=`<p class="error placeholder--padded">${i.message}</p>`}}async function _tfDownloadBlob(e,t){const n={},a=getStoredToken();a&&(n.Authorization="Bearer "+a);const s=await fetch(e,{headers:n,credentials:"include"});if(!s.ok){let l="";try{l=(await s.json()).detail||""}catch{}const d=new Error(typeof l=="string"&&l?l:`Download failed (${s.status})`);throw d.status=s.status,d}const i=s.headers.get("Content-Disposition")||"",o=/filename="?([^";]+)"?/i.exec(i),r=await s.blob(),c=document.createElement("a");c.href=URL.createObjectURL(r),c.download=o&&o[1]||t,document.body.appendChild(c),c.click(),c.remove(),setTimeout(()=>URL.revokeObjectURL(c.href),2e3)}
1621async function _tfExportCampaign(e,t,n){if(!e||t&&t.disabled)return;const a=n||t&&t.textContent||"Export";t&&(t.disabled=!0,t.textContent="Preparing\u2026");try{await _tfDownloadBlob(`/api/campaigns/${e}/export`,"threadfall-campaign-export.zip")}catch(s){showToast(s&&s.status===429?"Too many exports in a row, wait a minute and try again.":"Export failed, try again.",4e3)}finally{t&&(t.disabled=!1,t.textContent=a)}}async function _renderProfileMyCampaigns(){const e=document.getElementById("profile-export-section"),t=document.getElementById("profile-export-divider"),n=document.getElementById("profile-export-list");if(!e||!n)return;let a=[];try{a=await api("GET","/api/campaigns")}catch{a=[]}const s=currentUser&&currentUser.id,i=(Array.isArray(a)?a:[]).filter(r=>currentUser&&currentUser.is_admin?r.primary_gm_id===s:r.user_role==="gm"||r.user_role==="co_gm");if(!i.length){n.innerHTML="",e.classList.add("hidden"),t?.classList.add("hidden");return}const o=r=>r==="daggerheart"?"Daggerheart":"D&amp;D 5e";n.innerHTML=i.map(r=>{const c=Number(r.session_count)||0;return`
1622    <div class="archived-campaign-row">
1623      <div>
1624        <div class="archived-campaign-name">${escHtml(r.name)}</div>
1625        <div class="archived-campaign-meta">${o(r.game_system)} \xB7 ${c} session${c===1?"":"s"}</div>
1626      </div>
1627      <button class="btn-new btn-xs" onclick="_tfExportCampaign(${r.id}, this, 'Export')">Export</button>
1628    </div>`}).join(""),e.classList.remove("hidden"),t?.classList.remove("hidden")}async function _renderProfileArchived(){const e=document.getElementById("profile-archived-section"),t=document.getElementById("profile-archived-divider"),n=document.getElementById("profile-archived-list");if(!e||!n)return;let a=[];try{a=await api("GET","/api/campaigns/archived")}catch{a=[]}if(!Array.isArray(a)||a.length===0){n.innerHTML="",e.classList.add("hidden"),t?.classList.add("hidden");return}n.innerHTML=a.map(s=>`
1629    <div class="archived-campaign-row">
1630      <div>
1631        <div class="archived-campaign-name">${escHtml(s.name)}</div>
1632        <div class="archived-campaign-meta">${s.owner?escHtml(s.owner)+" \xB7 ":""}${s.days_remaining} day(s) to restore</div>
1633      </div>
1634      <span class="quest-detail-meta-actions">
1635        <button class="btn-new btn-xs" onclick="_tfExportCampaign(${s.id}, this, 'Export')" title="Download this campaign's data before the restore window closes">Export</button>
1636        <button class="btn-new btn-xs" onclick="restoreCampaign(${s.id}, this)">Restore</button>
1637      </span>
1638    </div>`).join(""),e.classList.remove("hidden"),t?.classList.remove("hidden")}async function restoreCampaign(e,t){t.disabled=!0,t.textContent="Restoring\u2026";try{await api("POST",`/api/campaigns/${e}/restore`),showToast("Campaign restored."),_renderProfileArchived();const n=document.getElementById("campaign-selector");n&&!n.classList.contains("hidden")&&loadCampaignSelector()}catch(n){showToast("Restore failed: "+n.message),t.disabled=!1,t.textContent="Restore"}}document.getElementById("campaign-sort-btn").addEventListener("click",()=>{if(!_csAllCampaigns.length)return;const e=["next","session","recent","name","created","role","anthology"],t=e.indexOf(_csSortKey);_csSortKey=e[(t+1)%e.length],_renderCampaignCards(_csAllCampaigns)});let _csAnthologies=[],_anthModalView="list",_anthModalSelectedId=null;const _ANTHOLOGY_TIERS=["master","pro"];function _anthChevronSvg(e){return'<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="'+{up:"M2.5 7.5 6 4l3.5 3.5",down:"M2.5 4.5 6 8l3.5-3.5",right:"M4.5 2.5 8 6l-3.5 3.5"}[e]+'"/></svg>'}function _anthologyGmCampaignCount(){return(_csAllCampaigns||[]).filter(e=>e.user_role==="gm").length}let _csAnthologiesLoaded=!1;function _renderAnthologiesSection(){const e=document.getElementById("cs-shelf-antho");if(!e)return;const t=_anthologyGmCampaignCount(),n=_ANTHOLOGY_TIERS.includes(_tfBillingTier||"free"),a=!!(currentUser&&currentUser.is_admin);if(!t&&!a){e.innerHTML="",_csSyncShelfMeta();return}if(a||n&&t>=2){_csAnthologiesLoaded||(_csAnthologiesLoaded=!0,_loadAnthologies().then(_renderAnthologiesSection));const s=(_csAnthologies||[]).length>0;e.innerHTML='<button type="button" class="cs-shelf-antho-link" id="cs-shelf-antho-open" title="'+(s?"Manage your anthologies: group related campaigns into a shared timeline and link codex entries across them.":"Group related campaigns into a shared timeline and link codex entries across them.")+'">'+(s?"Manage Anthologies \u2192":"Start an Anthology \u2192")+"</button>",document.getElementById("cs-shelf-antho-open")?.addEventListener("click",openAnthologiesModal)}else n?e.innerHTML='<span class="cs-shelf-antho-locked" title="Anthologies bind campaigns you GM into one saga: a shared timeline, with codex entries linked across them. Start a second campaign to unlock.">Anthologies \xB7 Link codex + timeline across campaigns \xB7 Needs a second campaign you GM</span>':(e.innerHTML='<button type="button" class="cs-shelf-antho-locked cs-shelf-antho-locked--upsell" id="cs-shelf-antho-upsell" title="Anthologies bind campaigns you GM into one saga: a shared timeline, with codex entries linked across them. Unlocks on the Master tier with a second campaign as GM.">Anthologies \xB7 Link codex + timeline across campaigns \xB7 Unlocks on Master \u2192</button>',document.getElementById("cs-shelf-antho-upsell")?.addEventListener("click",()=>{typeof _showPricingScreen=="function"?_showPricingScreen():location.hash="#pricing"}));
1638_csSyncShelfMeta()}let _csAnthologiesLoadError=!1;async function _loadAnthologies(){try{const e=await api("GET","/api/anthologies");_csAnthologies=e&&e.anthologies||[],_csAnthologiesLoadError=!1}catch{_csAnthologies=[],_csAnthologiesLoadError=!0}}function openAnthologiesModal(){_anthModalView="list",_anthModalSelectedId=null,document.getElementById("anthologies-modal").classList.remove("hidden"),renderAnthologiesModal(),_loadAnthologies().then(renderAnthologiesModal)}function closeAnthologiesModal(){document.getElementById("anthologies-modal").classList.add("hidden")}document.getElementById("anthologies-modal-close")?.addEventListener("click",closeAnthologiesModal);let _anthModalMousedownOnBackdrop=!1;document.getElementById("anthologies-modal")?.addEventListener("mousedown",e=>{_anthModalMousedownOnBackdrop=e.target===document.getElementById("anthologies-modal")}),document.getElementById("anthologies-modal")?.addEventListener("click",e=>{e.target===document.getElementById("anthologies-modal")&&_anthModalMousedownOnBackdrop&&closeAnthologiesModal()});function _anthRestoreFocus(){const e=document.getElementById("anthologies-modal");if(!e||e.classList.contains("hidden")||e.contains(document.activeElement)&&document.activeElement!==document.body)return;const t=e.querySelector("#anthology-back-btn")||e.querySelector(".anthology-card")||document.getElementById("anthologies-modal-close");try{t?.focus({preventScroll:!0})}catch{}}function renderAnthologiesModal(){const e=document.getElementById("anthologies-modal-body"),t=document.getElementById("anthologies-modal-title");e&&(_anthModalView==="detail"&&_anthModalSelectedId!=null?(t&&(t.textContent="Anthology"),_renderAnthologyDetailVie
1638w(e)):(t&&(t.textContent="Manage Anthologies"),_renderAnthologiesListView(e)))}function _renderAnthologiesListView(e){const t=(_csAnthologies||[]).map(a=>{const s=Number(a.campaign_count)||0,i=s===1?"1 campaign":s+" campaigns",o=a.type==="concurrent"?"Concurrent":"Era / Multiverse",r=a.owner_id&&currentUser&&a.owner_id!==currentUser.id&&a.owner_name?" \xB7 GM "+escHtml(a.owner_name):"";return'<div class="anthology-card" data-anthology-id="'+a.id+'" tabindex="0" role="button" title="Open '+escHtml(a.title)+'"><div class="anthology-card-info"><span class="anthology-card-name">'+escHtml(a.title)+'</span><span class="anthology-card-meta">'+escHtml(o)+" \xB7 "+i+r+'</span></div><span class="anthology-card-chevron" aria-hidden="true">\u203A</span></div>'}).join(""),n=_csAnthologiesLoadError?'<p class="placeholder--padded error">Could not load your anthologies, check your connection and reopen.</p>':t||'<p class="placeholder--padded">No anthologies yet. Create one below to group related campaigns.</p>';e.innerHTML='<div class="anthology-list">'+n+'</div><div class="anthology-create-block"><span class="form-section-label">New Anthology</span><p class="form-helper-text">Group campaigns you GM into a shared saga: after creating, add campaigns and link codex entries across them.</p><form id="anthology-create-form" class="anthology-create-form"><div class="form-row"><label for="anthology-new-title">Title</label><input type="text" id="anthology-new-title" required autocomplete="off" placeholder="The Sundered Realms" /></div><div class="form-row"><label for="anthology-new-desc">Description <span class="form-helper-text">(optional)</span></label><input type="text" id="anthology-new-desc" autocomplete="off" placeholder="A multiverse spanning Faer\xFBn and the Astral Sea" /></div><p id="anthology-create-error" class="error hidden"></p><div class="form-actions"><button type="submit" class="btn-new">Create Anthology \u2192</button></div></form></div>',e.querySelectorAll(".anthology-card").forEach(a=>{const s=()=>{_anthModalSelectedId=parseInt(a.dataset.anthologyId),_anthModalView="detail",renderAnthologiesModal()};a.addEventListener("click",s),a.addEventListener("keydown",i=>{(i.key==="Enter"||i.key===" ")&&(i.preventDefault(),s())})}),document.getElementById("anthology-create-form")?.addEventListener("submit",async a=>{a.preventDefault();const s=document.getElementById("anthology-create-error");s.classList.add("hidden");const i=document.getElementById("anthology-new-title").value.trim(),o=document.getElementById("anthology-new-desc").value.trim();if(i)try{const r=await api("POST","/api/anthologies",{title:i,description:o||null});await _loadAnthologies(),_anthModalSelectedId=r.id,_anthModalView="detail",renderAnthologiesModal(),_renderAnthologiesSection()}catch(r){s.textContent=r&&r.message||"Could not create anthology.",s.classList.remove("hidden")}}),_anthRestoreFocus()}
1638async function _renderAnthologyDetailView(e){e.innerHTML='<p class="placeholder--padded">Loading\u2026</p>';let t;try{t=await api("GET","/api/anthologies/"+_anthModalSelectedId)}catch(p){e.innerHTML='<p class="placeholder--padded error">Could not load anthology: '+escHtml(p&&p.message||"Unknown")+"</p>";return}const n=t.id,a=Array.isArray(t.world_labels)?t.world_labels:[],s=Array.isArray(t.campaigns)?t.campaigns:[],i=new Set(s.map(p=>p.id)),o=(_csAllCampaigns||[]).filter(p=>p.user_role==="gm"&&!p.anthology_id&&!i.has(p.id)),r=p=>['<option value="">(world)</option>'].concat(a.map(m=>'<option value="'+escHtml(m)+'"'+(m===p?" selected":"")+">"+escHtml(m)+"</option>")).join(""),c=s.map(p=>'<div class="anthology-camp-row" data-campaign-id="'+p.id+'"><div class="anthology-camp-titlerow"><span class="anthology-camp-reorder"><button type="button" class="anthology-camp-up" title="Move earlier" aria-label="Move earlier">'+_anthChevronSvg("up")+'</button><button type="button" class="anthology-camp-down" title="Move later" aria-label="Move later">'+_anthChevronSvg("down")+'</button></span><span class="anthology-camp-name">'+escHtml(p.name)+'</span><label class="pref-toggle anthology-camp-active-toggle" title="Active campaign: cross-campaign content flows into it"><input type="checkbox" class="anthology-active-toggle-input"'+(p.is_active?" checked":"")+' /><span class="pref-toggle-track" aria-hidden="true"><span class="pref-toggle-thumb"></span></span><span class="pref-toggle-label">Active</span></label></div><div class="anthology-camp-fields"><input type="number" class="anthology-era-order" readonly value="'+(p.anthology_era_order!=null?p.anthology_era_order:"")+'" placeholder="#" title="Timeline order: set with the reorder arrows" /><input type="text" class="anthology-era-label" value="'+escHtml(p.anthology_era_label||"")+'" placeholder="Era label" title="Chronology label (e.g. 1372 DR)" /><select class="anthology-world-label" title="World">'+r(p.anthology_world_label||"")+'</select><button type="button" class="btn-ghost btn-xs anthology-camp-remove" title="Remove from anthology">Remove</button></div></div>').join("")||'<p class="placeholder--padded">No campaigns yet. Add ones you GM below.</p>',l=o.length?'<div class="anthology-add-camp"><select id="anthology-add-camp-select">'+o.map(p=>'<option value="'+p.id+'">'+escHtml(p.name)+"</option>").join("")+'</select><button type="button" id="anthology-add-camp-btn" class="btn-new btn-sm">+ Add campaign</button></div>':'<p class="form-helper-text">Every campaign you GM is already in this anthology or another one.</p>',d=t.type==="concurrent"?"Concurrent":"Era / Multiverse";e.innerHTML='<div class="anthology-detail"><button type="button" id="anthology-back-btn" class="btn-new btn-sm anthology-back-btn">\u2039 All anthologies</button><form id="anthology-edit-form" class="anthology-edit-form"><div class="form-row"><label for="anthology-edit-title">Title</label><input type="text" id="anthology-edit-title" required autocomplete="off" value="'+escHtml(t.title||"")+'" /></div><div class="form-row"><label for="anthology-edit-desc">Description</label><input type="text" id="anthology-edit-desc" autocomplete="off" value="'+escHtml(t.description||"")+'" /></div><div class="form-row"><label for="anthology-edit-worlds">World labels <span class="form-helper-text">(comma-separated: Faer\xFBn, Spelljammer)</span></label><input type="text" id="anthology-edit-worlds" autocomplete="off" value="'+escHtml(a.join(", "))+'" /></div><p id="anthology-edit-error" class="error hidden"></p><p class="form-helper-text anthology-type-note">'+escHtml(d)+'</p></form><div class="anthology-share-block"><span class="form-section-label">Sharing</span><label class="pref-toggle anthology-share-toggle"><input type="checkbox" id="anthology-share-input"'+(t.is_shared?" checked":"")+` /><span class="pref-toggle-track" aria-hidden="true"><span class="pref-toggle-thumb"></span></span><span class="pref-toggle-label">Share the series with players</span></label><p class="form-helper-text">Players in any of these campaigns see every volume on their shelf, including books from before their time. They can't open campaigns they aren't in.</p></div><span class="form-section-label">Campaigns</span><div class="anthology-camp-list">`+c+"</div>"+l+`<div id="anthology-crossmap-mount"></div><div class="anthology-save-row"><button type="submit" form="anthology-edit-form" class="btn-new">Save</button></div><div class="tf-danger-zone"><button type="button" id="anthology-delete-btn" class="btn-ghost btn-xs btn-ghost--danger">Delete anthology</button><span class="form-helper-text">Campaigns keep all their data, they're just released from the anthology.</span></div></div>`,document.getElementById("anthology-back-btn")?.addEventListener("click",()=>{_anthModalView="list",_anthModalSelectedId=null,_loadAnthologies().then(renderAnthologiesModal)}),document.getElementById("anthology-edit-form")?.addEventListener("submit",async p=>{p.preventDefault();
1638const m=document.getElementById("anthology-edit-error");m.classList.add("hidden");const f=document.getElementById("anthology-edit-title").value.trim(),g=document.getElementById("anthology-edit-desc").value.trim(),h=document.getElementById("anthology-edit-worlds").value.split(",").map(v=>v.trim()).filter(Boolean);try{await api("PATCH","/api/anthologies/"+n,{title:f,description:g||null,world_labels:h}),showToast("Anthology saved."),renderAnthologiesModal()}catch(v){m.textContent=v&&v.message||"Could not save.",m.classList.remove("hidden")}}),document.getElementById("anthology-share-input")?.addEventListener("change",async p=>{const m=p.target.checked;try{await api("PATCH","/api/anthologies/"+n,{is_shared:m}),t.is_shared=m,showToast(m?"Series shared with players.":"Series sharing turned off.")}catch(f){p.target.checked=!m,showToast(f&&f.message||"Could not update sharing.")}}),document.getElementById("anthology-delete-btn")?.addEventListener("click",async()=>{if(await confirmDialog({title:"Delete anthology",message:"Delete this anthology? Its campaigns keep all their data, they're just released.",confirmLabel:"Delete"}))try{await api("DELETE","/api/anthologies/"+n),(_csAllCampaigns||[]).forEach(m=>{m.anthology_id===n&&(m.anthology_id=null,m.anthology_title=null)}),_anthModalView="list",_anthModalSelectedId=null,await _loadAnthologies(),renderAnthologiesModal(),_renderAnthologiesSection(),Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns)}catch(m){showToast(m&&m.message||"Could not delete anthology.")}}),document.getElementById("anthology-add-camp-btn")?.addEventListener("click",async()=>{const p=document.getElementById("anthology-add-camp-select");if(!p||!p.value)return;const m=parseInt(p.value);try{await api("POST","/api/anthologies/"+n+"/campaigns",{campaign_id:m});const f=(_csAllCampaigns||[]).find(g=>g.id===m);f&&(f.anthology_id=n,f.anthology_title=t.title),renderAnthologiesModal(),_renderAnthologiesSection(),Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns)}catch(f){showToast(f&&f.message||"Could not add campaign.")}});const u=async()=>{const p=[...e.querySelectorAll(".anthology-camp-row")];let m=!1;for(let f=0;f<p.length;f++){const g=p[f].querySelector(".anthology-era-order"),h=f+1;if(parseInt(g.value)===h)continue;g.value=String(h);const v=parseInt(p[f].dataset.campaignId);try{await api("PATCH","/api/anthologies/"+n+"/campaigns/"+v,{era_order:h,era_label:p[f].querySelector(".anthology-era-label").value.trim()||null,world_label:p[f].querySelector(".anthology-world-label").value||null})}catch(y){m=!0,showToast(y&&y.message||"Could not save order.")}const b=(_csAllCampaigns||[]).find(y=>y.id===v);b&&(b.anthology_era_order=h)}m||showToast("Order updated.")};e.querySelectorAll(".anthology-camp-row").forEach(p=>{const m=parseInt(p.dataset.campaignId);p.querySelector(".anthology-camp-up")?.addEventListener("click",async()=>{const g=p.previousElementSibling;g&&g.classList.contains("anthology-camp-row")&&(p.parentNode.insertBefore(p,g),await u())}),p.querySelector(".anthology-camp-down")?.addEventListener("click",async()=>{const g=p.nextElementSibling;g&&g.classList.contains("anthology-camp-row")&&(p.parentNode.insertBefore(g,p),await u())}),p.querySelector(".anthology-camp-remove")?.addEventListener("click",async()=>{const g=p.querySelector(".anthology-camp-name")?.textContent||"this campaign",h=s.length<=2;if(await confirmDialog({title:"Remove campaign",message:h?'Remove "'+g+`"? It's the second-to-last campaign, so the anthology itself will dissolve, both campaigns keep all their data.`:'Remove "'+g+'" from this anthology? Its era settings and cross-campaign links are dropped; the campaign keeps all its data.',confirmLabel:"Remove"}))try{const b=await api("DELETE","/api/anthologies/"+n+"/campaigns/"+m),y=(_csAllCampaigns||[]).find(_=>_.id===m);if(y&&(y.anthology_id=null,y.anthology_title=null),b&&b.anthology_dissolved){const _=parseInt(n,10);_csAnthologies=(_csAnthologies||[]).filter(w=>w.id!==_),(_csAllCampaigns||[]).forEach(w=>{w.anthology_id===_&&(w.anthology_id=null,w.anthology_title=null)}),showToast("Anthology removed, only one campaign was left.")}renderAnthologiesModal(),_renderAnthologiesSection(),Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns)}catch(b){showToast(b&&b.message||"Could not remove campaign.")}});const f=async()=>{const g=p.querySelector(".anthology-era-order").value.trim();try{await api("PATCH","/api/anthologies/"+n+"/campaigns/"+m,{era_order:g===""?null:parseInt(g),era_label:p.querySelector(".anthology-era-label").value.trim()||null,world_label:p.querySelector(".anthology-world-label").value||null})}catch(h){showToast(h&&h.message||"Could not save campaign settings.")}};p.querySelector(".anthology-era-order")?.addEventListener("change",f),p.querySelector(".anthology-era-label")?.addEventListener("change",f),p.querySelector(".anthology-world-label")?.addEventListener("change",f),p.querySelector(".anthology-active-toggle-input")?.addEventListener("change",async g=>{const h=g.target.checked,v=p.querySelector(".anthology-camp-name")?.textContent||"this campaign",b=h?'Make "'+v+'" the active campaign? Cross-campaign content flows into the active campaign, so this changes what players see, review your cross-map flow settings after switching.':"Clear the active campaign? With no active campaign, no cross-campaign content flows to players.";if(!await confirmDialog({title:h?"Switch active campaign":"Clear active campaign",message:b,confirmLabel:h?"Make active":"Clear",danger:!1})){renderAnthologiesModal();return}try{await api("POST","/api/anthologies/"+n+"/active",{campaign_id:h?m:null}),showToast(h?"Active campaign updated.":"Active campaign cleared."),renderAnthologiesModal()}catch(_){showToast(_&&_.message||"Could not update active campaign."),renderAnthologiesModal()}})}),typeof _renderAnthologyCrossMap=="function"&&_renderAnthologyCrossMap(n),_anthRestoreFocus()}const _ANTH_FLOW_OPTIONS=[["off","Off"],["gm_only","GM only"],["players_note","Players: note"],["players_full","Players: full"]],_anthCrossMapCollapsed={world_fixture:!0,character:!1},_anthEntityPreviewCache={};
1638function _anthXmapRowHtml(e){const t=e.user_status==="dismissed",n=_ANTH_FLOW_OPTIONS.map(i=>'<option value="'+i[0]+'"'+(e.flow_level===i[0]?" selected":"")+">"+i[1]+"</option>").join(""),a=e.flow_level==="players_note",s=(i,o,r,c)=>'<span class="anth-xmap-entity" tabindex="0" data-entity-id="'+r+'" data-campaign-id="'+c+'">'+escHtml(i)+' <span class="anth-xmap-entity-camp">\xB7 '+escHtml(o)+"</span></span>";return'<div class="anth-xmap-row'+(t?" anth-xmap-row--dismissed":"")+'" data-link-id="'+e.id+'" data-flow-level="'+escHtml(e.flow_level)+'" data-user-status="'+escHtml(e.user_status)+'"><div class="anth-xmap-row-entities">'+s(e.entity_a_name,e.entity_a_campaign_name,e.entity_a_id,e.entity_a_campaign_id)+'<span class="anth-xmap-link-glyph" aria-hidden="true">\u2194</span>'+s(e.entity_b_name,e.entity_b_campaign_name,e.entity_b_id,e.entity_b_campaign_id)+(e.link_source==="manual"?'<span class="anth-xmap-src-tag">manual</span>':"")+'</div><div class="anth-xmap-row-controls"><select class="anth-xmap-flow"'+(t?" disabled":"")+">"+n+'</select><button type="button" class="btn-ghost btn-xs anth-xmap-dismiss">'+(t?"Restore":"Dismiss")+'</button></div><div class="anth-xmap-note-wrap'+(a?"":" hidden")+'"><input type="text" class="anth-xmap-note" value="'+escHtml(e.gm_note||"")+`" placeholder="Player-facing note (shown at 'Players: note')" /></div></div>`}async function _renderAnthologyCrossMap(e){const t=document.getElementById("anthology-crossmap-mount");if(!t)return;const n='<span class="form-section-label">Cross-Campaign Links</span>';t.innerHTML=n+'<p class="placeholder--padded">Loading links\u2026</p>';let a;try{const o=await api("GET","/api/anthologies/"+e+"/links");a=o&&o.links||[]}catch(o){t.innerHTML=n+'<p class="placeholder--padded error">Could not load links: '+escHtml(o&&o.message||"Unknown")+"</p>";return}if(!a.length){t.innerHTML=n+`<p class="placeholder--padded">No cross-campaign links yet. The auto-linker connects entities that share a canonical name across this anthology's campaigns, links appear as you add campaigns or create matching entities.</p>`;return}const s={character:[],world_fixture:[]};a.forEach(o=>{(s[o.category]||s.character).push(o)});const i=(o,r,c)=>{const l=s[o];if(!l.length)return"";const d=_anthCrossMapCollapsed[o],u=d?"":'<p class="form-helper-text anth-xmap-tier-hint">'+escHtml(c)+'</p><div class="anth-xmap-bulk"><label>Set all in this group to</label><select class="anth-xmap-bulk-select" data-tier="'+o+'">'+_ANTH_FLOW_OPTIONS.map(p=>'<option value="'+p[0]+'">'+p[1]+"</option>").join("")+'</select><button type="button" class="btn-ghost btn-xs anth-xmap-bulk-apply" data-tier="'+o+'">Apply</button></div>'+l.map(_anthXmapRowHtml).join("");return'<div class="anth-xmap-tier" data-tier="'+o+'"><button type="button" class="anth-xmap-tier-hdr" data-tier="'+o+'"><span class="anth-xmap-tier-caret">'+_anthChevronSvg(d?"right":"down")+'</span><span class="form-section-label anth-xmap-tier-label">'+escHtml(r)+'</span><span class="anth-xmap-tier-count">'+l.length+"</span></button>"+u+"</div>"};t.innerHTML=n+i("character","Characters","PCs, NPCs and factions. Auto-links default to Off: decide per link whether each flows into the active campaign.")+i("world_fixture","World fixtures","Locations, deities, items and lore. Auto-links default to GM only: cross-campaign matches here are usually genuinely the same thing."),t.querySelectorAll(".anth-xmap-tier-hdr").forEach(o=>{o.addEventListener("click",()=>{_anthCrossMapCollapsed[o.dataset.tier]=!_anthCrossMapCollapsed[o.dataset.tier],_renderAnthologyCrossMap(e)})}),t.querySelectorAll(".anth-xmap-row").forEach(o=>{const r=parseInt(o.dataset.linkId),c=o.querySelector(".anth-xmap-flow"),l=o.querySelector(".anth-xmap-note");c?.addEventListener("change",async()=>{try{await api("PATCH","/api/anthologies/"+e+"/links/"+r,{flow_level:c.value}),o.dataset.flowLevel=c.value;const d=o.querySelector(".anth-xmap-note-wrap");d&&d.classList.toggle("hidden",c.value!=="players_note"),showToast("Flow updated.")}catch(d){showToast(d&&d.message||"Could not update flow level.")}}),l?.addEventListener("change",async()=>{try{await api("PATCH","/api/anthologies/"+e+"/links/"+r,{gm_note:l.value.trim()||null}),showToast("Note saved.")}catch(d){showToast(d&&d.message||"Could not save note.")}}),o.querySelector(".anth-xmap-dismiss")?.addEventListener("click",async()=>
1638{const d=o.dataset.userStatus!=="dismissed";try{await api("PATCH","/api/anthologies/"+e+"/links/"+r,{user_status:d?"dismissed":"active"}),_renderAnthologyCrossMap(e)}catch(u){showToast(u&&u.message||"Could not update link.")}}),o.querySelectorAll(".anth-xmap-entity").forEach(d=>{d.addEventListener("mouseenter",()=>_anthShowEntityPreview(d,o)),d.addEventListener("mouseleave",_anthHideEntityPreview),d.addEventListener("focus",()=>_anthShowEntityPreview(d,o)),d.addEventListener("blur",_anthHideEntityPreview)})}),t.querySelectorAll(".anth-xmap-bulk-apply").forEach(o=>{o.addEventListener("click",async()=>{const r=o.dataset.tier,c=t.querySelector('.anth-xmap-bulk-select[data-tier="'+r+'"]');if(!c)return;const l=c.value;o.disabled=!0;const d=(s[r]||[]).filter(u=>u.user_status!=="dismissed");try{for(const u of d)await api("PATCH","/api/anthologies/"+e+"/links/"+u.id,{flow_level:l});showToast("Updated "+d.length+" link"+(d.length===1?"":"s")+"."),_renderAnthologyCrossMap(e)}catch(u){showToast(u&&u.message||"Bulk update failed."),o.disabled=!1}})})}let _anthPreviewEl=null;function _anthGetPreviewEl(){return _anthPreviewEl||(_anthPreviewEl=document.createElement("div"),_anthPreviewEl.className="anth-xmap-preview hidden",document.body.appendChild(_anthPreviewEl)),_anthPreviewEl}function _anthHideEntityPreview(){_anthPreviewEl&&_anthPreviewEl.classList.add("hidden")}async function _anthShowEntityPreview(e,t){const n=parseInt(e.dataset.entityId),a=parseInt(e.dataset.campaignId),s=t.dataset.flowLevel||"off",i=_anthGetPreviewEl(),o=e.getBoundingClientRect();i.style.left=Math.round(Math.max(8,Math.min(o.left,window.innerWidth-340)))+"px",i.style.top=Math.round(o.bottom+6)+"px",i.classList.remove("hidden");const r=d=>{if(i.classList.contains("hidden"))return;if(!d){i.innerHTML='<p class="anth-xmap-preview-empty">Preview unavailable.</p>';return}let u;if(s==="players_note")u=`<p class="anth-xmap-preview-note">At this level players see only the note you write, not this entity's description.</p>`;else{const m=(d.description||"").trim();u=m?'<p class="anth-xmap-preview-desc">'+escHtml(m.slice(0,400))+(m.length>400?"\u2026":"")+"</p>":'<p class="anth-xmap-preview-empty">No description.</p>'}const p=s==="off"?"Not flowing into the active campaign.":s==="gm_only"?"Flows to the GM only: players don't see this.":s==="players_note"?"Players see your note.":"Players see this full entity.";i.innerHTML='<div class="anth-xmap-preview-hdr">'+escHtml(d.name||"")+' <span class="anth-xmap-preview-type">'+escHtml((d.type||"").toUpperCase())+"</span></div>"+u+'<p class="anth-xmap-preview-foot">'+p+"</p>"},c=a+":"+n,l=_anthEntityPreviewCache[c];if(l&&l!=="loading"&&l!=="error"){r(l);return}if(l==="error"){r(null);return}_anthEntityPreviewCache[c]="loading",i.innerHTML='<p class="anth-xmap-preview-empty">Loading\u2026</p>';try{const d=await api("GET","/api/campaigns/"+a+"/entities/"+n);_anthEntityPreviewCache[c]=d,r(d)}catch{_anthEntityPreviewCache[c]="error",r(null)}}let _entityXlinkCache={};async function _renderEntityCrosslinks(e){const t=document.querySelector('.entity-crosslinks-mount[data-entity-id="'+e+'"]');if(!t||!currentCampaign)return;let n;try{n=await api("GET","/api/campaigns/"+currentCampaign.id+"/entities/"+e+"/crosslinks")}catch{return}const a=n&&n.crosslinks||[];if(!a.length){t.innerHTML="";return}a.forEach(i=>{_entityXlinkCache[i.link_id]=i});const s=a.map(i=>{const o=[i.campaign_name,i.era_label].filter(Boolean).join(" \xB7 ");return'<div class="entity-xlink-row" data-link-id="'+i.link_id+'" tabindex="0" role="button" title="View '+escHtml(i.entity_name)+" in "+escHtml(i.campaign_name)+'"><div class="entity-xlink-row-main"><span class="entity-xlink-name">'+escHtml(i.entity_name)+"</span>"+(o?'<span class="entity-xlink-meta">'+escHtml(o)+"</span>":"")+"</div>"+(i.gm_note?'<p class="entity-xlink-note">'+escHtml(i.gm_note)+"</p>":"")+'<span class="entity-xlink-chevron" aria-hidden="true">\u203A</span></div>'}).join("");t.innerHTML='<span class="form-section-label">Also Appears In</span><div class="entity-crosslinks-list">'+s+"</div>",t.querySelectorAll(".entity-xlink-row").forEach(i=>
1638{const o=()=>_openEntityXlinkModal(_entityXlinkCache[parseInt(i.dataset.linkId)]);i.addEventListener("click",o),i.addEventListener("keydown",r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),o())})})}function _openEntityXlinkModal(e){if(!e)return;const t=document.getElementById("entity-xlink-modal-body");if(!t)return;const n=[e.campaign_name,e.era_label,e.world_label].filter(Boolean).map(escHtml).join(" \xB7 ");let a;typeof e.description=="string"&&e.description.trim()?a='<p class="entity-xlink-desc">'+escHtml(e.description.trim())+"</p>":e.gm_note?a='<p class="entity-xlink-gmnote">'+escHtml(e.gm_note)+"</p>":a='<p class="placeholder">No further details shared for this appearance.</p>',t.innerHTML='<div class="journey-summary-card tf-entering"><div class="journey-summary-hdr"><span class="journey-summary-title">'+escHtml(e.entity_name||"")+'</span><span class="entity-xlink-modal-type">'+escHtml((e.entity_type||"").toUpperCase())+"</span></div>"+(n?'<p class="entity-xlink-modal-meta">'+n+"</p>":"")+'<div class="journey-summary-body">'+a+"</div></div>",document.getElementById("entity-xlink-modal").classList.remove("hidden")}function _closeEntityXlinkModal(){document.getElementById("entity-xlink-modal").classList.add("hidden")}document.getElementById("entity-xlink-modal-close")?.addEventListener("click",_closeEntityXlinkModal);let _entityXlinkModalMousedownOnBackdrop=!1;document.getElementById("entity-xlink-modal")?.addEventListener("mousedown",e=>{_entityXlinkModalMousedownOnBackdrop=e.target===document.getElementById("entity-xlink-modal")}),document.getElementById("entity-xlink-modal")?.addEventListener("click",e=>{e.target===document.getElementById("entity-xlink-modal")&&_entityXlinkModalMousedownOnBackdrop&&_closeEntityXlinkModal()}),document.getElementById("join-campaign-btn").addEventListener("click",()=>{document.getElementById("join-camp-code").value="",document.getElementById("join-camp-error").classList.add("hidden"),document.getElementById("join-campaign-modal").classList.remove("hidden"),document.getElementById("join-camp-code").focus()}),(()=>{const e=document.getElementById("cs-view-binders-btn"),t=document.getElementById("cs-view-cards-btn");if(!e||!t)return;const n=()=>{e.classList.toggle("active",_csViewMode==="binders"),t.classList.toggle("active",_csViewMode==="cards"),e.setAttribute("aria-pressed",String(_csViewMode==="binders")),t.setAttribute("aria-pressed",String(_csViewMode==="cards"))},a=s=>{if(_csViewMode!==s){_csViewMode=s;try{localStorage.setItem("tf_cs_view",s)}catch{}n(),Array.isArray(_csAllCampaigns)&&_renderCampaignCards(_csAllCampaigns)}};e.addEventListener("click",()=>a("binders")),t.addEventListener("click",()=>a("cards")),n()})(),document.getElementById("join-camp-close").addEventListener("click",()=>document.getElementById("join-campaign-modal").classList.add("hidden"));let _joinCampModalMousedownOnBackdrop=!1;document.getElementById("join-campaign-modal").addEventListener("mousedown",e=>{_joinCampModalMousedownOnBackdrop=e.target===document.getElementById("join-campaign-modal")}),document.getElementById("join-campaign-modal").addEventListener("click",e=>{e.target===document.getElementById("join-campaign-modal")&&_joinCampModalMousedownOnBackdrop&&document.getElementById("join-campaign-modal").classList.add("hidden")});async function doJoinCampaign(){const e=document.getElementById("join-camp-code").value.trim(),t=document.getElementById("join-camp-error");t.classList.add("hidden");try{const n=await api("POST","/api/campaigns/join",{invite_code:e});document.getElementById("join-campaign-modal").classList.add("hidden"),n.already_member?showToast(`Already a member of "${n.campaign.name}".`):showToast(`Joined "${n.campaign.name}"!`),await loadCampaignSelector()}catch(n){t.textContent=n.message.includes("Invalid")?"Invalid invite code.":n.message,t.classList.remove("hidden")}}document.getElementById("join-camp-submit").addEventListener("click",doJoinCampaign),document.getElementById("join-camp-code").addEventListener("keydown",e=>{e.key==="Enter"&&doJoinCampaign()});let settingsCampaignId=null;async function openCampSettings(e,t){settingsCampaignId=e,document.getElementById("camp-invite-msg")?.classList.add("hidden"),document.getElementById("camp-name-edit-msg")?.classList.add("hidden");const n=document.getElementById("camp-name-edit-input");n&&(n.value=t||"");const a=_partyFaction(),s=document.getElementById("camp-party-name-input");s&&(s.value=a&&a.name||currentCampaign&&currentCampaign.party_name||""),_partyAliases=(a&&Array.isArray(a.aliases)?a.aliases:[]).filter(f=>(f||"").toLowerCase()!=="the party"),_renderPartyAliasChips(),document.getElementById("camp-party-alias-msg")?.classList.add("hidden");const i=document.getElementById("camp-game-type-select");
1638i&&(i.value=currentCampaign&&currentCampaign.game_type_default||"",typeof _renderGameTypeHelper=="function"&&_renderGameTypeHelper()),document.getElementById("camp-game-type-msg")?.classList.add("hidden");const o=document.getElementById("camp-system-select");o&&(o.value=_tfGameSystem()),document.getElementById("camp-system-msg")?.classList.add("hidden");const r=document.getElementById("camp-bardic-name-input");r&&(r.value=currentCampaign&&currentCampaign.bardic_companion_name||"");const c=document.getElementById("camp-gm-name-input");c&&(c.value=currentCampaign&&currentCampaign.gm_credit_name||""),document.getElementById("camp-gm-name-msg")?.classList.add("hidden"),_renderGmLogoPreview(currentCampaign&&currentCampaign.gm_brand_logo_url||"");const l=document.getElementById("camp-gm-logo-status");l&&(l.textContent="");const d=document.getElementById("camp-header-style-input");d&&(d.value=currentCampaign&&currentCampaign.session_header_image_style||_TF_DEFAULT_HEADER_STYLE),document.getElementById("camp-party-name-msg")?.classList.add("hidden"),document.getElementById("camp-bardic-name-msg")?.classList.add("hidden"),document.getElementById("camp-header-style-msg")?.classList.add("hidden");try{const f=await api("GET",`/api/campaigns/${e}/invite-code`);document.getElementById("camp-invite-input").value=f.invite_code||"",_campSavedInviteCode=f.invite_code||"";const g=document.getElementById("camp-linkjoin-chk");g&&(g.checked=f.allow_link_join!==!1),_campUpdateShareLink()}catch{_campSavedInviteCode=""}{const f=document.getElementById("camp-flare-counter-chk");f&&(f.checked=currentCampaign?.flare_counter_visible===!0)}const u=(_csAllCampaigns||[]).find(f=>f.id===e);_renderCampCoverPreview(u&&u.cover_image_url||currentCampaign&&currentCampaign.id===e&&currentCampaign.cover_image_url||"");const p=document.getElementById("camp-cover-status");p&&(p.textContent=""),_nextSessionSkippedOnce=!1,_renderNextSessionInput(),typeof _applyRecurrenceUI=="function"&&_applyRecurrenceUI();{const f=document.getElementById("manage-discord-section");f&&delete f.dataset.idsTouched}_loadDiscordConfig(e),_loadPublicStatus(e),_renderCompletedStatus();const m=!!(currentCampaign&&(currentCampaign.game_type_default||currentCampaign.bardic_companion_name||currentCampaign.gm_credit_name||currentCampaign.gm_brand_logo_url||currentCampaign.session_header_image_style||currentCampaign.flare_counter_visible===!0));typeof _tfCustomizationSet=="function"&&_tfCustomizationSet(!0),typeof _tfManageRailDot=="function"&&_tfManageRailDot("customization",m)}function _renderCompletedStatus(){const e=document.getElementById("camp-completed-toggle-btn");if(!e||!currentCampaign)return;const t=!!currentCampaign.completed_at;e.textContent=t?"Reopen Campaign":"Mark Complete",e.onclick=async()=>{const n=!currentCampaign.completed_at;if(!(n&&!await confirmDialog({title:"Mark Completed",message:`Mark \u201C${currentCampaign.name}\u201D as Completed? It moves to \u201COn the Shelf\u201D on your campaign list. This does NOT archive or delete it; you can reopen it anytime.`,confirmLabel:"Mark Completed"}))){e.disabled=!0;try{const a=await api("POST",`/api/campaigns/${currentCampaign.id}/completed`,{completed:n});currentCampaign.completed_at=a.completed?new Date().toISOString():null,showToast(a.completed?"Marked complete, it\u2019s on the shelf.":"Reopened."),_renderCompletedStatus();try{const s=await api("GET","/api/campaigns");_csAllCampaigns=s,typeof _renderCampaignCards=="function"&&_renderCampaignCards(s)}catch{}}catch(a){showToast("Failed: "+a.message)}finally{e.disabled=!1}}}}async function _loadPublicStatus(e){const t=document.getElementById("manage-publish-body");if(t)try{const n=await api("GET",`/api/campaigns/${e}/public-status`);if(settingsCampaignId!==e)return;_renderPublicStatus(n)}catch{settingsCampaignId===e&&(t.innerHTML=`<p class="form-helper-text">Couldn't load publish status.</p>`)}}function _fmtPublishDate(e){try{return new Date(e).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}catch{return""}}function _joinNames(e){const t=e.map(escHtml);return t.length<=1?t[0]||"":t.length===2?t[0]+" and "+t[1]:t.slice(0,-1).join(", ")+" and "+t[t.length-1]}function _renderPublicStatus(e){const t=document.getElementById("manage-publish-body");if(t){if(_tfManageStatus("publish",e.is_public?"Public":"Private",e.is_public?"moss":"ghost"),e.is_public){const n=e.published_at?_fmtPublishDate(e.published_at):"",a=e.url||"",s=e.redacted_members||[],i=s.length?`<p class="form-helper-text">${_joinNames(s.map(o=>o.name))} ${s.length===1?"isn't":"aren't"} shown on the public page.</p>`:"";t.innerHTML=`<div class="manage-publish-status manage-publish-status--live"><span class="manage-publish-dot"></span>Public${n?" since "+escHtml(n):""}</div><div class="camp-invite-row"><input type="text" id="manage-publish-url" class="login-input camp-invite-inp" readonly value="${esc
1638Html(a)}" /><button id="manage-publish-copy" type="button" class="btn-new" title="Copy public link">Copy</button><a class="btn-ghost" href="${escHtml(a)}" target="_blank" rel="noopener" title="Open the public page">Open \u2197</a></div>`+i+'<div class="camp-invite-row"><button id="manage-publish-toggle" type="button" class="btn-ghost" data-action="unpublish">Make private</button></div>'}else{const n=e.can_publish!==!1;t.innerHTML=`<div class="manage-publish-status">Private</div><div class="camp-invite-row"><button id="manage-publish-toggle" type="button" class="btn-new" data-action="publish"${n?"":" disabled"}>Make this campaign public</button></div>`+(n?'<p class="form-helper-text">You can make it private again anytime, the link stays the same.</p>':'<p class="form-helper-text">Publish after your first recap, your page grows with every session you chronicle.</p>')}_bindPublishActions()}}function _bindPublishActions(){document.getElementById("manage-publish-toggle")?.addEventListener("click",_onPublishToggle),document.getElementById("manage-publish-copy")?.addEventListener("click",()=>{const e=document.getElementById("manage-publish-url");e&&navigator.clipboard&&navigator.clipboard.writeText(e.value).then(()=>{typeof showToast=="function"&&showToast("Public link copied")},()=>{})})}async function _onPublishToggle(e){const t=e.currentTarget,n=t.dataset.action,a=settingsCampaignId;if(a){t.disabled=!0;try{if(await api("POST",`/api/campaigns/${a}/${n}`),settingsCampaignId!==a)return;await _loadPublicStatus(a),typeof showToast=="function"&&showToast(n==="publish"?"Campaign is now public":"Campaign is private now")}catch(s){t.disabled=!1,typeof showToast=="function"&&showToast(s&&s.message||"Something went wrong")}}}function _renderNextSessionInput(){const e=document.getElementById("camp-next-session-input"),t=document.getElementById("camp-next-session-msg");if(!e)return;t&&t.classList.add("hidden");const n=currentCampaign?.next_session_at;if(!n)e.value="";else{const a=new Date(n);if(isNaN(a.getTime()))e.value="";else{const s=i=>String(i).padStart(2,"0");e.value=`${a.getFullYear()}-${s(a.getMonth()+1)}-${s(a.getDate())}T${s(a.getHours())}:${s(a.getMinutes())}`}}typeof _refreshInputClearVisibility=="function"&&_refreshInputClearVisibility(e),_tfManageStatusNextSession()}let _nextSessionSkippedOnce=!1;function _recurrenceSelectValueFromCampaign(){const e=currentCampaign&&currentCampaign.recurrence_rule||"none",t=currentCampaign&&currentCampaign.recurrence_interval||1;return e==="weekly"?`weekly:${Math.min(4,Math.max(1,t))}`:e==="monthly"?"monthly":"none"}function _parseRecurrenceSelect(e){if(e==="monthly")return{recurrence_rule:"monthly",recurrence_interval:1};const t=/^weekly:(\d)$/.exec(e||"");return t?{recurrence_rule:"weekly",recurrence_interval:parseInt(t[1],10)}:{recurrence_rule:"none",recurrence_interval:1}}function _recurrenceLabel(e,t){return e==="monthly"?"monthly":e==="weekly"?t===1?"weekly":`every ${t} weeks`:""}function _renderNextSessionScheduleLine(){const e=document.getElementById("camp-next-session-schedule-line"),t=document.getElementById("camp-next-session-schedule-text"),n=document.getElementById("camp-next-session-skip-link");if(!e||!t)return;const a=document.getElementById("camp-recurrence-rule"),s=document.getElementById("camp-next-session-input"),i=_parseRecurrenceSelect(a?a.value:"none");let o=null;const r=(s&&s.value||"").trim();if(r){const p=new Date(r);isNaN(p.getTime())||(o=p)}else if(currentCampaign&&currentCampaign.next_session_at){const p=new Date(currentCampaign.next_session_at);isNaN(p.getTime())||(o=p)}if(!o){e.classList.add("hidden"),t.textContent="",n&&n.classList.add("hidden");return}const c=o.toLocaleString(void 0,{weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}),l=_recurrenceLabel(i.recurrence_rule,i.recurrence_interval);t.textContent=l?`Next: ${c}, then ${l}`:`Next: ${c}`,e.classList.remove("hidden");const d=currentCampaign&&currentCampaign.recurrence_rule||"none",u=(d==="weekly"||d==="monthly")&&!!(currentCampaign&&currentCampaign.next_session_at);n&&(u?(n.textContent=_nextSessionSkippedOnce?"Undo skip":"Skip next \u2192",n.classList.remove("hidden")):n.classList.add("hidden"))}function _applyRecurrenceUI(){const e=document.getElementById("camp-recurrence-rule");e&&(e.value=_recurrenceSelectValueFromCampaign()),_syncEventSyncVisibilityForRecurrence(),_renderNextSessionScheduleLine()}function _syncEventSyncVisibilityForRecurrence(){const e=document.getElementById("camp-recurrence-rule"),t=document.getElementById("camp-event-sync-row"),n=document.getElementById("camp-event-sync-recur-note"),a=e?_parseRecurrenceSelect(e.value).recurrence_rule!=="none":!1;t&&t.classList.toggle("hidden",a),n&&n.classList.toggle("hidden",!a)}document.getElementById("camp-recurrence-rule")?.addEventListener("change",async()=>{const e=document.getElementById("camp-recurrence-rule"),t=e?_parseRecurrenceSelect(e.value).recurrence_rule!=="none":!1;if(_syncEventSyncVisibilityForRecurrence(),_renderNextSessionScheduleLine(),t){const n=document.getElementById("camp-discord-event-sync-chk");if(n&&n.checked&&settingsCampaignId&&_campDiscordConfig&&_campDiscordConfig.guild_id){n.checked=!1,_applyEventSyncReadonlyState();try{_campDiscordConfig=await api("PUT",`/api/campaigns/${settingsCampaignId}/discord/config`,{guild_id:_campDiscordConfig.guild_id,recap_channel_id:_campDiscordConfig.recap_channel_id||null,announce_channel_id:_campDiscordConfig.announce_channel_id||null,reveal_channel_id:_campDiscordConfig.reveal_channel_id||null,event_sync_enabled:!1}),document.getElementById("camp-discord-sync-now-btn")?.classList.add("hidden")}catch{}}}}),document.getElementById("camp-next-session-skip-link")?.addEventListener("click",async e=>{if(e.preventDefault(),!settingsCampaignId)return;const t=_nextSessionSkippedOnce?"undo":"skip";try{const n=await api("POST",`/api/campaigns/${settingsCampaignId}/next-session/skip`,{direction:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.next_session_at=n.next_session_at),_nextSessionSkippedOnce=t==="skip",_renderNextSessionInput(),_renderNextSessionScheduleLine(),typeof _applyNextSessionPingState=="function"&&_applyNextSessionPingState(),showToast(t==="skip"?"Skipped, reminder moved to the next session.":"Skip undone.",3e3)}catch(n){showToast(n.message||"Couldn\u2019t update the schedule.",5e3)}}),document.getElementById("camp-next-session-input")?.addEventListener("input",()=>{_renderNextSessionScheduleLine()});function _applyEventSyncReadonlyState(){const e=document.getElementById("camp-discord-event-sync-chk"),t=document.getElementById("camp-next-session-input"),n=document.getElementById("camp-next-session-auto-badge");!e||!t||(e.checked&&!e.disabled?(t.disabled=!0,t.style.opacity="0.65",n?.classList.remove("hidden")):(t.disabled=!1,t.style.opacity="",n?.classList.add("hidden")),typeof _refreshInputClearVisibility=="function"&&_refreshInputClearVisibility(t))}document.getElementById("camp-discord-event-sync-chk")?.addEventListener("change",async()=>{_applyEventSyncReadonlyState();const e=document.getElementById("camp-discord-event-sync-chk");if(!(!e||e.disabled)&&!(!settingsCampaignId||!_campDiscordConfig||!_campDiscordConfig.guild_id))try{const t=await api("PUT",`/api/campaigns/${settingsCampaignId}/discord/config`,{guild_id:_campDiscordConfig.guild_id,recap_channel_id:_campDiscordConfig.recap_channel_id||null,announce_channel_id:_campDiscordConfig.announce_channel_id||null,reveal_channel_id:_campDiscordConfig.reveal_channel_id||null,event_sync_enabled:e.checked});_campDiscordConfig=t;const n=document.getElementById("camp-discord-sync-now-btn");n&&(t.guild_id&&e.checked?n.classList.remove("hidden"):n.classList.add("hidden"))}catch(t){e.checked=!e.checked,_applyEventSyncReadonlyState(),showToast(t.message||"Sync toggle failed to save.",5e3)}}),document.getElementById("camp-discord-autopost-chk")?.addEventListener("change",async()=>{const e=document.getElementById("camp-discord-autopost-chk");if(!(!e||e.disabled)&&!(!settingsCampaignId||!_campDiscordConfig||!_campDiscordConfig.guild_id))try{_campDiscordConfig=await api("PUT",`/api/campaigns/${settingsCampaignId}/discord/config`,{guild_id:_campDiscordConfig.guild_id,recap_channel_id:_campDiscordConfig.recap_channel_id||null,announce_channel_id:_campDiscordConfig.announce_channel_id||null,reveal_channel_id:_campDiscordConfig.reveal_channel_id||null,recap_autopost:e.checked})}catch(t){e.checked=!e.checked,showToast(t.message||"Auto-post toggle failed to save.",5e3)}}),document.addEventListener("click",async e=>{const t=e.target.closest(".autopost-now-btn"),n=e.target.closest(".autopost-hold-btn");if(!t&&!n)return;const a=t||n,s=parseInt(a.dataset.sid);if(!s||!currentCampaign)return;const i=a.closest(".chr2-autopost-chip");a.disabled=!0;try{await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${s}/recap-autopost`,{action:t?"post_now":"hold"}),i&&(i.textContent=t?"Posted to Discord \u2713":"Auto-post held"),showToast(t?"Chapter posted to Discord.":"Auto-post held \u2014 the Publish button still works any time.")}catch(o){a.disabled=!1,showToast("Failed: "+(o.message||"try again"))}});function _applyEventSyncCapability(){const e=document.getElementById("camp-discord-event-sync-chk"),t=document.getElementById("camp-discord-event-sync-label");if(!e||!t)return;!!(_campDiscordConfig&&_campDiscordConfig.guild_id)?(e.disabled=!1,t.title="Pulls the soonest upcoming Discord scheduled event for this server every hour.",t.classList.remove("cap-gate-disabled")):(e.disabled=!0,e.checked=!1,t.title="Set Server ID + Channel ID in Discord below first.",t.classList.add("cap-gate-disabled")),_applyEventSyncReadonlyState()}async function _applyLiveInviteCapability(){}document.getElementById("camp-next-session-save")?.addEventListener("click",async()=>
1638{if(!settingsCampaignId)return;const e=document.getElementById("camp-next-session-input"),t=document.getElementById("camp-next-session-msg"),n=(e?.value||"").trim();t.classList.add("hidden");let a=null,s=null;if(n){const c=new Date(n);if(isNaN(c.getTime())){showToast("Invalid date/time.",4e3);return}a=c.toISOString(),s=Intl.DateTimeFormat().resolvedOptions().timeZone||null}const i=document.getElementById("camp-recurrence-rule");let{recurrence_rule:o,recurrence_interval:r}=_parseRecurrenceSelect(i?i.value:"none");a||(o="none",r=1);try{const c=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{next_session_at:a,next_session_tz:s,recurrence_rule:o,recurrence_interval:r});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.next_session_at=c.next_session_at,currentCampaign.next_session_tz=c.next_session_tz,currentCampaign.recurrence_rule=c.recurrence_rule,currentCampaign.recurrence_interval=c.recurrence_interval),_nextSessionSkippedOnce=!1,t.textContent="Saved!",t.classList.remove("hidden"),setTimeout(()=>t.classList.add("hidden"),3e3),_renderNextSessionInput(),_applyRecurrenceUI(),typeof _refreshInputClearVisibility=="function"&&_refreshInputClearVisibility(e),typeof _applyNextSessionPingState=="function"&&_applyNextSessionPingState()}catch(c){showToast(c.message||"Save failed.",5e3)}});function _fmtNextSessionPingDate(e){if(!e)return"";const t=new Date(e);if(isNaN(t.getTime()))return"";try{return t.toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}catch{return""}}async function _applyNextSessionPingState(){const e=document.getElementById("camp-next-session-ping-btn"),t=document.getElementById("camp-next-session-ping-msg");if(!e)return;const n=settingsCampaignId;if(e.disabled=!0,!n){e.textContent="Send reminder now",t&&(t.textContent="");return}let a=null;try{a=await api("GET",`/api/campaigns/${n}/discord/next-session-ping`)}catch{e.textContent="Send reminder now",t&&(t.textContent="");return}let s=!1,i="Send reminder now",o="Post the \u201CLast time on\u2026\u201D reminder to your Discord recap channel now.",r="";if(!a.tier_ok)s=!0,o="Discord integration isn't available on this plan.",r="Discord reminders aren't available on this plan.";else if(!a.server_set)s=!0,o="Set a Discord Server ID above first.",r="Connect a Discord server above to enable reminders.";else if(!a.channel_ready)s=!0,o="Set a recap channel ID above first.",r="Set a recap channel above to enable reminders.";else if(!a.next_session_at)s=!0,o="Set a next-session date & time above first.",r="Set a next-session time above to enable reminders.";else if(a.already_sent){s=!0,i="\u2713 Reminder sent";const c=_fmtNextSessionPingDate(a.next_session_at);r=`Already sent for Session ${a.session_number}${c?", "+c:""}.`,o=r}e.disabled=s,e.textContent=i,e.title=o,t&&(t.textContent=r)}document.getElementById("camp-next-session-ping-btn")?.addEventListener("click",async()=>{const e=document.getElementById("camp-next-session-ping-btn");if(!e||e.disabled||!settingsCampaignId)return;e.disabled=!0;const t=e.textContent;e.textContent="Sending\u2026";try{await api("POST",`/api/campaigns/${settingsCampaignId}/discord/next-session-ping`),showToast("Reminder posted to Discord.",3e3)}catch(n){showToast(n.message||"Couldn\u2019t send the reminder.",5e3),e.textContent=t}finally{typeof _applyNextSessionPingState=="function"&&await _applyNextSessionPingState()}});let _campDiscordConfig=null;async function _loadDiscordConfig(e){const t=document.getElementById("camp-discord-guild-id"),n=document.getElementById("camp-discord-recap-channel-id"),a=document.getElementById("camp-discord-announce-channel-id"),s=document.getElementById("camp-discord-reveal-channel-id"),i=document.getElementById("camp-discord-voice-channel-id"),o=document.getElementById("camp-discord-event-sync-chk"),r=document.getElementById("camp-discord-sync-now-btn");t&&(t.value=""),n&&(n.value=""),a&&(a.value=""),s&&(s.value=""),i&&(i.value=""),o&&(o.checked=!1),r&&r.classList.add("hidden");try{const l=await api("GET",`/api/campaigns/${e}/discord/config`);if(_campDiscordConfig=l,l){t&&(t.value=l.guild_id||""),n&&(n.value=l.recap_channel_id||""),a&&(a.value=l.announce_channel_id||""),s&&(s.value=l.reveal_channel_id||""),i&&(i.value=l.voice_channel_id||"");const d=l&&l.opt_in_flags||{},u=d.event_sync_enabled===!0||d.event_sync_enabled==="true";o&&(o.checked=u),r&&l.guild_id&&r.classList.remove("hidden");const p=document.getElementById("camp-discord-autopost-chk");p&&(p.checked=d.recap_autopost===!0||d.recap_autopost==="true")}typeof _applyDiscordAnnounceCapability=="function"&&_applyDiscordAnnounceCapability(),typeof _applyEventSyncCapability=="function"&&_applyEventSyncCapability(),typeof _applyLiveInviteCapability=="function"&&_applyLiveInviteCapability()}catch{_campDiscordConfig=null,typeof _applyDiscordAnnounceCapability=="function"&&_applyDiscordAnnounceCapability(),typeof _applyEventSyncCapability=="function"&&_applyEventSyncCapability(),typeof _applyLiveInviteCapability=="function"&&_applyLiveInviteCapability()}typeof _tfDiscordAdvancedSet=="function"&&_tfDiscordAdvancedSet(!!(a&&a.value||s&&s.value));const c=document.getElementById("camp-discord-invite-btn");if(c){let l=null;try{l=localStorage.getItem("tf_jwt")}catch{}c.href=e&&l?`/discord/install-bot?campaign_id=${e}&token=${encodeURIComponent(l)}`:"/discord/install-bot"}_refreshAllInputClearVisibility(),typeof _applyNextSessionPingState=="function"&&_applyNextSessionPingState(),typeof _syncEventSyncVisibilityForRecurrence=="function"&&_syncEventSyncVisibilityForRecurrence(),_resolveDiscordNames(),_loadDiscordSpeakers(e)}function _setDiscordResolved(e,t,n){const a=document.getElementById(`camp-discord-${e}-resolved`);a&&(a.innerHTML=t||"",a.classList.toggle("is-error",!!n))}const _DISCORD_CHANNEL_ROLE_INPUTS={recap:"camp-discord-recap-channel-id",announce:"camp-discord-announce-channel-id",reveal:"camp-discord-reveal-channel-id",voice:"camp-discord-voice-channel-id"};async function _resolveDiscordNames(){const e=settingsCampaignId||typeof currentCampaign<"u"&&currentCampaign&&currentCampaign.id;if(!e)return;const t=(document.getElementById("camp-discord-guild-id")?.value||"").trim();_tfManageStatusDiscord();const n={};
1638Object.entries(_DISCORD_CHANNEL_ROLE_INPUTS).forEach(([o,r])=>{n[o]=(document.getElementById(r)?.value||"").trim()}),t||(_setDiscordResolved("server","",!1),_tfDiscordIdsConnected(!1)),Object.entries(n).forEach(([o,r])=>{r||_setDiscordResolved(o,"",!1)});const a=Object.values(n).filter(Boolean);if(!t&&!a.length)return;let s;try{const o=new URLSearchParams;t&&o.set("guild_id",t),a.length&&o.set("channels",a.join(",")),s=await api("GET",`/api/campaigns/${e}/discord/resolve?${o.toString()}`)}catch{return}if(t){const o=s.guild||{};o.name?(_setDiscordResolved("server",escHtml(o.name),!1),_tfManageStatusDiscord(o.name),_tfDiscordIdsConnected(!0)):o.error==="not_visible"?_setDiscordResolved("server","Bot can\u2019t see this server, is it invited?",!0):_setDiscordResolved("server","",!1)}const i=s.channels||{};Object.entries(n).forEach(([o,r])=>{if(!r)return;const c=i[r];if(!c){_setDiscordResolved(o,"",!1);return}if(c.name)if(!c.guild_id||!t||c.guild_id===t)_setDiscordResolved(o,`<span class="dr-hash">#</span>${escHtml(c.name)}`,!1);else{const d=c.guild_name?escHtml(c.guild_name):"another server";_setDiscordResolved(o,`<span class="dr-path">${d} \u203A </span><span class="dr-hash">#</span>${escHtml(c.name)}`,!1)}else c.error==="not_visible"?_setDiscordResolved(o,"Bot can\u2019t see this channel",!0):_setDiscordResolved(o,"",!1)})}["camp-discord-guild-id","camp-discord-recap-channel-id","camp-discord-announce-channel-id","camp-discord-reveal-channel-id","camp-discord-voice-channel-id"].forEach(e=>{const t=document.getElementById(e);t&&(t.addEventListener("change",()=>_resolveDiscordNames()),t.addEventListener("paste",()=>setTimeout(_resolveDiscordNames,60)))});function _refreshInputClearVisibility(e){if(!e)return;const t=e.closest(".input-with-clear");if(!t)return;const n=t.querySelector(".input-clear-x");if(!n)return;!!(e.value&&String(e.value).trim().length)&&!e.disabled&&!e.readOnly?n.classList.remove("hidden"):n.classList.add("hidden")}function _refreshAllInputClearVisibility(){document.querySelectorAll(".input-with-clear > input").forEach(e=>{_refreshInputClearVisibility(e)})}document.addEventListener("input",e=>{e.target&&e.target.matches&&e.target.matches(".input-with-clear > input")&&_refreshInputClearVisibility(e.target)}),document.addEventListener("click",e=>{const t=e.target.closest&&e.target.closest(".input-clear-x");if(!t)return;e.preventDefault();const n=t.getAttribute("data-clear-target");if(!n)return;const a=document.getElementById(n);a&&(a.value="",a.dispatchEvent(new Event("input",{bubbles:!0})),a.focus())});async function _saveDiscordConfigFromRow(e){if(!settingsCampaignId)return;if(e==="voice"){const i=document.getElementById("camp-discord-guild-id")?.value.trim(),o=document.getElementById("camp-discord-voice-channel-id")?.value.trim()||"";if(!i){showToast("Server ID is required, set it before saving the voice channel.",4e3);return}try{const r=await api("PUT",`/api/campaigns/${settingsCampaignId}/discord/voice-config`,{guild_id:i,voice_channel_id:o});_campDiscordConfig?_campDiscordConfig.voice_channel_id=r.voice_channel_id:_campDiscordConfig={guild_id:i,voice_channel_id:r.voice_channel_id};const c=document.getElementById("camp-discord-voice-msg");c&&(c.classList.remove("hidden"),setTimeout(()=>c.classList.add("hidden"),3e3)),typeof _renderRecModeRow=="function"&&_renderRecModeRow(),_refreshAllInputClearVisibility(),_resolveDiscordNames()}catch(r){showToast(r.message||"Save failed.",5e3)}return}const t=document.getElementById("camp-discord-guild-id").value.trim(),n=document.getElementById("camp-discord-recap-channel-id").value.trim(),a=document.getElementById("camp-discord-announce-channel-id")?.value.trim()||"",s=document.getElementById("camp-discord-reveal-channel-id")?.value.trim()||"";if(!t){if(e!=="server"){showToast("Server ID is required, set it before saving a channel.",4e3);return}if(!await confirmDialog({title:"Disconnect Discord?",message:"Clearing the Server ID disconnects Discord from this campaign: recap publishing, announces, and reveals all stop. Nothing already posted to Discord is removed.",confirmLabel:"Disconnect"}))return;try{await api("DELETE",`/api/campaigns/${settingsCampaignId}/discord/config`),_campDiscordConfig=null,document.getElementById("camp-discord-recap-channel-id").value="";const o=document.getElementById("camp-discord-announce-channel-id");o&&(o.value="");const r=document.getElementById("camp-discord-reveal-channel-id");r&&(r.value="");const c=document.getElementById("camp-discord-event-sync-chk");c&&(c.checked=!1),document.getElementById("camp-discord-sync-now-btn")?.classList.add("hidden"),typeof _applyDiscordAnnounceCapability=="function"&&_applyDiscordAnnounceCapability(),typeof _applyEventSyncCapability=="function"&&_applyEventSyncCapability(),_refreshAllInputClearVisibility(),_resolveDiscordNames(),showToast("Discord disconnected.",3e3)}catch(o){showToast(o.message||"Disconnect failed.",5e3)}return}try{const i=await api("PUT",`/api/campaigns/${settingsCampaignId}/discord/config`,{guild_id:t,recap_channel_id:n||null,announce_channel_id:a||null,reveal_channel_id:s||null});_campDiscordConfig=i;const o=document.getElementById("camp-discord-event-sync-chk"),r=document.getElementById("camp-discord-sync-now-btn");r&&i.guild_id&&o&&!o.disabled&&o.checked&&r.classList.remove("hidden"),typeof _applyEventSyncCapability=="function"&&_applyEventSyncCapability();const c=document.getElementById(`camp-discord-${e}-msg`);c&&(c.classList.remove("hidden"),setTimeout(()=>c.classList.add("hidden"),3e3)),typeof _applyDiscordAnnounceCapability=="function"&&_applyDiscordAnnounceCapability(),_refreshAllInputClearVisibility(),_resolveDiscordNames()}catch(i){showToast(i.message||"Save failed.",5e3)}}
1638document.querySelectorAll("[data-discord-save-row]").forEach(e=>{e.addEventListener("click",()=>_saveDiscordConfigFromRow(e.getAttribute("data-discord-save-row")))});const _DPITCH_MAX_SHOWS=3;function _tfDiscordPostMockHtml(e){return`<div class="tf-dmock" aria-hidden="true">
1639      <div class="tf-dmock-avatar"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 2.5c-3.5.5-7 2-9.5 5.5L2.5 13.5l5.5-1.5c3.5-2.5 5-6 5.5-9.5Z"/><path d="M6.5 9.5 2.5 13.5"/></svg></div>
1640      <div class="tf-dmock-body">
1641        <div class="tf-dmock-hdr"><span class="tf-dmock-name">Threadfall</span><span class="tf-dmock-bot">BOT</span><span class="tf-dmock-time">Today</span></div>
1642        <div class="tf-dmock-text">${escHtml((e||"").trim()||"Your session recap lands here after every game: the bardic blurb in the channel, a link back to the full chronicle.")}</div>
1643        <div class="tf-dmock-embed"><span class="tf-dmock-embed-title">Read the full recap on Threadfall</span></div>
1644      </div>
1645    </div>`}function _tfDiscordPitchKey(e){const t=typeof currentCampaign<"u"&&currentCampaign?currentCampaign.id:null;return t?`tf_dpitch_${e}_${t}`:null}function _tfDiscordPitchHtml(e){if(typeof _isGm=="function"&&!_isGm()||_campDiscordConfig&&_campDiscordConfig.recap_channel_id)return"";const t=_tfDiscordPitchKey("dismissed"),n=_tfDiscordPitchKey("shows");if(!t)return"";try{if(localStorage.getItem(t))return"";const a=parseInt(localStorage.getItem(n)||"0",10)||0;if(a>=_DPITCH_MAX_SHOWS)return"";localStorage.setItem(n,String(a+1))}catch{return""}return`<div class="tf-dpitch" id="recap-discord-pitch">
1646      <div class="form-section-label">Send this to your table&rsquo;s Discord</div>
1647      ${_tfDiscordPostMockHtml(e&&e.session_blurb)}
1648      <div class="tf-dpitch-actions">
1649        <button type="button" class="btn-new" data-dpitch="setup">Set up Discord publishing</button>
1650        <button type="button" class="btn-ghost" data-dpitch="later">Maybe later</button>
1651      </div>
1652    </div>`}function _tfWireDiscordPitch(e){const t=e.querySelector("#recap-discord-pitch");t&&(t.querySelector('[data-dpitch="setup"]')?.addEventListener("click",()=>{document.getElementById("nav-manage-btn")?.click(),setTimeout(()=>document.getElementById("manage-discord-section")?.scrollIntoView({behavior:"smooth",block:"start"}),250)}),t.querySelector('[data-dpitch="later"]')?.addEventListener("click",()=>{const n=_tfDiscordPitchKey("dismissed");try{n&&localStorage.setItem(n,"1")}catch{}t.remove()}))}function _tfDiscordIdsSet(e){const t=document.getElementById("manage-discord-section"),n=document.getElementById("camp-discord-ids-toggle");!t||!n||(t.classList.toggle("discord-ids-open",e),n.setAttribute("aria-expanded",e?"true":"false"),n.textContent=e?"Set up by hand \u25BE":"Set up by hand \u25B8")}function _tfDiscordIdsConnected(e){const t=document.getElementById("manage-discord-section");if(!t)return;const n=(document.getElementById("camp-discord-voice-channel-id")?.value||"").trim(),a=(document.getElementById("camp-discord-recap-channel-id")?.value||"").trim(),s=e&&n&&a;t.classList.toggle("discord-connected",!!e),t.dataset.idsTouched||_tfDiscordIdsSet(!s)}document.getElementById("camp-discord-ids-toggle")?.addEventListener("click",()=>{const e=document.getElementById("manage-discord-section");e&&(e.dataset.idsTouched="1"),_tfDiscordIdsSet(!(e&&e.classList.contains("discord-ids-open")))});function _tfDiscordAdvancedSet(e){const t=document.getElementById("manage-discord-section"),n=document.getElementById("camp-discord-advanced-toggle");!t||!n||(t.classList.toggle("discord-advanced-open",e),n.setAttribute("aria-expanded",e?"true":"false"),n.textContent=e?"Advanced channels \u25BE":"Advanced channels \u25B8")}document.getElementById("camp-discord-advanced-toggle")?.addEventListener("click",()=>{const e=document.getElementById("manage-discord-section");_tfDiscordAdvancedSet(!(e&&e.classList.contains("discord-advanced-open")))});function _tfCustomizationSet(e){const t=document.getElementById("manage-customization-section"),n=document.getElementById("camp-customization-toggle"),a=document.getElementById("camp-customization-chevron");!t||!n||(t.classList.toggle("customization-open",e),n.setAttribute("aria-expanded",e?"true":"false"),a&&(a.textContent=e?"\u25BE":"\u25B8"))}document.getElementById("camp-customization-toggle")?.addEventListener("click",()=>{const e=document.getElementById("manage-customization-section");_tfCustomizationSet(!(e&&e.classList.contains("customization-open")))});function _tfFmtEventStart(e){if(!e)return"";const t=new Date(e);
1652return isNaN(t)?"":t.toLocaleString(void 0,{weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function _tfShowEventLinkPicker(e){const t=document.getElementById("camp-event-link-panel");if(!t)return;const n=e.candidates||[];if(!n.length){t.classList.add("hidden"),showToast("No scheduled events found on your Discord server yet.",5e3);return}const a=e.status==="link_broken",s=a?"We lost track of the Discord event for this campaign.":"Your Discord server has several games.",i=a?"It looks like it was removed or replaced. Your next session date is unchanged until you pick again.":`We found ${n.length} scheduled events on this server. Pick the one for this campaign.`,o=(n[0]&&n[0].why&&n[0].why.length?n[0].why[0]:"this is the soonest upcoming event on your server").replace(/\.+$/,"");t.innerHTML=`
1653    <span class="form-section-label">${escHtml(s)}</span>
1654    <p class="form-helper-text">${escHtml(i)}</p>
1655    <div class="evlink-list" role="radiogroup" aria-label="Discord events">
1656      ${n.map((c,l)=>`
1657        <label class="evlink-row${l===0?" evlink-row--suggested":""}">
1658          <input type="radio" name="evlink" value="${escHtml(c.event_id)}"${l===0?" checked":""}>
1659          <span class="evlink-body">
1660            <span class="evlink-name">${escHtml(c.name)}</span>
1661            <span class="evlink-meta">${escHtml(c.recurrence||"")}${c.start?" \xB7 Next: "+escHtml(_tfFmtEventStart(c.start)):""}</span>
1662            ${l===0?`<span class="evlink-suggested">Suggested. ${escHtml(o.charAt(0).toUpperCase()+o.slice(1))}.</span>`:""}
1663          </span>
1664        </label>`).join("")}
1665    </div>
1666    <div class="evlink-actions">
1667      <button type="button" class="btn-new btn-sm" id="evlink-confirm">Use this event</button>
1668      <button type="button" class="btn-ghost btn-xs" id="evlink-decline">None of these, I'll set the date myself</button>
1669    </div>`,t.classList.remove("hidden");const r=async c=>{try{const l=await api("POST",`/api/campaigns/${settingsCampaignId}/discord/event-link`,{event_id:c});t.classList.add("hidden"),l.status==="linked"?(currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.next_session_at=l.next_session_at),typeof _renderNextSessionInput=="function"&&_renderNextSessionInput(),typeof _applyNextSessionPingState=="function"&&_applyNextSessionPingState(),showToast(`Linked to "${l.event_name}". Next session updated.`,5e3)):showToast("Set the date here whenever you like.",4e3)}catch(l){showToast(l.message||"Could not link that event.",5e3)}};t.querySelector("#evlink-confirm")?.addEventListener("click",()=>{const c=t.querySelector('input[name="evlink"]:checked');c&&r(c.value)}),t.querySelector("#evlink-decline")?.addEventListener("click",()=>r(null))}document.getElementById("camp-discord-sync-now-btn")?.addEventListener("click",async()=>{if(!settingsCampaignId)return;const e=document.getElementById("camp-discord-sync-now-btn"),t=document.getElementById("camp-discord-msg"),n=e.textContent;e.disabled=!0,e.textContent="Syncing\u2026",t?.classList.add("hidden"),t?.classList.remove("error");try{const a=await api("POST",`/api/campaigns/${settingsCampaignId}/discord/sync-events`);let s;if(a.status==="synced")currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.next_session_at=a.next_session_at),typeof _renderNextSessionInput=="function"&&_renderNextSessionInput(),typeof _applyNextSessionPingState=="function"&&_applyNextSessionPingState(),s=a.event_name?`\u2713 Synced to "${a.event_name}". Next session updated.`:"\u2713 Synced. Next session updated.";else if(a.status==="unchanged")s="Already in sync with the next Discord event.";else if(a.status==="no_events")s="No scheduled events found on your Discord server yet.";else if(a.status==="needs_confirmation"||a.status==="link_broken"){_tfShowEventLinkPicker(a);return}else a.status==="no_config"?s="Save your Discord config first.":s=`Sync failed (${a.status||"unknown"}).`;showToast(s,5e3)}catch(a){showToast(a.message||"Sync failed.",5e3)}finally{e.disabled=!1,e.textContent=n}}),document.addEventListener("click",async e=>{const t=e.target.closest(".btn-publish-discord");if(!t)return;e.preventDefault();const n=t.dataset.id;if(!n||!currentCampaign)return;if(!_campDiscordConfig?.recap_channel_id){showToast("Set a recap channel in Manage \u2192 Discord first.",5e3);return}const a=await _tfFetchInviteState(currentCampaign.id),s=!!(a&&a.has_active&&a.allow_link_join!==!1),i=_campDiscordConfig&&_campDiscordConfig.opt_in_flags||{},o=i.recap_full_thread===!0||i.recap_full_thread==="true",r=[{key:"notify",label:"Notify players (@here)",defaultChecked:!0},{key:"tally",label:"Include session tally (Kills \xB7 Collateral \xB7 Conquests \xB7 Near Wipes)",defaultChecked:!1}
1669,{key:"fullrecap",label:"Post full recap in thread (off = blurb + link only)",defaultChecked:o}],c=(allSessions||[]).find(y=>String(y.id)===String(n)),l=c?_chr2TitleCandidates(c):[],d=c?(c.session_blurb||"").trim():"",u=d||(c&&typeof _chr2BriefText=="function"?_chr2BriefText(c):""),p=await confirmDialog({title:"Publish recap to Discord?",pillChoice:c&&(c.title||"").trim()?{label:"Chapter title players will see",current:(c.title||"").trim(),options:l.map(y=>y.title),editable:!0}:null,textInput:c?{label:"TL;DR players will read",defaultValue:u,rows:4,placeholder:"What players will read\u2026"}:null,fieldOrder:["pill","text","checkboxes"],message:"This is the post: the title and TL;DR below, plus a button to the full recap. Change either one here before it goes out, or just publish. Tick \u201CPost full recap in thread\u201D to also fan every chapter into a Discord thread."+(s?`
1670
1671Links in this post let members of your server join the campaign as players. Turn that off next to your invite link in Manage.`:""),confirmLabel:"Publish",checkboxes:r});if(!p||!p.confirmed)return;const m=(p.text||"").replace(/^#+\s*/gm,"").replace(/\*\*/g,"").trim();if(c&&m&&m!==d)try{const y=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${n}`,{session_blurb:m});allSessions=(allSessions||[]).map(_=>String(_.id)===String(n)?y:_),typeof renderChronicleRecap=="function"&&_chr2Panel()?._chrSid==n&&renderChronicleRecap(n)}catch(y){showToast("TL;DR change failed, nothing published: "+(y&&y.message||y),5e3);return}const f=!!(p.checked&&p.checked.tally),g=!!(p.checked&&p.checked.notify),h=!!(p.checked&&p.checked.fullrecap);_campDiscordConfig&&(_campDiscordConfig.opt_in_flags=Object.assign({},_campDiscordConfig.opt_in_flags,{recap_full_thread:h}));const v=(p.pill||"").trim();if(v&&c&&v!==(c.title||"").trim())try{const y=await api("PUT",`/api/campaigns/${currentCampaign.id}/sessions/${n}`,{title:v});allSessions=(allSessions||[]).map(_=>String(_.id)===String(n)?y:_),typeof renderChronicleRecap=="function"&&_chr2Panel()?._chrSid==n&&renderChronicleRecap(n)}catch(y){showToast("Title change failed, nothing published: "+(y&&y.message||y),5e3);return}const b=t.textContent;await _doPublishToDiscord(t,n,b,{include_tally:f,notify_players:g,post_full_recap:h})});const _tfInviteStateCache=new Map;async function _tfFetchInviteState(e){if(_tfInviteStateCache.has(e))return _tfInviteStateCache.get(e);try{const t=await api("GET",`/api/campaigns/${e}/invite-code`),n=t&&t.invite_code||"",a=t&&t.expires_at,s=!!n&&(!a||new Date(a)>new Date),i=!t||t.allow_link_join!==!1,o={has_active:s,code:n,expires_at:a||null,allow_link_join:i};return _tfInviteStateCache.set(e,o),o}catch{const n={has_active:!1,code:"",expires_at:null,allow_link_join:!1};return _tfInviteStateCache.set(e,n),n}}function _tfInvalidateInviteCache(e){e===void 0?_tfInviteStateCache.clear():_tfInviteStateCache.delete(e)}async function _doPublishToDiscord(e,t,n,a){e.disabled=!0,e.textContent="Publishing\u2026";try{const s=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${t}/publish-to-discord`,a);e.textContent=s.beats_posted>0?`\u2713 Published (${s.beats_posted} beats)`:"\u2713 Published (TL;DR)",showToast(s.was_republish?"Recap re-published, old post replaced.":"Recap published to Discord.",4e3),setTimeout(()=>{e.disabled=!1,e.textContent=n},4e3)}catch(s){if(e.disabled=!1,e.textContent=n,s.status===409&&s.detail&&s.detail.already_published){const i=s.detail.published_at?new Date(s.detail.published_at).toLocaleString(void 0,{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"}):"earlier",o=await confirmDialog({title:"Already published",message:`This session was published to Discord on ${i}. Re-publishing will delete the old post and thread, then post a fresh recap.`,confirmLabel:"Re-publish (replace)",checkbox:{label:"Notify players again (@here)",defaultChecked:!1}});o&&o.confirmed&&await _doPublishToDiscord(e,t,n,{...a,notify_players:!!o.checked,force:!0});return}showToast(s.message||"Publish failed.",6e3)}}async function loadManageTab(){currentCampaign&&(_tfManageRailInit(),openCampSettings(currentCampaign.id,currentCampaign.name),loadRoster(),loadFeed(),_loadCorrectionsSection().then(_tfManageRailSync,_tfManageRailSync),_renderDeadringerSection(),_tfManageRailSync())}
1671function _tfManageStatus(e,t,n){_tfManageRailDot(e,n==="moss");const a=document.getElementById(`manage-status-${e}`);if(!a)return;const s=n==="moss"?"tf-badge--moss":n==="amber"?"tf-badge--amber":"tf-badge--ghost";a.className=`tf-badge ${s}`+(t?"":" hidden"),a.textContent=t||""}function _tfManageFeedSet(e){const t=document.getElementById("manage-feed-col"),n=document.getElementById("manage-feed-toggle"),a=t&&t.closest(".manage-layout");if(!t||!n)return;t.classList.toggle("is-collapsed",!e),a&&a.classList.toggle("feed-collapsed",!e),n.setAttribute("aria-expanded",e?"true":"false");const s=n.querySelector(".manage-feed-handle-chev");s&&(s.textContent=e?"\u25B8":"\u25C2");try{localStorage.setItem("tf_manage_feed_open",e?"1":"0")}catch{}typeof _tfManageRailMarkActive=="function"&&_tfManageRailMarkActive()}document.getElementById("manage-feed-toggle")?.addEventListener("click",()=>{const e=document.getElementById("manage-feed-col");_tfManageFeedSet(!!(e&&e.classList.contains("is-collapsed")))});function _tfManageRailDot(e,t){const n=document.querySelector(`#manage-rail [data-dot="${e}"] .manage-rail-dot`);n&&n.classList.toggle("is-on",!!t)}function _tfRailDot(e,t,n,a){const s=e&&e.querySelector(`.manage-rail-chip[data-rail="${t}"]`);if(!s)return;let i=s.querySelector(".manage-rail-dot");if(!n){i&&i.remove(),s.removeAttribute("data-tone"),s.removeAttribute("title");return}i||(i=document.createElement("span"),i.className="manage-rail-dot",i.setAttribute("aria-hidden","true"),s.insertBefore(i,s.firstChild)),i.classList.toggle("is-alarm",n==="alarm"),i.classList.toggle("is-watch",n==="watch"),i.classList.toggle("is-on",n==="clear"),s.dataset.tone=n,a?s.title=a:s.removeAttribute("title")}function _tfManageRailSync(){document.querySelectorAll("#manage-rail .manage-rail-chip").forEach(e=>{const t=document.getElementById(e.dataset.rail);e.classList.toggle("hidden",!t||t.classList.contains("hidden"))}),_tfManageRailMarkActive()}let _tfManageRailState={pinned:null,active:null};function _tfRailReveal(e){if(!e||e.scrollWidth<=e.clientWidth+1)return;const t=e.querySelector(".manage-rail-chip.is-active");if(!t)return;const n=t.offsetLeft-e.offsetLeft,a=n+t.offsetWidth;(n<e.scrollLeft+8||a>e.scrollLeft+e.clientWidth-8)&&e.scrollTo({left:Math.max(0,n-16),behavior:"auto"})}function _tfRailMarkActive(e){const{rail:t,panel:n,scroller:a,state:s}=e;if(!t||!n||!a||n.classList.contains("hidden"))return;if(s.pinned){t.querySelectorAll(".manage-rail-chip").forEach(m=>{const f=m.dataset.rail===s.pinned;m.classList.toggle("is-active",f),f?m.setAttribute("aria-current","true"):m.removeAttribute("aria-current")}),_tfRailReveal(t);return}const i=n.getBoundingClientRect(),o=a.getBoundingClientRect(),r=e.bandTop(),c=Math.min(i.bottom,o.bottom),l=Math.max(1,c-r),d=e.sections();if(!d.length)return;const u=m=>{const f=m.getBoundingClientRect();return Math.max(0,Math.min(f.bottom,c)-Math.max(f.top,r))};let p=null;if(a.scrollTop>=a.scrollHeight-a.clientHeight-4)p=d[d.length-1].id;else{let m=null,f=-1,g=0;d.forEach(v=>{const b=u(v);v.id===s.active&&(g=b),b>f&&(f=b,m=v.id)}),p=s.active&&d.some(v=>v.id===s.active)&&f-g<.15*l?s.active:m}e.onActive&&p!==s.reported&&(s.reported=p,e.onActive(p)),s.active=p,t.querySelectorAll(".manage-rail-chip").forEach(m=>{const f=m.dataset.rail===p;m.classList.toggle("is-active",f),f?m.setAttribute("aria-current","true"):m.removeAttribute("aria-current")}),_tfRailReveal(t)}function _tfRailScrollTo(e,t,n){const{scroller:a,state:s}=e,i=t.getBoundingClientRect().top-a.getBoundingClientRect().top+a.scrollTop-e.scrollOffset()-12;s.pinned=t.id,s.active=t.id,_tfRailMarkActive(e);const o=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches;a.scrollTo({top:Math.max(0,i),behavior:n||(o?"auto":"smooth")})}function _tfRailInit(e){const{rail:t,panel:n,scroller:a,state:s}=e;if(!t||!n||!a||t.dataset.ready)return;t.dataset.ready="1",t.addEventListener("click",o=>{const r=o.target.closest(".manage-rail-chip");if(!r)return;const c=document.getElementById(r.dataset.rail);c&&(e.onSelect&&e.onSelect(c),_tfRailScrollTo(e,c))}),["wheel","touchstart","keydown"].forEach(o=>a.addEventListener(o,()=>{s.pinned=null},{passive:!0}));let i=0;a.addEventListener("scroll",()=>{i||(i=requestAnimationFrame(()=>{i=0,_tfRailMarkActive(e)}))},{passive:!0})}function _tfManageRailCfg(){const e=document.getElementById("tab-manage"),t=document.getElementById("manage-rail");return{rail:t,panel:e,scroller:e,state:_tfManageRailState,sections:()=>e?[...e.querySelectorAll(".manage-section[id]:not(.hidden)")]:[],bandTop:()=>t.getBoundingClientRect().bottom,scrollOffset:()=>t.getBoundingClientRect().height,onSelect:n=>{n.id==="manage-customization-section"&&typeof _tfCustomizationSet=="function"&&_tfCustomizationSet(!0)}}}function _tfManageRailMarkActive(){_tfRailMarkActive(_tfManageRailCfg())}function _tfManageRailInit(){_tfRailInit(_tfManageRailCfg())}(()=>{let e=!1;try{e=localStorage.getItem("tf_manage_feed_open")==="1"}catch{}_tfManageFeedSet(e)})();function _tfManageStatusNextSession(){const e=currentCampaign&&currentCampaign.next_session_at,t=typeof _fmtNextSessionPingDate=="function"?_fmtNextSessionPingDate(e):"";if(!t){_tfManageStatus("nextsession","Not scheduled","ghost");return}const n=new Date(e).getTime()<Date.now();_tfManageStatus("nextsession",t,n?"amber":"moss")}function _tfManageStatusDiscord(e){if(!(document.getElementById("camp-discord-guild-id")?.value||"").trim()){_tfManageStatus("discord","Not connected","ghost");return}_tfManageStatus("discord",e?`Connected \xB7 ${e}`:"Connected","moss")}const _TF_DEADRINGER_URL="https://deadringer.threadfall.app";function _tfDeadringerHref(e){return e?`${_TF_DEADRINGER_URL}/tf/${e}`:_TF_DEADRINGER_URL}function _renderDeadringerSection(){const e=document.getElementById("manage-dr-connect-btn"),t=document.getElementById("manage-dr-open-btn");if(!e||!t)return;const n=_tfDeadringerHref(currentCampaign&&currentCampaign.id);e.href=n,t.href=n;const a=!!(currentUser&&currentUser.dr_linked);e.classList.toggle("hidden",a),t.classList.toggle("hidden",!a),_tfManageStatus("deadringer",a?"Connected":"Not connected",a?"moss":"ghost")}async function _loadCorrectionsSection(){const e=document.getElementById("manage-corrections-section"),t=document.getElementById("manage-corrections-body");if(!e||!t)return;const n=currentCampaign?.id;
1671if(!n||!(_isGm()||currentUser&&currentUser.is_admin)){e.classList.add("hidden");return}let a;try{a=await api("GET",`/api/campaigns/${n}/corrections`)}catch{e.classList.add("hidden");return}if(_campaignStale(n))return;const s=Object.entries(a.corrections||{}),i=a.dismissed||[];e.classList.remove("hidden"),typeof _tfManageRailSync=="function"&&_tfManageRailSync(),t.innerHTML=(!s.length&&!i.length?'<p class="placeholder placeholder--padded">No fixes yet. Accept a suggestion from a session&rsquo;s Name check to add one.</p>':"")+s.map(([o,r])=>`<div class="correction-map-row" data-corr-garble="${escHtml(o)}"><span class="correction-map-pair">&ldquo;${escHtml(o)}&rdquo; &rarr; ${escHtml(r)}</span> <button type="button" class="btn-ghost btn-xs btn-ghost--danger" data-corr-remove="1">Remove</button></div>`).join("")+(i.length?'<div class="correction-map-dismissed"><span class="form-helper-text">Marked &ldquo;not a name&rdquo;:</span> '+i.map(o=>`<span class="correction-map-row correction-map-row--dismissed" data-corr-garble="${escHtml(o)}">&ldquo;${escHtml(o)}&rdquo; <button type="button" class="btn-ghost btn-xs" data-corr-undismiss="1">Suggest again</button></span>`).join(" ")+"</div>":""),!t.dataset.corrWired&&(t.dataset.corrWired="1",t.addEventListener("click",async o=>{const r=o.target.closest("[data-corr-remove]"),c=o.target.closest("[data-corr-undismiss]");if(!r&&!c)return;const d=o.target.closest("[data-corr-garble]")?.dataset.corrGarble;if(d){(r||c).disabled=!0;try{await api("POST",`/api/campaigns/${n}/corrections`,{garble:d,action:r?"remove":"undismiss"}),showToast(r?`\u201C${d}\u201D removed. Future recaps leave it as heard; already-corrected text is unchanged. You may be asked again if it comes up.`:`\u201C${d}\u201D can be suggested again.`,5e3),_loadCorrectionsSection()}catch(u){showToast("Update failed: "+u.message,5e3),(r||c).disabled=!1}}}))}async function _tfEnsureCsBuilders(){if(_csPreviewBuilders)return!0;if(typeof _renderCampaignCards!="function")return!1;if(!Array.isArray(_csAllCampaigns)||!_csAllCampaigns.length)try{const e=localStorage.getItem("tf_campaigns_"+(currentUser&&currentUser.id||"anon")),t=e?JSON.parse(e):null;Array.isArray(t)&&t.length&&(_csAllCampaigns=t)}catch{}if(!Array.isArray(_csAllCampaigns)||!_csAllCampaigns.length)try{_csAllCampaigns=await api("GET","/api/campaigns")}catch{return!1}if(!Array.isArray(_csAllCampaigns)||!_csAllCampaigns.length)return!1;try{_renderCampaignCards(_csAllCampaigns)}catch{}return!!_csPreviewBuilders}function _renderCampCoverPreview(e){const t=document.getElementById("camp-cover-preview"),n=document.getElementById("camp-cover-remove-btn"),a=document.getElementById("camp-cover-frame-btn");if(a&&(a.disabled=!e),n&&n.classList.toggle("hidden",!e),!t)return;const s=settingsCampaignId||currentCampaign&&currentCampaign.id;if(!_csPreviewBuilders){t.dataset.priming||(t.dataset.priming="1",_tfEnsureCsBuilders().then(g=>{g&&_renderCampCoverPreview(e||currentCampaign&&currentCampaign.id===s&&currentCampaign.cover_image_url||"")}));return}const i=(_csAllCampaigns||[]).find(g=>g.id===s)||{},o=currentCampaign&&currentCampaign.id===s?currentCampaign:{},r={...i,id:s,name:o.name||i.name||"",party_name:o.party_name!=null?o.party_name:i.party_name,gm_credit_name:o.gm_credit_name!=null?o.gm_credit_name:i.gm_credit_name,cover_image_url:e||"",cover_framing:o.cover_framing||i.cover_framing||null,completed_at:null,pending_recap:null},c={...r,completed_at:i.completed_at||new Date().toISOString()};let l="",d="",u="";try{l=_csPreviewBuilders.binder(r,!1),d=_csPreviewBuilders.card(r,!1);const g=e&&_csPreviewBuilders.faceout?_csPreviewBuilders.faceout(c):"";u=g?`<div class="cs-book">${_csPreviewBuilders.spine(c,!1)}${g}</div>`:_csPreviewBuilders.spine(c,!1)}catch(g){console.warn("cover preview",g)}t.innerHTML=`<div class="mcp-tri-item mcp-tri-item--binder"><div class="cs-view-binders mcp-tri-host">${l}</div><span class="mcp-tri-cap">Binder</span></div><div class="mcp-tri-item mcp-tri-item--card"><div class="mcp-tri-host">${d}</div><span class="mcp-tri-cap">Card</span></div><div class="mcp-tri-item mcp-tri-item--shelf"><div class="mcp-tri-host">${u}</div><span class="mcp-tri-cap">On the Shelf</span></div>`;const p=e?`url("${e.replace(/"/g,"%22")}")`:"";t.querySelectorAll(".cc-card[data-id]").forEach(g=>{e?g.style.setProperty("--cover-img",p):g.style.removeProperty("--cover-img"),_applyCoverFramingVars(g,r)});const m=t.querySelector(".cs-spine");m&&e&&m.style.setProperty("--spine-cover",`url("${_tfSpineCoverUrl(e).replace(/"/g,"%22")}")`);const f=t.querySelector(".cs-faceout");if(f&&e){const g=e.includes("/image/upload/")?e.replace("/image/upload/","/image/upload/c_fill,g_auto,ar_2:3,w_400,q_auto:good/"):e;f.style.setProperty("--faceout-cover",`url("${g.replace(/"/g,"%22")}")`);const h=()=>_tfFitFaceoutTitle(f);h(),requestAnimationFrame(h),document.fonts&&document.fonts.ready&&document.fonts.ready.then(h)}}function _renderGmLogoPreview(e){const t=document.getElementById("camp-gm-logo-preview"),n=document.getElementById("camp-gm-logo-remove-btn");t&&(e?(t.innerHTML=`<img src="${escHtml(e)}" alt="GM brand logo" />`,t.classList.remove("hidden"),n?.classList.remove("hidden")):(t.innerHTML="",t.classList.add("hidden"),n?.classList.add("hidden")))}function _campUpdateShareLink(){const e=document.getElementById("camp-invite-share-link");if(!e)return;const t=(_campSavedInviteCode||"").trim();if(!t){e.classList.add("hidden");return}const n=`https://thread-fall.com/join/${encodeURIComponent(t)}`;e.href=n,e.textContent=n.replace(/^https:\/\//,""),e.classList.remove("hidden");const a=!!document.getElementById("camp-linkjoin-chk")?.checked;e.classList.toggle("camp-invite-share-link--off",!a),e.title=a?"Preview the join page this link opens":"Link joining is off; this link won't grant access"}async function _campCommitInviteCode(e){const t=document.getElementById("camp-invite-msg");
1671if(t?.classList.add("hidden"),e&&!_TF_INVITE_CODE_RE.test(e))return showToast("Invite codes use letters, numbers, hyphens and underscores only, 4 to 64 characters. No spaces or punctuation.",7e3),!1;let n="";try{n=(await _tfFetchInviteState(settingsCampaignId)||{}).code||""}catch{}if(n&&e!==n&&!await confirmDialog({title:"Reset invite link?",message:"Changing the code invalidates the invite links in every recap, reminder, and announce post already in your Discord. Members you already have keep their access. Share the new link going forward.",confirmLabel:"Reset link",cancelLabel:"Keep current",danger:!1}))return!1;try{return await api("PUT",`/api/campaigns/${settingsCampaignId}/invite-code`,{invite_code:e}),_tfInvalidateInviteCache(settingsCampaignId),_campSavedInviteCode=e,_campUpdateShareLink(),t&&(t.classList.remove("hidden"),setTimeout(()=>t.classList.add("hidden"),3e3)),!0}catch(a){return showToast(a.message,5e3),!1}}document.getElementById("camp-invite-copy-link")?.addEventListener("click",async()=>{const e=document.getElementById("camp-invite-input").value.trim();if(!e){showToast("Set an invite code first, then copy the link.",4e3);return}if(e!==_campSavedInviteCode&&!await _campCommitInviteCode(e))return;const t=`https://thread-fall.com/join/${encodeURIComponent(_campSavedInviteCode)}`;navigator.clipboard.writeText(t).then(()=>{const n=document.getElementById("camp-invite-copy-link");n.textContent="Copied!",setTimeout(()=>{n.textContent="Copy invite link"},2e3)}).catch(()=>showToast("Could not copy, try manually selecting the code.",4e3))}),document.getElementById("camp-invite-copy").addEventListener("click",async()=>{const e=document.getElementById("camp-invite-input").value.trim();if(!e||e!==_campSavedInviteCode&&!await _campCommitInviteCode(e))return;const t=_campSavedInviteCode;navigator.clipboard.writeText(t).then(()=>{const n=document.getElementById("camp-invite-copy");n.textContent="Copied!",setTimeout(()=>{n.textContent="Copy code"},2e3)}).catch(()=>showToast("Could not copy, try manually selecting the code.",4e3))}),document.getElementById("camp-linkjoin-chk")?.addEventListener("change",async e=>{const t=e.currentTarget;try{await api("PUT",`/api/campaigns/${settingsCampaignId}/link-join`,{enabled:t.checked}),showToast(t.checked?"Link joining on: links in your posts let players join.":"Link joining off: your links no longer grant access.",4e3)}catch(n){t.checked=!t.checked,showToast(n.message,5e3)}_campUpdateShareLink()}),document.getElementById("camp-flare-counter-chk")?.addEventListener("change",async e=>{const t=e.currentTarget;try{await api("PUT",`/api/campaigns/${settingsCampaignId}`,{flare_counter_visible:t.checked}),currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.flare_counter_visible=t.checked),showToast(t.checked?"Flare counts now show on player screens.":"Flare counts hidden \u2014 flares stay silent tallies.",4e3)}catch(n){t.checked=!t.checked,showToast(n.message,5e3)}}),document.getElementById("camp-invite-save").addEventListener("click",async()=>{await _campCommitInviteCode(document.getElementById("camp-invite-input").value.trim())}),document.getElementById("camp-name-edit-save").addEventListener("click",async()=>{const e=document.getElementById("camp-name-edit-input"),t=(e?.value||"").trim(),n=document.getElementById("camp-name-edit-msg");if(n.classList.add("hidden"),!t){showToast("Campaign name cannot be empty.",4e3),e?.focus();return}if(settingsCampaignId)try{const a=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{name:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.name=a.name);const s=document.getElementById("nav-campaign-name");if(s){const i=s.querySelector(".ncn-text");i&&(i.textContent=a.name||""),s.title=a.name||""}if(Array.isArray(_csAllCampaigns)){const i=_csAllCampaigns.find(o=>o.id===settingsCampaignId);i&&(i.name=a.name)}n.classList.remove("hidden"),setTimeout(()=>n.classList.add("hidden"),3e3)}catch(a){showToast(a.message,5e3)}}),document.getElementById("camp-party-name-save")?.addEventListener("click",async()=>{const t=(document.getElementById("camp-party-name-input")?.value||"").trim(),n=document.getElementById("camp-party-name-msg");
1671if(n?.classList.add("hidden"),!!settingsCampaignId)try{const a=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{party_name:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.party_name=a.party_name||""),typeof loadEntities=="function"&&await loadEntities(),typeof _renderSavedFates=="function"&&_renderSavedFates(),n?.classList.remove("hidden"),setTimeout(()=>n?.classList.add("hidden"),3e3)}catch(a){showToast(a.message,5e3)}});let _partyAliases=[];function _renderPartyAliasChips(){const e=document.getElementById("camp-party-alias-chips");e&&(e.innerHTML="",_partyAliases.forEach((t,n)=>{const a=document.createElement("span");a.className="alias-chip",a.innerHTML=`${escHtml(t)}<button type="button" aria-label="Remove alias ${escHtml(t)}" data-idx="${n}">\u2715</button>`,a.querySelector("button").addEventListener("click",()=>{const s=_partyAliases.filter((i,o)=>o!==n);_savePartyAliases(s)}),e.appendChild(a)}),e.classList.toggle("hidden",!_partyAliases.length))}async function _savePartyAliases(e){if(!settingsCampaignId)return;const t=_partyAliases;_partyAliases=e,_renderPartyAliasChips();const n=document.getElementById("camp-party-alias-msg");try{await api("PUT",`/api/campaigns/${settingsCampaignId}`,{party_aliases:e}),typeof loadEntities=="function"&&await loadEntities(),n&&(n.classList.remove("hidden"),setTimeout(()=>n.classList.add("hidden"),2500))}catch(a){_partyAliases=t,_renderPartyAliasChips(),showToast(a.message||"Couldn\u2019t save the alias.",5e3)}}function _addPartyAliasFromInput(){const e=document.getElementById("camp-party-alias-input"),t=(e?.value||"").trim();t&&(e.value="",t.toLowerCase()!=="the party"&&(_partyAliases.some(n=>n.toLowerCase()===t.toLowerCase())||(_savePartyAliases([..._partyAliases,t]),e.focus())))}document.getElementById("camp-party-alias-add-btn")?.addEventListener("click",_addPartyAliasFromInput),document.getElementById("camp-party-alias-input")?.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),_addPartyAliasFromInput())});const _GAME_TYPE_HELPER={"":"Threadfall remembers whatever capture mode you used last on this campaign.",manual:"No audio: paste your notes or a transcript and Threadfall writes the recap. Covers play-by-post campaigns too.",inperson:"Your device mic records the whole room. Wired/USB mic works best, Bluetooth breaks transcription.",virtual:"Captures whatever plays through your speakers: works with Zoom, Teams, or any call app. One shared stream.",discord:"The bot joins your voice channel and records each speaker separately, even through cross-talk. Requires Discord setup."};function _renderGameTypeHelper(){const e=document.getElementById("camp-game-type-select"),t=document.getElementById("camp-game-type-helper");e&&t&&(t.textContent=_GAME_TYPE_HELPER[e.value]??"")}document.getElementById("camp-game-type-select")?.addEventListener("change",async()=>{const e=document.getElementById("camp-game-type-select"),t=document.getElementById("camp-game-type-msg");if(_renderGameTypeHelper(),t?.classList.add("hidden"),!(!settingsCampaignId||!e))try{const n=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{game_type_default:e.value||null});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.game_type_default=n.game_type_default||null);const a=(_csAllCampaigns||[]).find(s=>s.id===settingsCampaignId);a&&(a.game_type_default=n.game_type_default||null),t?.classList.remove("hidden"),setTimeout(()=>t?.classList.add("hidden"),3e3)}catch(n){showToast(n.message,5e3)}});function _tfGameSystem(){return currentCampaign&&currentCampaign.game_system||"dnd5e"}function _tfIsDaggerheart(){return _tfGameSystem()==="daggerheart"}function _tfDocTitleFor(e){return e?"Threadfall: The live Daggerheart companion that turns your game into a book":"Threadfall: The live D&D companion that turns your game into a book"}function _tfApplyDocTitle(){try{document.title=_tfDocTitleFor(_tfIsDaggerheart())}catch{}}function _applySystemGating(){document.body.classList.toggle("sys-daggerheart",_tfIsDaggerheart())}document.getElementById("camp-system-select")?.addEventListener("change",async()=>{const e=document.getElementById("camp-system-select"),t=document.getElementById("camp-system-msg");
1671if(t?.classList.add("hidden"),!(!settingsCampaignId||!e))try{const n=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{game_system:e.value});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.game_system=n.game_system||"dnd5e",typeof _applySystemGating=="function"&&_applySystemGating());const a=(_csAllCampaigns||[]).find(s=>s.id===settingsCampaignId);a&&(a.game_system=n.game_system||"dnd5e"),t?.classList.remove("hidden"),setTimeout(()=>t?.classList.add("hidden"),3e3)}catch(n){showToast(n.message,5e3)}}),document.getElementById("camp-bardic-name-save")?.addEventListener("click",async()=>{const t=(document.getElementById("camp-bardic-name-input")?.value||"").trim(),n=document.getElementById("camp-bardic-name-msg");if(n?.classList.add("hidden"),!!settingsCampaignId)try{const a=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{bardic_companion_name:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.bardic_companion_name=a.bardic_companion_name||""),n?.classList.remove("hidden"),setTimeout(()=>n?.classList.add("hidden"),3e3)}catch(a){showToast(a.message,5e3)}}),document.getElementById("camp-gm-name-save")?.addEventListener("click",async()=>{const t=(document.getElementById("camp-gm-name-input")?.value||"").trim(),n=document.getElementById("camp-gm-name-msg");if(n?.classList.add("hidden"),!!settingsCampaignId)try{const a=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{gm_credit_name:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.gm_credit_name=a.gm_credit_name||""),n?.classList.remove("hidden"),setTimeout(()=>n?.classList.add("hidden"),3e3)}catch(a){showToast(a.message,5e3)}}),document.getElementById("camp-header-style-save")?.addEventListener("click",async()=>{const t=(document.getElementById("camp-header-style-input")?.value||"").trim()||null,n=document.getElementById("camp-header-style-msg");if(n?.classList.add("hidden"),!!settingsCampaignId)try{const a=await api("PUT",`/api/campaigns/${settingsCampaignId}`,{session_header_image_style:t});currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.session_header_image_style=a.session_header_image_style||""),n?.classList.remove("hidden"),setTimeout(()=>n?.classList.add("hidden"),3e3)}catch(a){showToast(a.message,5e3)}});const _CAMP_LIB_FILTERS=[{key:"all",label:"All",test:()=>!0},{key:"header",label:"Headers",test:e=>e.kind==="header"||e.kind==="generated"},{key:"cover",label:"Cover",test:e=>e.kind==="cover"},{key:"map",label:"Maps",test:e=>e.kind==="map"},{key:"unassigned",label:"Unassigned",test:e=>!(e.assignments&&e.assignments.length)}];function _campLibOpen(e){if(!(currentCampaign&&currentCampaign.id))return;let n=document.getElementById("camp-image-library-modal");if(!n){n=document.createElement("div"),n.id="camp-image-library-modal",n.className="modal-overlay chr2-art-modal hidden",n.innerHTML=`<div class="modal-box modal-box--xl chr2-am-box">
1672      <div class="chr2-am-hdr"><span class="chr2-am-title">Image Library</span>
1673        <button type="button" class="chr2-am-close" aria-label="Close">&times;</button></div>
1674      <div class="chr2-am-body"></div></div>`,document.body.appendChild(n);const s=()=>{clearTimeout(n._libBakeTimer),n._pick=null,n.classList.add("hidden")};n.querySelector(".chr2-am-close").addEventListener("click",s),n.addEventListener("click",i=>{i.target===n&&s()}),document.addEventListener("keydown",i=>{_tfModalEsc(i,n)&&s()})}n._pick=e||null,n._libFilter=e&&e.filter||"all",document.body.appendChild(n);const a=n.querySelector(".chr2-am-title");a&&(a.textContent=e?e.title||"Choose from your library":"Image Library"),n.querySelector(".chr2-am-body").innerHTML='<div class="chr2-am-loading">Loading images\u2026</div>',n.classList.remove("hidden"),_campLibRender(n)}async function _campLibPick(e,t){const n=currentCampaign&&currentCampaign.id,a=e._pick;if(!a)return;let s=null;if(a.autoAssign!==!1){const i={target:a.target};a.target==="header"&&(i.session_id=a.session_id),a.target==="entity"&&(i.entity_id=a.entity_id);try{s=await api("POST",`/api/campaigns/${n}/images/${t.id}/assign`,i)}catch(o){showToast(o&&o.message||"Could not use that image.",4e3);return}}clearTimeout(e._libBakeTimer),e._pick=null,e.classList.add("hidden"),typeof a.onPick=="function"&&a.onPick(t,s)}function _libImgBake(e){const t=(e.assignments||[]).filter(n=>n.target==="entity"&&n.treatment);return t.length?t.some(n=>!n.baked&&(n.bake_status==="pending"||n.bake_status==="processing"))?"baking":t.some(n=>n.baked)?"ready":null:null}function _libBakeBadge(e){return e==="baking"?'<span class="cil-bake cil-bake--work" title="Preparing the print treatment\u2026"><span class="tf-spinner tf-spinner--inline" aria-hidden="true"><svg viewBox="0 0 556 580" preserveAspectRatio="xMidYMin meet"><use href="#tf-mark"/><g class="tf-thread-g tf-thread--a"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--b"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--c"><line x1="322" y1="210" x2="322" y2="270"/></g></svg></span>Baking</span>':e==="ready"?'<span class="cil-bake cil-bake--ready" title="Print treatment ready">\u2713 Treated</span>':""}async function _campLibRender(e){const t=currentCampaign&&currentCampaign.id,n=e.querySelector(".chr2-am-body");n.classList.remove("is-uploading");let a=[];try{const m=await api("GET",`/api/campaigns/${t}/images`);a=m&&m.images||[]}catch{n.innerHTML='<p class="chr2-am-none">Could not load the library.</p>';return}const s=_CAMP_LIB_FILTERS.find(m=>m.key===(e._libFilter||"all"))||_CAMP_LIB_FILTERS[0],i=a.filter(s.test),o=_CAMP_LIB_FILTERS.map(m=>`<button type="button" class="cil-tab${m.key===e._libFilter?" is-current":""}" data-filter="${m.key}">${m.label}</button>`).join(""),r=m=>{const f=m.label||m.kind,g=m.assignments&&m.assignments.length?`<span class="chr2-am-thumb-tag">${escHtml(m.assignments[0].label||m.assignments[0].target)}${m.assignments.length>1?" +"+(m.assignments.length-1):""}</span>`:"",h=_libBakeBadge(_libImgBake(m));return`<button type="button" class="chr2-am-thumb" data-img="${m.id}
1674" title="${escHtml(f)}"><img src="${encodeURI(m.url)}" alt="" loading="lazy"><span class="chr2-am-thumb-name">${escHtml(f)}</span>${g}${h}</button>`};let c="";if(e._pick&&e._pick.includeEntityArt&&typeof allEntities<"u"&&Array.isArray(allEntities)){const m={pc:0,companion:1,npc:2,faction:3,deity:4,location:5,item:6},f=allEntities.filter(g=>g&&g.name&&typeof _entityImageUrlRaw=="function"&&_entityImageUrlRaw(g)).sort((g,h)=>(m[g.type]!=null?m[g.type]:9)-(m[h.type]!=null?m[h.type]:9)||(g.name||"").localeCompare(h.name||""));f.length&&(c=`<div class="cil-section-label form-section-label">Character &amp; codex art</div><div class="chr2-am-grid">${f.map(h=>{const v=_entityImageUrlRaw(h);return`<button type="button" class="chr2-am-thumb cil-entity-art" data-ent-url="${encodeURI(v)}" title="${escHtml(h.name)}"><img src="${encodeURI(v)}" alt="" loading="lazy"><span class="chr2-am-thumb-name">${escHtml(h.name)}</span></button>`}).join("")}</div>`)}const l=e._pick?e._pick.subtitle||"Click an image to use it here. Upload a new one below if you need to.":"Reusable campaign images: session headers, covers, maps, and uploads. Character art lives on each Codex entry.";n.innerHTML=`
1675    <p class="chr2-am-sub">${escHtml(l)}</p>
1676    ${c}
1677    ${c?'<div class="cil-section-label form-section-label">Campaign library</div>':""}
1678    <div class="cil-filter-bar">${o}</div>
1679    ${i.length?`<div class="chr2-am-grid">${i.map(r).join("")}</div>`:`<p class="chr2-am-none">No images${e._libFilter!=="all"?" in this filter":" yet"}, upload one below.</p>`}
1680    <label class="chr2-am-upload"><input type="file" accept="image/*" class="hidden">
1681      <span class="chr2-am-upload-cta">Drop an image here, or click to add to the library</span>
1682      <span class="chr2-am-upload-hint">JPG \xB7 PNG \xB7 GIF \xB7 WebP \xB7 10&nbsp;MB max</span></label>`,n.querySelectorAll(".cil-tab[data-filter]").forEach(m=>m.addEventListener("click",()=>{e._libFilter=m.dataset.filter,_campLibRender(e)})),n.querySelectorAll(".chr2-am-thumb[data-img]").forEach(m=>m.addEventListener("click",()=>{const f=a.find(g=>g.id===+m.dataset.img);f&&(e._pick?_campLibPick(e,f):_campLibDetail(e,f))})),n.querySelectorAll(".cil-entity-art[data-ent-url]").forEach(m=>m.addEventListener("click",()=>{_campLibPick(e,{url:m.dataset.entUrl})}));const d=n.querySelector(".chr2-am-upload"),u=d.querySelector('input[type="file"]'),p=async m=>{if(!m||!(m.type||"").startsWith("image/")){showToast("Please choose an image file.",4e3);return}if(m.size>TF_UPLOAD_MAX_BYTES){showToast("Image must be 10 MB or smaller.",4e3);return}n.classList.add("is-uploading");try{await _tfUploadDirect(m,{paramsPath:`/api/campaigns/${t}/upload-params`,kind:"library",endpoint:`/api/campaigns/${t}/images`})}catch{n.classList.remove("is-uploading"),showToast("Upload failed.",4e3);return}_campLibRender(e)};u.addEventListener("change",()=>{const m=u.files&&u.files[0];m&&p(m)}),d.addEventListener("dragover",m=>{m.preventDefault(),d.classList.add("drag-over")}),d.addEventListener("dragleave",()=>d.classList.remove("drag-over")),d.addEventListener("drop",m=>{m.preventDefault(),d.classList.remove("drag-over");const f=m.dataTransfer&&m.dataTransfer.files&&m.dataTransfer.files[0];f&&p(f)}),clearTimeout(e._libBakeTimer),i.some(m=>_libImgBake(m)==="baking")&&(e._libBakeTimer=setTimeout(()=>{e.classList.contains("hidden")||_campLibRender(e)},15e3))}function _campLibDetail(e,t){const n=currentCampaign&&currentCampaign.id,a=e.querySelector(".chr2-am-body"),s=t.assignments&&t.assignments.length?t.assignments.map(c=>{const l=c.target==="entity"&&c.treatment?_libBakeBadge(!c.baked&&(c.bake_status==="pending"||c.bake_status==="processing")?"baking":c.baked?"ready":null):"";return`<span class="cil-assign-chip">${escHtml(c.label||c.target)}${l}</span>`}).join(""):'<span class="cil-assign-none">Not assigned anywhere</span>',i=`${t.label||t.assignments&&t.assignments[0]&&t.assignments[0].label||t.kind}-${t.id}`,o=(allSessions||[]).slice().sort((c,l)=>l.id-c.id).map(c=>`<option value="${c.id}">${escHtml(c.title||"Session "+c.id)}</option>`).join(""),r=(allEntities||[]).filter(c=>c&&c.type).slice().sort((c,l)=>(c.name||"").localeCompare(l.name||"")).map(c=>`<option value="${c.id}">${escHtml(c.name||"")}</option>`).join("");a.innerHTML=`
1683    <button type="button" class="cil-back">&lsaquo; Back to library</button>
1684    <div class="cil-detail">
1685      <div class="cil-detail-art" style="background-image:url('${encodeURI(t.url)}')"></div>
1686      <div class="cil-detail-side">
1687        <span class="chr2-am-eyebrow">Original file</span>
1688        <div class="cil-assign-row"><a class="btn-new cil-download" href="${encodeURI(_tfDownloadUrl(t.url,i))}" download target="_blank" rel="noopener">Download</a></div>
1689        <span class="chr2-am-eyebrow">Currently used as</span>
1690        <div class="cil-assigns">${s}</div>
1691        <span class="chr2-am-eyebrow">Assign as</span>
1692        <div class="cil-assign-row"><button type="button" class="btn-new cil-do" data-target="cover">Campaign background / cover</button></div>
1693        <div class="cil-assign-row"><button type="button" class="btn-new cil-do" data-target="header">Session header</button><select class="login-input cil-sess">${o}</select></div>
1694        <div class="cil-assign-row"><button type="button" class="btn-new cil-do" data-target="map">Add as a new map</button></div>
1695        <div class="cil-assign-row"><button type="button" class="btn-new cil-do" data-target="entity">Entity art</button><select class="login-input cil-ent">${r}</select></div>
1696        <div class="cil-assign-row cil-delete-row"><button type="button" class="btn-danger cil-delete">Delete from everywhere</button></div>
1697      </div>
1698    </div>`,a.querySelector(".cil-back").addEventListener("click",()=>_campLibRender(e)),a.querySelectorAll(".cil-do[data-target]").forEach(c=>c.addEventListener("click",async()=>{const l=c.dataset.target,d={target:l};if(l==="header"&&(d.session_id=+a.querySelector(".cil-sess").value||null,!d.session_id)){showToast("No sessions to assign to.",4e3);return}if(l==="entity"&&(d.entity_id=+a.querySelector(".cil-ent").value||null,!d.entity_id)){showToast("No entities to assign to.",4e3);return}c.disabled=!0;let u;try{u=await api("POST",`/api/campaigns/${n}/images/${t.id}/assign`,d)}catch(p){c.disabled=!1,showToast(p&&p.message||"Could not assign.",4e3);return}currentCampaign&&l==="cover"&&(currentCampaign.cover_image_url=t.url),u&&u.bake_queued?showToast("Assigned, preparing the print treatment. The art shows full-bleed until the bake finishes.",5e3):showToast("Image assigned.",2500),_campLibRender(e)})),a.querySelector(".cil-delete").addEventListener("click",async()=>{const c=t.assignments&&t.assignments.length?t.assignments.map(d=>d.label||d.target).join(", "):null;if(await confirmDialog({title:"Delete image",message:c?`This image is currently used as: ${c}. Deleting removes it from those surfaces AND the library. This can't be undone.`:"Delete this image from the library? This can't be undone.",confirmLabel:"Delete everywhere",danger:!0})){try{await api("DELETE",`/api/campaigns/${n}/images/${t.id}`)}catch(d){showToast(d&&d.message||"Could not delete.",4e3);return}_campLibRender(e)}})}document.getElementById("camp-image-library-btn")?.addEventListener("click",()=>_campLibOpen());const _CCF_ASPECT={binder:200/280,card:2.2};let _ccF=null;function _ccfRect(e,t){if(!_ccF||!_ccF.natW||!_ccF.natH)return null;const{natW:n,natH:a}=_ccF,s=Math.max(1,Math.min(8,+e.z||1)),i=Math.min(n,a*t)/s,o=i/t,r=i/n,c=o/a,l=Math.max(r/2,Math.min(1-r/2,+e.cx||.5)),d=Math.max(c/2,Math.min(1-c/2,+e.cy||.5));return{x:l-r/2,y:d-c/2,w:r,h:c,cx:l,cy:d,z:s}}function _ccfSync(){const e=document.getElementById("camp-frame-modal");if(!e||!_ccF)return;const t=e.querySelector("#ccf-box"),n=e.querySelector("#ccf-zval"),a=_ccF.frames[_ccF.active],s=_ccfRect(a,_CCF_ASPECT[_ccF.active]);if(!s){t.classList.add("hidden");return}t.classList.remove("hidden"),t.style.left=s.x*100+"%",t.style.top=s.y*100+"%",t.style.width=s.w*100+"%",t.style.height=s.h*100+"%",n&&(n.textContent=Math.round((+a.z||1)*100)+"%");const i=e.querySelector("#ccf-warn");if(i){const o=_ccF.active==="binder"?560:316,r=s.h*_ccF.natH;i.classList.toggle("hidden",!(r>0&&o/r>1.15))}e.querySelectorAll(".ccf-chip").forEach(o=>o.classList.toggle("on",o.dataset.frame===_ccF.active))}function _campCoverFramingOpen(){const e=settingsCampaignId||currentCampaign&&currentCampaign.id;if(!e)return;const t=(_csAllCampaigns||[]).find(c=>c.id===e)||currentCampaign,n=t&&t.cover_image_url;if(!n){showToast("Set a cover image first.");return}let a=document.getElementById("camp-frame-modal");if(!a){a=document.createElement("div"),a.id="camp-frame-modal",a.className="modal-overlay hidden",a.innerHTML=`<div class="modal-box modal-box--lg cool-paper-surface camp-frame-box">
1699      <div class="chr2-am-hdr"><span class="chr2-am-title">Cover framing</span>
1700        <button type="button" class="chr2-am-close" aria-label="Close">&times;</button></div>
1701      <p class="form-helper-text ccf-help">Each frame keeps its own crop: pick the frame, then drag to position and scroll or use &plus;/&minus; to zoom.</p>
1702      <div class="cx2-frame-chips">
1703        <button type="button" class="cx2-imgmod-typebtn ccf-chip on" data-frame="binder">Binder cover</button>
1704        <button type="button" class="cx2-imgmod-typebtn ccf-chip" data-frame="card">Card cover</button>
1705      </div>
1706      <div class="ccf-stage">
1707        <div class="cx2-focal-wrap ccf-wrap" id="ccf-wrap" tabindex="0" role="application"
1708             aria-label="Cover framing: drag to position; scroll, plus/minus, or arrow keys to adjust">
1709          <img class="cx2-focal-img" id="ccf-img" alt="" draggable="false">
1710          <div class="cx2-frame-box hidden" id="ccf-box"></div>
1711        </div>
1712      </div>
1713      <div class="cx2-frame-zoomrow" id="ccf-zoomrow">
1714        <button type="button" class="cx2-imgmod-typebtn cx2-frame-zbtn" data-z="-1" aria-label="Zoom out">&minus;</button>
1715        <span class="cx2-frame-zval" id="ccf-zval"></span>
1716        <button type="button" class="cx2-imgmod-typebtn cx2-frame-zbtn" data-z="1" aria-label="Zoom in">+</button>
1717      </div>
1718      <p class="form-helper-text form-helper-text--warn hidden" id="ccf-warn">This zoom will upscale the image, quality may soften.</p>
1719      <div class="ccf-actions">
1720        <button type="button" class="btn-ghost btn-sm" id="ccf-reset">Reset both</button>
1721        <span class="ccf-actions-gap"></span>
1722        <button type="button" class="btn-ghost btn-sm" id="ccf-cancel">Cancel</button>
1723        <button type="button" class="btn-new" id="ccf-save">Save framing</button>
1724      </div></div>`,document.body.appendChild(a);const c=()=>a.classList.add("hidden");a.querySelector(".chr2-am-close").addEventListener("click",c),a.querySelector("#ccf-cancel").addEventListener("click",c),a.addEventListener("click",f=>{f.target===a&&c()}),document.addEventListener("keydown",f=>{_tfModalEsc(f,a)&&c()});const l=a.querySelector("#ccf-wrap"),d=f=>{const g=l.getBoundingClientRect();if(!g.width||!g.height||!_ccF)return;const h=_ccF.frames[_ccF.active];h.cx=Math.max(0,Math.min(1,(f.clientX-g.left)/g.width)),h.cy=Math.max(0,Math.min(1,(f.clientY-g.top)/g.height));const v=_ccfRect(h,_CCF_ASPECT[_ccF.active]);v&&(h.cx=v.cx,h.cy=v.cy),_ccfSync()};let u=!1;l.addEventListener("pointerdown",f=>{f.preventDefault(),u=!0;try{l.setPointerCapture(f.pointerId)}catch{}d(f)}),l.addEventListener("pointermove",f=>{u&&d(f)});const p=f=>{u=!1;try{l.releasePointerCapture(f.pointerId)}catch{}};l.addEventListener("pointerup",p),l.addEventListener("pointercancel",p);const m=f=>{if(!_ccF)return;const g=_ccF.frames[_ccF.active];g.z=Math.max(1,Math.min(8,f));const h=_ccfRect(g,_CCF_ASPECT[_ccF.active]);h&&(g.cx=h.cx,g.cy=h.cy),_ccfSync()};l.addEventListener("wheel",f=>{_ccF&&(f.preventDefault(),m((+_ccF.frames[_ccF.active].z||1)*(f.deltaY<0?1.08:1/1.08)))},{passive:!1}),l.addEventListener("keydown",f=>{if(!_ccF)return;const g={ArrowLeft:[-.01,0],ArrowRight:[.01,0],ArrowUp:[0,-.01],ArrowDown:[0,.01]}[f.key];if(g){f.preventDefault();const h=_ccF.frames[_ccF.active];h.cx=(+h.cx||.5)+g[0],h.cy=(+h.cy||.5)+g[1];const v=_ccfRect(h,_CCF_ASPECT[_ccF.active]);v&&(h.cx=v.cx,h.cy=v.cy),_ccfSync()}else f.key==="+"||f.key==="="?(f.preventDefault(),m((+_ccF.frames[_ccF.active].z||1)*1.15)):(f.key==="-"||f.key==="_")&&(f.preventDefault(),m((+_ccF.frames[_ccF.active].z||1)/1.15))}),a.querySelectorAll(".cx2-frame-zbtn").forEach(f=>f.addEventListener("click",()=>{if(!_ccF)return;const g=+_ccF.frames[_ccF.active].z||1;m(f.dataset.z==="1"?g*1.15:g/1.15)})),a.querySelectorAll(".ccf-chip").forEach(f=>f.addEventListener("click",()=>{_ccF&&(_ccF.active=f.dataset.frame,_ccfSync())})),a.querySelector("#ccf-reset").addEventListener("click",async()=>{const f=_ccF&&_ccF.cid;if(f)try{await api("PUT",`/api/campaigns/${f}`,{cover_framing:null}),_ccfApplyLocal(f,null),a.classList.add("hidden"),showToast("Framing reset to the default fit.")}catch(g){showToast(g&&g.message||"Could not reset framing.")}}),a.querySelector("#ccf-save").addEventListener("click",async()=>{if(!_ccF||!_ccF.natW)return;const f={src:_ccF.url},g=_ccF.natH?_ccF.natW/_ccF.natH:0;for(const h of["binder","card"]){const v=_ccfRect(_ccF.frames[h],_CCF_ASPECT[h]);v&&(f[h]={cx:v.cx,cy:v.cy,z:v.z,x:v.x,y:v.y,w:v.w,h:v.h,...g>0?{a:g}:{}})}try{await api("PUT",`/api/campaigns/${_ccF.cid}`,{cover_framing:f}),_ccfApplyLocal(_ccF.cid,f),a.classList.add("hidden"),showToast("Cover framing saved.")}catch(h){showToast(h&&h.message||"Could not save framing.")}})}const s=t.cover_framing&&t.cover_framing.src===n?t.cover_framing:null,i=c=>{const l=s&&s[c];return l?{cx:+l.cx||.5,cy:+l.cy||.5,z:+l.z||1}:{cx:.5,cy:.5,z:1}};_ccF={cid:e,url:n,natW:0,natH:0,active:"binder",frames:{binder:i("binder"),card:i("card")}};const o=a.querySelector("#ccf-img"),r=()=>{o.naturalWidth&&_ccF&&(_ccF.natW=o.naturalWidth,_ccF.natH=o.naturalHeight,_ccfSync())};o.onload=r,o.src=n,o.complete&&r(),a.querySelector("#ccf-box").classList.add("hidden"),document.body.appendChild(a),a.classList.remove("hidden"),_ccfSync()}function _ccfApplyLocal(e,t){currentCampaign&&currentCampaign.id===e&&(currentCampaign.cover_framing=t);const n=(_csAllCampaigns||[]).find(a=>a.id===e);n&&(n.cover_framing=t),Array.isArray(_csAllCampaigns)&&_csAllCampaigns.length&&_renderCampaignCards(_csAllCampaigns),n&&(settingsCampaignId===e||currentCampaign&&currentCampaign.id===e)&&_renderCampCoverPreview(n.cover_image_url||"")}document.getElementById("camp-cover-frame-btn")?.addEventListener("click",_campCoverFramingOpen),document.getElementById("camp-cover-upload-btn").addEventListener("click",()=>{document.getElementById("camp-cover-file").click()}),document.getElementById("camp-cover-lib-btn")?.addEventListener("click",()=>{currentCampaign&&_campLibOpen({target:"cover",filter:"cover",title:"Cho
1724ose a cover from your library",subtitle:"Pick a loose campaign image to use as the cover / background.",onPick:e=>{_renderCampCoverPreview(e.url||""),currentCampaign&&(currentCampaign.cover_image_url=e.url);const t=(_csAllCampaigns||[]).find(n=>n.id===(settingsCampaignId||currentCampaign&&currentCampaign.id));t&&(t.cover_image_url=e.url),_csAllCampaigns&&_renderCampaignCards(_csAllCampaigns),showToast("Cover updated.")}})}),document.getElementById("camp-cover-gen-btn")?.addEventListener("click",()=>{const e=settingsCampaignId||currentCampaign&&currentCampaign.id;if(!e)return;const t=document.getElementById("camp-cover-gen");if(!t)return;if(!t.classList.contains("hidden")){t.classList.add("hidden"),t.innerHTML="";return}t.classList.remove("hidden");const n=(_csAllCampaigns||[]).find(a=>a.id===e)||currentCampaign;_tfRenderAiImagePanel(t,{suggestUrl:`/api/campaigns/${e}/cover/suggest-scene`,generateUrl:`/api/campaigns/${e}/cover/generate`,sceneLabel:"Scene",suggestLabel:"Suggest from this campaign",scenePlaceholder:"Describe the cover scene (no characters are shown), or use Suggest.",styleSeed:_tfCampaignStyleSeed(n),busyMsg:"Generating cover: this can take ~15\u201330s\u2026",onDone:async a=>{const s=a&&a.cover_image_url;if(!s)return;_renderCampCoverPreview(s),currentCampaign&&currentCampaign.id===e&&(currentCampaign.cover_image_url=s);const i=(_csAllCampaigns||[]).find(o=>o.id===e);i&&(i.cover_image_url=s),_csAllCampaigns&&_renderCampaignCards(_csAllCampaigns),t.classList.add("hidden"),t.innerHTML="",showToast("Cover generated.")}})}),document.getElementById("camp-cover-file").addEventListener("change",async e=>{const t=e.target.files?.[0];if(e.target.value="",!t||!settingsCampaignId)return;if(t.size>TF_UPLOAD_MAX_BYTES){showToast("Cover image must be 10 MB or smaller",5e3);return}const n=document.getElementById("camp-cover-status");n.textContent="Uploading\u2026";try{const a=await _tfUploadDirect(t,{paramsPath:`/api/campaigns/${settingsCampaignId}/upload-params`,kind:"cover",endpoint:`/api/campaigns/${settingsCampaignId}/cover-image`,label:"Cover image"});n.textContent="Saved.",setTimeout(()=>{n.textContent=""},2e3),_renderCampCoverPreview(a.cover_image_url||"");const s=(_csAllCampaigns||[]).find(i=>i.id===settingsCampaignId);s&&(s.cover_image_url=a.cover_image_url),_csAllCampaigns&&_renderCampaignCards(_csAllCampaigns)}catch(a){n.textContent="",showToast("Cover upload failed: "+a.message,6e3)}}),document.getElementById("camp-cover-remove-btn").addEventListener("click",async()=>{if(!settingsCampaignId||!await confirmDialog({title:"Remove Cover Image",message:"Remove the cover image for this campaign?",confirmLabel:"Remove"}))return;const e=document.getElementById("camp-cover-status");e.textContent="Removing\u2026";try{await api("DELETE",`/api/campaigns/${settingsCampaignId}/cover-image`),e.textContent="",_renderCampCoverPreview("");const t=(_csAllCampaigns||[]).find(n=>n.id===settingsCampaignId);t&&(t.cover_image_url=null),_csAllCampaigns&&_renderCampaignCards(_csAllCampaigns)}catch(t){e.textContent="",showToast("Failed to remove cover: "+t.message,6e3)}}),document.getElementById("camp-gm-logo-upload-btn")?.addEventListener("click",()=>{document.getElementById("camp-gm-logo-file")?.click()}),document.getElementById("camp-gm-logo-file")?.addEventListener("change",async e=>{const t=e.target.files?.[0];if(e.target.value="",!t||!settingsCampaignId)return;if(t.size>TF_UPLOAD_MAX_BYTES){showToast("Logo must be 10 MB or smaller",5e3);return}const n=document.getElementById("camp-gm-logo-status");n&&(n.textContent="Uploading\u2026");try{const{url:a}=await _tfUploadDirect(t,{paramsPath:`/api/campaigns/${settingsCampaignId}/upload-params`,kind:"brand",endpoint:`/api/campaigns/${settingsCampaignId}/brand-logo/upload`,label:"Logo"});await api("PUT",`/api/campaigns/${settingsCampaignId}`,{gm_brand_logo_url:a}),currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.gm_brand_logo_url=a),_renderGmLogoPreview(a),n&&(n.textContent="Saved.",setTimeout(()=>{n.textContent=""},2e3))}catch(a){n&&(n.textContent=""),showToast("Logo upload failed: "+a.message,6e3)}}),document.getElementById("camp-gm-logo-remove-btn")?.addEventListener("click",async()=>
1724{if(!settingsCampaignId||!await confirmDialog({title:"Remove GM Brand Logo",message:"Remove your brand logo from this campaign?",confirmLabel:"Remove"}))return;const e=document.getElementById("camp-gm-logo-status");e&&(e.textContent="Removing\u2026");try{await api("PUT",`/api/campaigns/${settingsCampaignId}`,{gm_brand_logo_url:null}),currentCampaign&&currentCampaign.id===settingsCampaignId&&(currentCampaign.gm_brand_logo_url=null),_renderGmLogoPreview(""),e&&(e.textContent="")}catch(t){e&&(e.textContent=""),showToast("Failed to remove logo: "+t.message,6e3)}});let _deleteCampSource="settings";function _openCampDeleteModal(e,t,n="settings"){settingsCampaignId=e,_deleteCampSource=n,document.getElementById("camp-delete-name").textContent=t,document.getElementById("camp-delete-input").value="",document.getElementById("camp-delete-confirm").disabled=!0,document.getElementById("camp-delete-confirm").textContent="Delete Campaign",_tfCoolPaperizeModal(document.getElementById("camp-delete-modal")),document.getElementById("camp-delete-modal").classList.remove("hidden")}document.getElementById("camp-delete-btn").addEventListener("click",()=>{const e=currentCampaign?.name||"";_openCampDeleteModal(settingsCampaignId||currentCampaign?.id,e,"settings")}),document.getElementById("camp-export-btn")?.addEventListener("click",e=>{_tfExportCampaign(settingsCampaignId||currentCampaign?.id,e.currentTarget,"Export Campaign Data")}),document.getElementById("profile-export-btn")?.addEventListener("click",e=>{e.preventDefault();const t=e.currentTarget;if(t.dataset.busy)return;t.dataset.busy="1";const n=t.textContent;t.textContent="Preparing\u2026",_tfDownloadBlob("/api/profile/export","threadfall-account-data.json").catch(()=>showToast("Download failed, try again.",4e3)).finally(()=>{delete t.dataset.busy,t.textContent=n})}),document.getElementById("camp-delete-input").addEventListener("input",e=>{document.getElementById("camp-delete-confirm").disabled=e.target.value!=="DELETE"});function _closeCampDeleteModal(){document.getElementById("camp-delete-modal").classList.add("hidden"),document.getElementById("camp-delete-input").value="",document.getElementById("camp-delete-confirm").disabled=!0}document.getElementById("camp-delete-cancel").addEventListener("click",_closeCampDeleteModal),document.getElementById("camp-delete-close").addEventListener("click",_closeCampDeleteModal);let _campDeleteModalMousedownOnBackdrop=!1;document.getElementById("camp-delete-modal").addEventListener("mousedown",e=>{_campDeleteModalMousedownOnBackdrop=e.target===document.getElementById("camp-delete-modal")}),document.getElementById("camp-delete-modal").addEventListener("click",e=>{e.target===document.getElementById("camp-delete-modal")&&_campDeleteModalMousedownOnBackdrop&&_closeCampDeleteModal()}),document.getElementById("camp-delete-confirm").addEventListener("click",async()=>{const e=settingsCampaignId,t=_deleteCampSource,n=document.getElementById("camp-delete-confirm");n.disabled=!0,n.textContent="Deleting\u2026";try{await api("DELETE",`/api/campaigns/${e}`),_csAnthologiesLoaded=!1,_closeCampDeleteModal(),settingsCampaignId=null,showToast("Campaign deleted."),t==="admin"?await adminLoadCampaigns():document.getElementById("nav-logo-btn").click()}catch(a){showToast(a.message,5e3),n.disabled=!1,n.textContent="Delete Campaign"}});let _sessionDeleteId=null;function _openSessionDeleteModal(e,t){_sessionDeleteId=e,document.getElementById("session-delete-name").textContent=t,document.getElementById("session-delete-input").value="",document.getElementById("session-delete-confirm").disabled=!0,document.getElementById("session-delete-confirm").textContent="Delete Session",document.getElementById("session-delete-modal").classList.remove("hidden")}function _closeSessionDeleteModal(){document.getElementById("session-delete-modal").classList.add("hidden"),document.getElementById("session-delete-input").value="",document.getElementById("session-delete-confirm").disabled=!0,_sessionDeleteId=null}document.getElementById("session-delete-input").addEventListener("input",e=>{document.getElementById("session-delete-confirm").disabled=e.target.value!=="DELETE"}),document.getElementById("session-delete-cancel").addEventListener("click",_closeSessionDeleteModal),document.getElementById("session-delete-close").addEventListener("click",_closeSessionDeleteModal);let _sessionDeleteModalMousedownOnBackdrop=!1;document.getElementById("session-delete-modal").addEventListener("mousedown",e=>{_sessionDeleteModalMousedownOnBackdrop=e.target===document.getElementById("session-delete-modal")}),document.getElementById("session-delete-modal").addEventListener("click",e=>{e.target===document.getElementById("session-delete-modal")&&_sessionDeleteModalMousedownOnBackdrop&&_closeSessionDeleteModal()}),document.getElementById("session-delete-confirm").addEventListener("click",async()=>{const e=_sessionDeleteId,t=document.getElementById("session-delete-confirm");if(!(!e||!currentCampaign)){t.disabled=!0,t.textContent="Deleting\u2026";try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/sessions/${e}`),allSessions=allSessions.filter(n=>n.id!==e),expandedSessionId===e&&(expandedSessionId=null),_closeSessionDeleteModal(),_chr2DeletedFromDrawer&&(_chr2DeletedFromDrawer=!1,typeof renderChronicleTimeline=="function"&&renderChronicleTimeline()),showToast("Session deleted.")}catch(n){showToast(n.message,5e3),t.disabled=!1,t.textContent="Delete Session"}}});function _openCreateCampaignModal(){document.getElementById("new-camp-name").value="",document.getElementById("create-camp-error").classList.add("hidden"),document.getElementById("create-campaign-modal").classList.remove("hidden"),document.getElementById("new-camp-name").focus()}document.getElementById("new-campaign-btn")?.addEventListener("click",_openCreateCampaignModal),document.getElementById("cs-empty-new-campaign-btn")?.addEventListener("click",_openCreateCampaignModal);const _AIMPORT_TIERS=[{mode:"enhanced",name:"Enhanced",badge:"Recommended",pitch:"Your recaps kept word-for-word, plus a bard's-voice intro above each chapter, Loose Threads surfaced, and quotes pulled out. What most GMs choose."},{mode:"faithful",name:"Faithful",pitch:"Just your words. We organize, type, and cross-link your campaign, nothing is rewritten. No AI."},{mode:"recast",name:"Chronicle Recast",pitch:"Every session retold in Threadfall's bardic voice, a fresh chronicle rather than an archive. Uses the most AI."}];let _aimportFile=null,_aimportMode="enhanced",_aimportJobId=null,_aimportDoneCid=null,_aimportPollTok=0;function _aimportStep(e){["upload","preview","progress","done","failed"].forEach(t=>document.getElementById("aimport-step-"+t)?.classList.toggle("hidden",t!==e))}function _aimportResetDropLabel(){document.getElementById("aimport-drop-label").innerHTML="Drop your Archivist <b>.zip</b> here, or click to choose"}function _aimportOpen(){_aimportFile=null,_aimportMode="enhanced",_aimportJobId=null,_aimportDoneCid=null,_aimportPollTok++,document.getElementById("aimport-file").value="",_aimportResetDropLabel(),document.getElementById("aimport-upload-error").classList.add("hidden"),document.getElementById("aimport-start-error").classList.add("hidden"),_aimportStep("upload"),document.getElementById("archivist-import-modal").classList.remove("hidden")}function _aimportClose(){_aimportPollTok++,document.getElementById("archivist-import-modal").classList.add("hidden")}async function _aimportUploadReq(e,t){const n=new FormData;n.append("file",_aimportFile),t&&Object.entries(t).forEach(([o,r])=>n.append(o,r));const a=await fetch(e,{method:"POST",headers:{Authorization:"Bearer "+(localStorage.getItem("tf_jwt")||"")},body:n}),s=await a.text();let i;try{i=JSON.parse(s)}catch{i={detail:s}}if(!a.ok)throw new Error(i&&i.detail||a.statusText);return i}async function _aimportChoose(e){if(!e)return;_aimportFile=e;const t=document.getElementById("aimport-upload-error");t.classList.add("hidden"),document.getElementById("aimport-drop-label").textContent="Reading "+e.name+"\u2026";try{const n=await _aimportUploadReq("/api/import/archivist/preview");_aimportRenderPreview(n),_aimportStep("preview")}catch(n){_aimportResetDropLabel(),t.textContent=n.message,t.classList.remove("hidden")}}function _aimportRenderPreview(e){const t=(i,o)=>`<div class="aimport-stat"><span class="aimport-stat-num">${i}</span><span class="aimport-stat-lbl">${o}</span></div>
1724`,n=e.first_session;let a=`<div class="aimport-stat-grid">
1725      ${t(e.sessions,"sessions")}
1726      ${t(e.entities_create,"entities")}
1727      ${t(e.quests,"quests")}
1728      ${e.portraits?t(e.portraits,"portraits"):""}
1729    </div>`;const s=e.date_range&&e.date_range[0]?`${e.date_range[0]} \u2192 ${e.date_range[1]}`:"";(s||e.campaign_name)&&(a+=`<p class="form-helper-text">${s?"Played "+s:""}${s&&e.campaign_name?" \xB7 ":""}${e.campaign_name?escHtml(e.campaign_name):""}</p>`),n&&(a+=`<div class="aimport-excerpt"><span class="aimport-excerpt-hdr">Cleaned Session ${n.number}: ${escHtml(n.title||"")}</span><br>${escHtml(n.cleaned_excerpt||"")}\u2026</div>`),document.getElementById("aimport-preview-summary").innerHTML=a,document.getElementById("aimport-tiers").innerHTML=_AIMPORT_TIERS.map(i=>`
1730    <div class="aimport-tier${i.mode===_aimportMode?" selected":""}" data-mode="${i.mode}" role="button" tabindex="0" aria-pressed="${i.mode===_aimportMode}">
1731      <div class="aimport-tier-top"><span class="aimport-tier-name">${i.name}</span>${i.badge?`<span class="aimport-tier-badge">${i.badge}</span>`:""}</div>
1732      <p class="aimport-tier-pitch">${i.pitch}</p>
1733    </div>`).join(""),document.querySelectorAll("#aimport-tiers .aimport-tier").forEach(i=>{const o=()=>{_aimportMode=i.dataset.mode,document.querySelectorAll("#aimport-tiers .aimport-tier").forEach(r=>{const c=r===i;r.classList.toggle("selected",c),r.setAttribute("aria-pressed",c)})};i.addEventListener("click",o),i.addEventListener("keydown",r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),o())})})}async function _aimportConfirm(){const e=document.getElementById("aimport-start-error");e.classList.add("hidden");const t=document.getElementById("aimport-confirm");t.disabled=!0;try{const n=await _aimportUploadReq("/api/import/archivist/start",{mode:_aimportMode});_aimportJobId=n.job_id,_aimportStep("progress"),_aimportSetProgress(0,n.total_sessions||0,"Starting\u2026"),_aimportPoll()}catch(n){e.textContent=n.message,e.classList.remove("hidden")}finally{t.disabled=!1}}function _aimportSetProgress(e,t,n){const a=t>0?Math.max(5,Math.round(e/t*100)):5;document.getElementById("aimport-progress-bar").style.width=a+"%",n&&(document.getElementById("aimport-progress-label").textContent=n)}async function _aimportPoll(){const e=++_aimportPollTok,t=_aimportJobId;for(;e===_aimportPollTok;){let n;try{n=await api("GET",`/api/import/${t}/status`)}catch{await new Promise(i=>setTimeout(i,4e3));continue}if(e!==_aimportPollTok)return;if(n.status==="done"){_aimportRenderDone(n),_aimportStep("done");return}if(n.status==="failed"){_aimportRenderFailed(n),_aimportStep("failed");return}const a=n.current_label||"";_aimportSetProgress(n.step||0,n.total_sessions||0,/^Chapter/.test(a)?"Rebuilding "+a.toLowerCase():a||"Working\u2026"),await new Promise(s=>setTimeout(s,2500))}}function _aimportRenderDone(e){_aimportDoneCid=e.campaign_id;const t=e.result||{},n=[];t.sessions_created&&n.push(`${t.sessions_created} chapters`),t.entities_created&&n.push(`${t.entities_created} codex entries`),t.quests&&n.push(`${t.quests} quests`),t.quotes&&n.push(`${t.quotes} quotes`),document.getElementById("aimport-done-summary").innerHTML=`<div class="aimport-done-title">Your campaign is ready.</div>
1734     <p class="form-helper-text">${escHtml(t.campaign_name||"")}${n.length?": "+n.join(" \xB7 "):""}.</p>`}function _aimportRenderFailed(e){document.getElementById("aimport-failed-msg").textContent=e.error||"The import didn't finish.",document.getElementById("aimport-undo2").classList.toggle("hidden",!e.campaign_id)}async function _aimportUndo(){if(_aimportJobId){try{await api("POST",`/api/import/${_aimportJobId}/undo`),showToast("Import undone.")}catch(e){showToast("Couldn't undo: "+e.message)}_aimportClose();try{typeof loadCampaignSelector=="function"&&await loadCampaignSelector()}catch{}}}document.getElementById("aimport-close")?.addEventListener("click",_aimportClose),(()=>{const e=document.getElementById("aimport-drop"),t=document.getElementById("aimport-file");!e||!t||(e.addEventListener("click",()=>t.click()),e.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),t.click())}),e.addEventListener("dragover",n=>{n.preventDefault(),e.classList.add("drag-over")}),e.addEventListener("dragleave",()=>e.classList.remove("drag-over")),e.addEventListener("drop",n=>{n.preventDefault(),e.classList.remove("drag-over"),_aimportChoose(n.dataTransfer.files[0])}),t.addEventListener("change",()=>_aimportChoose(t.files[0])))})(),document.getElementById("aimport-back")?.addEventListener("click",()=>{document.getElementById("aimport-file").value="",_aimportResetDropLabel(),_aimportStep("upload")}),document.getElementById("aimport-confirm")?.addEventListener("click",_aimportConfirm),document.getElementById("aimport-open")?.addEventListener("click",()=>{const e=_aimportDoneCid;_aimportClose(),e&&bootWithCampaignId(e)}),document.getElementById("aimport-undo")?.addEventListener("click",_aimportUndo),document.getElementById("aimport-undo2")?.addEventListener("click",_aimportUndo),document.getElementById("aimport-retry")?.addEventListener("click",_aimportOpen);function _applyCsCommunitySection(e){document.getElementById("cs-community-section")?.classList.toggle("hidden",!e),e?(_csRenderStacks(),_csInitShowcaseVideo(),_csStartAlmanacCycle()):(_csStopStacksCycle(),_csStopAlmanacCycle())}let _csAlmanacTimer=null;function _csStartAlmanacCycle(){const e=document.getElementById("cs-almanac-stats");if(!e)return;const t=Array.from(e.querySelectorAll(".cs-almanac-stat"));if(t.length<2)return;_csAlmanacTimer&&clearInterval(_csAlmanacTimer);let n=t.findIndex(a=>!a.classList.contains("hidden"));n<0&&(n=0,t[0].classList.remove("hidden")),_csAlmanacTimer=setInterval(()=>{e.matches(":hover")||(t[n].classList.add("hidden"),n=(n+1)%t.length,t[n].classList.remove("hidden"))},8e3)}function _csStopAlmanacCycle(){_csAlmanacTimer&&(clearInterval(_csAlmanacTimer),_csAlmanacTimer=null)}function _csInitShowcaseVideo(){const e=document.querySelector("#cs-community-section .cs-comm-showcase .cs-comm-video"),t=document.querySelector("#cs-community-section .cs-comm-showcase .cs-comm-wash");if(!e||!t||e.dataset.washBound)return;e.dataset.washBound="1";const n=1,a=3,s=1,i=150,o=6,r=m=>m*m*(3-2*m),c=(m,f)=>{t.style.opacity=m.toFixed(3),t.style.setProperty("--wash-size",(i-f*(i-o)).toFixed(1)+"%")};let l="play",d=0,u=0;const p=()=>{requestAnimationFrame(p);const m=e.duration;if(!m||isNaN(m)||m<=n+s+.5){t.style.opacity="0";return}if(l==="hold"){t.style.opacity="1",performance.now()-d>=a*1e3&&(l="play",t.classList.remove("cs-comm-wash--held"),e.play?.().catch(()=>{})),u=0;return}const f=e.currentTime;if(u>m-n-.5&&f<u-1){e.pause();try{e.currentTime=0}catch{}l="hold",d=performance.now(),c(1,1),t.classList.add("cs-comm-wash--held"),u=0;return}u=f;let g=0;f>m-n?g=(f-(m-n))/n:f<s&&(g=1-f/s),g=Math.max(0,Math.min(1,g)),c(r(g),g)};requestAnimationFrame(p),e.play?.().catch(()=>{})}let _csStacks=[],_csStacksIdx=0,_csStacksTimer=null;function _csStacksPaint(){const e=document.getElementById("cs-stacks-now"),t=_csStacks[_csStacksIdx];if(!e||!t)return;const n=escHtml(t.title||t.name||"A chronicle"),a=t.gm_credit_name?"GM "+escHtml(t.gm_credit_name):"A published chronicle",s=document.getElementById("cs-stacks-art");s&&(s.style.backgroundImage=t.cover_image_url?`url('${encodeURI(t.cover_image_url)}')`:""),e.innerHTML=`<span class="cs-stacks-now-name">${n}</span><span class="cs-stacks-now-credit">${a}</span>`}function _csStopStacksCycle(){_csStacksTimer&&(clearInterval(_csStacksTimer),_csStacksTimer=null)}function _csStartStacksCycle(){_csStopStacksCycle(),!(_csStacks.length<=1)&&(_csStacksTimer=setInterval(()=>{const e=document.getElementById("cs-stacks-now"),t=document.getElementById("cs-stacks-art");if(!e){_csStopStacksCycle();return}e.classList.add("is-fading"),t&&t.classList.add("is-fading"),setTimeout(()=>{_csStacksIdx=(_csStacksIdx+1)%_csStacks.length,_csStacksPaint(),e.classList.remove("is-fading"),t&&t.classList.remove("is-fading")},360)},5e3))}async function _csRenderStacks(){const e=document.getElementById("cs-stacks-tile"),t=document.querySelector(".cs-community-row");if(!e||!t)return;_csStopStacksCycle();let n=[];try{const a=await api("GET","/api/public/campaigns");n=a&&Array.isArray(a.campaigns)?a.campaigns:[]}catch{n=[]}
1734if(_csStacks=n,_csStacksIdx=0,typeof _csSyncShelfMeta=="function"&&_csSyncShelfMeta(),!n.length){e.classList.add("hidden"),t.classList.remove("has-stacks");return}t.classList.add("has-stacks"),e.classList.remove("hidden"),_csStacksPaint(),_csStartStacksCycle()}function _csStacksModalRows(){const e=document.getElementById("stacks-list");if(e){if(!_csStacks.length){e.innerHTML='<p class="stacks-empty">No published chronicles yet.</p>';return}e.innerHTML=_csStacks.map(t=>{const n=escHtml(t.title||t.name||"A chronicle"),a=t.snippet?escHtml(t.snippet):"A chronicle still being written.",s=t.gm_credit_name?`<span class="stacks-row-credit">GM ${escHtml(t.gm_credit_name)}</span>`:"";return`<a class="stacks-row" role="listitem" href="${"/c/"+encodeURIComponent(t.slug)}" target="_blank" rel="noopener noreferrer"><span class="stacks-row-title">${n}</span><span class="stacks-row-leader" aria-hidden="true"></span><span class="stacks-row-cta">Read \u2192</span><span class="stacks-row-teaser">${a}</span>`+s+"</a>"}).join("")}}function _csOpenStacksModal(){_csStacksModalRows(),document.getElementById("stacks-modal")?.classList.remove("hidden")}function _csCloseStacksModal(){document.getElementById("stacks-modal")?.classList.add("hidden")}document.getElementById("cs-stacks-tile")?.addEventListener("click",_csOpenStacksModal),document.getElementById("cs-stacks-tile")?.addEventListener("mouseenter",_csStopStacksCycle),document.getElementById("cs-stacks-tile")?.addEventListener("mouseleave",_csStartStacksCycle),document.getElementById("stacks-modal-close")?.addEventListener("click",_csCloseStacksModal),document.getElementById("stacks-modal")?.addEventListener("click",e=>{e.target.id==="stacks-modal"&&_csCloseStacksModal()}),document.addEventListener("keydown",e=>{_tfModalEsc(e,document.getElementById("stacks-modal"))&&_csCloseStacksModal()}),document.getElementById("create-camp-close").addEventListener("click",()=>document.getElementById("create-campaign-modal").classList.add("hidden")),document.getElementById("create-camp-cancel").addEventListener("click",()=>document.getElementById("create-campaign-modal").classList.add("hidden")),document.querySelectorAll("#create-campaign-modal .create-camp-go").forEach(e=>{e.addEventListener("click",()=>_createCampaignFromModal(e.dataset.campSystem,e))}),document.getElementById("create-campaign-modal-form").addEventListener("submit",e=>{e.preventDefault();const t=document.getElementById("new-camp-name");t&&t.value.trim()&&document.querySelector("#create-campaign-modal .create-camp-go")?.focus()});async function _createCampaignFromModal(e,t){const n=document.getElementById("new-camp-name").value.trim(),a=!0,s=document.getElementById("create-camp-error");if(s.classList.add("hidden"),!n){s.textContent="Give it a name first.",s.classList.remove("hidden"),document.getElementById("new-camp-name").focus();return}const i=[...document.querySelectorAll("#create-campaign-modal .create-camp-go")],o=t?t.textContent:"";i.forEach(r=>{r.disabled=!0}),t&&(t.textContent="Creating\u2026");try{currentCampaign=await api("POST","/api/campaigns",{name:n,use_extended:a,game_system:e}),currentRole=currentCampaign.user_role||"gm",document.getElementById("create-campaign-modal").classList.add("hidden"),applyRoleUI(),await showMain(),typeof _tfRoll20MaybeContinueImport=="function"&&_tfRoll20MaybeContinueImport()}catch(r){s.textContent=r.message,s.classList.remove("hidden")}finally{i.forEach(r=>{r.disabled=!1}),t&&(t.textContent=o)}}function _isEmailUnverifiedError(e){return!!(e&&e.status===403&&e.detail&&e.detail.code==="email_unverified")}function _showVerifyWall(){const e=document.getElementById("verify-wall-modal");if(!e)return;const t=document.getElementById("verify-wall-email");t&&(t.textContent=currentUser&&currentUser.email||"your inbox"),document.getElementById("verify-wall-resend-msg")?.classList.add("hidden"),e.classList.remove("hidden")}function _hideVerifyWall(){document.getElementById("verify-wall-modal")?.classList.add("hidden")}document.getElementById("verify-wall-close")?.addEventListener("click",_hideVerifyWall),document.getElementById("verify-wall-dismiss")?.addEventListener("click",_hideVerifyWall),document.getElementById("verify-wall-resend")?.addEventListener("click",async e=>{const t=e.currentTarget,n=currentUser&&currentUser.email,a=document.getElementById("verify-wall-resend-msg");
1734if(!n)return;t.disabled=!0;const s=t.textContent;t.textContent="Sending\u2026";try{await api("POST","/api/auth/resend-verification",{email:n}),a&&(a.textContent="Sent, check "+n+" for the link.",a.classList.remove("hidden"))}catch{a&&(a.textContent="Couldn\u2019t resend right now, try again in a moment.",a.classList.remove("hidden"))}finally{t.disabled=!1,t.textContent=s}}),document.getElementById("nav-logo-btn").addEventListener("click",async()=>{_presExitLive(),_presStopPoll(),_stopWatcherPoll(),_watcherIsLive=!1,document.getElementById("live-broadcast-banner")?.classList.add("hidden"),document.getElementById("live-watcher-panel")?.classList.add("hidden"),currentCampaign=null,_realIsAdmin!==null&&currentUser&&(currentUser.is_admin=_realIsAdmin),_realRole=null,_realIsAdmin=null,document.getElementById("profile-nav-btn").classList.add("hidden");const e=document.getElementById("nav-campaign-name");if(e){const n=e.querySelector(".ncn-text");n&&(n.textContent=""),e.title=""}try{document.title=_tfDocTitleFor(!1)}catch{}const t=document.querySelector(".nav-right");t&&(t.classList.remove("has-cover"),t.style.removeProperty("--cover-img")),showScreen("campaign-selector"),await loadCampaignSelector(),typeof _tfRouterPush=="function"&&_tfRouterPush({type:"selector"})});function _avatarColor(e){const t=["#0D9488","#D97706","#7C3AED","#DB2777","#2563EB","#16A34A","#EA580C"];let n=0;for(let a=0;a<e.length;a++)n=n*31+e.charCodeAt(a)>>>0;return t[n%t.length]}function _profileInitials(e){const t=e?.display_name||e?.email||"?",n=t.split(/[\s@._-]+/).filter(Boolean);return n.length>=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,2).toUpperCase()}function _avatarHtml(e,t,n){if(e)return`<img src="${escHtml(e)}" alt="${escHtml(t)}" style="width:${n}px;height:${n}px;object-fit:cover;border-radius:50%">`;const a=_profileInitials({display_name:t});return`<span style="color:#fff;background:${_avatarColor(t||"?")};width:100%;height:100%;display:flex;align-items:center;justify-content:center;border-radius:50%;font-size:${Math.round(n*.38)}px;font-weight:700">${escHtml(a)}</span>`}function updateNavAvatar(){if(!currentUser)return;const e=_avatarHtml(currentUser.avatar_url,currentUser.display_name||currentUser.email,28),t=document.getElementById("nav-avatar-img");t&&(t.innerHTML=e);const n=document.getElementById("cs-avatar-img");n&&(n.innerHTML=e)}function _renderDiscordLinkButton(){const e=document.getElementById("profile-discord-btn");if(!e||!currentUser)return;const t=currentUser.discord_username||"",n=!!(currentUser.discord_user_id&&t),a=e.querySelector("svg"),s=a?a.outerHTML:"";e.classList.toggle("is-linked",n),e.title=n?"Click to re-link to a different Discord account":"Import avatar from Discord",e.innerHTML=s+" "+(n?`\u2713 Linked as @${escHtml(t)}`:"Link Discord")}function openProfileModal(){const e=document.getElementById("profile-modal");if(!e||!currentUser)return;_initTestRecSettings();const t=document.getElementById("profile-avatar-lg");t.innerHTML=_avatarHtml(currentUser.avatar_url,currentUser.display_name||currentUser.email,88),document.getElementById("profile-display-name").value=currentUser.display_name||"",document.getElementById("profile-email-readonly").textContent=currentUser.email||"";const n=document.getElementById("profile-pw-email");n&&(n.value=currentUser.email||""),document.getElementById("profile-pw-msg")?.classList.add("hidden");let a="dark";try{a=localStorage.getItem("theme")==="light"?"light":"dark"}catch{}document.querySelectorAll(".profile-theme-toggle button[data-theme-pref]").forEach(c=>{c.classList.toggle("active",c.dataset.themePref===a)});const s=localStorage.getItem("tf_profanity_filter")==="true",i=document.getElementById("profile-profanity-toggle"),o=document.getElementById("profile-profanity-label");i&&(i.checked=s),o&&(o.textContent=s?"On":"Off"),_syncPublicHideToggle(currentUser&&currentUser.hide_from_public_pages||!1),api("GET","/api/profile").then(c=>{c&&typeof c.hide_from_public_pages=="boolean"&&(currentUser&&(currentUser.hide_from_public_pages=c.hide_from_public_pages),_syncPublicHideToggle(c.hide_from_public_pages))}).catch(()=>{});const r=document.getElementById("profile-pc-badge");r&&(currentCampaign?api("GET",`/api/campaigns/${currentCampaign.id}/my-pc`).then(c=>{const l=Array.isArray(c)?c[0]:c;r.textContent=l?`\u2694 ${l.name}`:"",r.style.display=l?"":"none"}).catch(()=>{r.style.display="none"}):r.style.display="none"),_renderProfileArchived(),_renderProfileMyCampaigns(),_renderDiscordLinkButton(),e.classList.remove("hidden"),document.getElementById("profile-display-name").focus(),_renderProfileBillingSections(),_renderProfileReferralCard()}async function _renderProfileReferralCard(){const e=document.getElementById("profile-referral-card");if(!e)return;let t;try{t=await api("GET","/api/profile/referral-code")}catch{e.classList.add("hidden");return}const n=document.getElementById("profile-referral-link"),a=document.getElementById("profile-referral-stats"),s=document.getElementById("profile-referral-copy-btn");if(n&&(n.value=t.link||""),a){const i=Number(t.total_credited??0),o=i*(t.credits_per_referral??2);i>0?a.textContent=`${i} friend${i===1?"":"s"} subscribed \xB7 ${o} credit${o===1?"":"s"} earned.`:a.textContent="Unlimited invites. Credits arrive when a friend subscribes."}e.classList.remove("hidden"),s&&!s.dataset.wired&&(s.dataset.wired="1",s.addEventListener("click",async()=>{const i=n?.value||"";if(!i)return;const o=s.textContent;let r=!1;try{navigator.clipboard?.writeText&&(await navigator.clipboard.writeText(i),r=!0)}catch{}if(!r&&n){n.select();try{r=document.execCommand("copy")}catch{}}s.textContent=r?"Copied!":"Press Ctrl+C",setTimeout(()=>{s.textContent=o},1500)}))}const _TIER_PRICE_LABEL_MONTHLY={free:"",hobby:"$10/mo",standard:"$25/mo",foun
1734ding_gm:"$7.99/mo",master:"$40/mo",pro:"$60/mo"},_TIER_PRICE_LABEL_ANNUAL={free:"",hobby:"$100/yr",standard:"$250/yr",founding_gm:"$79.90/yr",master:"$400/yr",pro:"$600/yr"},_TIER_PRICE_LABEL=_TIER_PRICE_LABEL_MONTHLY,_TIER_DELTA_COPY={free:"Threadfall recaps your sessions and auto-builds your campaign wiki, start free with 2 recaps on the house.",hobby:"Standard unlocks 6 recaps/mo + 3 campaigns.",standard:"Master adds AI Prep Suite: surfaces unresolved plot hooks and writes prep scenes between sessions.",founding_gm:"Master adds AI Prep Suite: surfaces unresolved plot hooks and writes prep scenes between sessions.",master:"Pro unlocks unlimited campaigns + unlimited live sessions.",pro:""};async function _renderProfileBillingSections(){try{await _tfLoadBillingTier()}catch{}const e=_tfBillingStatus||{},t=e.tier||"free";_renderProfileFoundingBanner(e),_renderProfileSubscriptionCard(e,t),_renderProfileUsageCard(e,t)}function _renderProfileFoundingBanner(e){const t=document.getElementById("profile-founding-banner"),n=document.getElementById("profile-founding-countdown");if(!t||!n)return;const a=!!e.founding_gm_eligible,s=e.founding_gm_locked_at,i=e.founding_gm_window_expires_at,o=e.founding_gm_deadline;if(!a||s){t.classList.add("hidden");return}const r=i?new Date(i):o?new Date(o+"T23:59:59Z"):null;if(!r){t.classList.add("hidden");return}const c=new Date;if(c>r){t.classList.add("hidden");return}const l=Math.ceil((r.getTime()-c.getTime())/864e5);n.textContent=`${l} day${l===1?"":"s"} left to lock in the rate.`,t.classList.remove("hidden")}function _renderProfileSubscriptionCard(e,t){const n=document.getElementById("profile-subscription-tier"),a=document.getElementById("profile-subscription-period"),s=document.getElementById("profile-subscription-delta"),i=document.getElementById("profile-subscription-cta");if(!n||!i)return;const o=_TIER_LABELS[t]||"Free",r=e.interval==="annual"?_TIER_PRICE_LABEL_ANNUAL[t]||"":_TIER_PRICE_LABEL_MONTHLY[t]||"";if(n.textContent=r?`${o} \xB7 ${r}`:o,e.current_period_end&&t!=="free"){const c=new Date(e.current_period_end),l={month:"long",day:"numeric",year:"numeric"},d=c.toLocaleDateString(void 0,l);a.textContent=e.cancel_at_period_end?`Cancels ${d}`:`Renews ${d}`}else a.textContent="";s.textContent=_TIER_DELTA_COPY[t]||"",t==="free"?(i.textContent="See Plans \u2192",i.onclick=()=>{document.getElementById("profile-modal")?.classList.add("hidden"),_showPricingScreen()}):(i.textContent="Manage Subscription \u2192",i.onclick=async()=>{if(!(t==="founding_gm"&&!await confirmDialog({title:"Before you open the Stripe portal",message:`Your Founding GM rate at $7.99/month is locked in only while the subscription stays active. Cancel (even once) and the rate is gone; resubscribing later will be at full Standard pricing ($25/mo).
1735
1736Open the portal anyway?`,confirmLabel:"Open Stripe portal",cancelLabel:"Stay subscribed",danger:!1}))){i.disabled=!0;try{await _tfOpenBillingPortal()}finally{i.disabled=!1}}})}function _renderProfileUsageCard(e,t){const n=document.getElementById("profile-usage-recaps"),a=document.getElementById("profile-usage-campaigns"),s=document.getElementById("profile-usage-bank"),i=document.getElementById("profile-usage-fullsess"),o=document.getElementById("profile-usage-recap"),r=document.getElementById("profile-usage-nudge");if(!n||!a||!s||!r)return;const c=e.usage||{},l=e.tier_caps||{},d=c.sessions_this_period||0,u=l.sessions_per_month;n.textContent=u>=100||u==null?`${d} run this period`:`${d} of ${u} run this period`;const p=c.campaigns_count||0,m=l.campaigns;a.textContent=m>=100||m==null?`${p} (unlimited)`:`${p} of ${m}`;const f=document.getElementById("profile-usage-images-row"),g=document.getElementById("profile-usage-images");if(f&&g){const E=c.images_this_period||0,k=l.header_images_per_month;k>0?(g.textContent=`${E} of ${k} this month`,f.classList.remove("hidden")):f.classList.add("hidden")}const h=Number(c.session_credits_remaining??0),v=Number(c.recap_credits_remaining??0),b=_tfHalfCredits(c,"session"),y=_tfHalfCredits(c,"recap");if(s.textContent=_tfCreditCount(h,b),i)if(u>=100||u==null)i.textContent=h>
17360||b?`Unlimited + ${_tfCreditCount(h,b)} banked`:"Unlimited";else{const k=Math.max(0,(u||0)-d)+h,C=h>0||b?` (incl. ${_tfCreditCount(h,b)} banked)`:"";i.textContent=`${_tfCreditCount(k,b)}${C}`}o&&(o.textContent=v>0||y?`${_tfCreditCount(v,y)} banked`:"0, buy more");const _=document.getElementById("profile-usage-reset");if(_)if(u>=100||u==null){const k=(t||"").replace(/_/g," ").replace(/\b\w/g,C=>C.toUpperCase())||"this plan";_.textContent=`Unlimited sessions with ${k}. Banked credits don't expire.`,_.classList.remove("hidden")}else{let k=null;if(e.current_period_end){const S=new Date(e.current_period_end).getUTCDate(),q=new Date;let I=new Date(Date.UTC(q.getUTCFullYear(),q.getUTCMonth(),S));I<=q&&(I=new Date(Date.UTC(q.getUTCFullYear(),q.getUTCMonth()+1,S))),k=I}else{const C=new Date;k=new Date(Date.UTC(C.getUTCFullYear(),C.getUTCMonth()+1,1))}if(k&&!isNaN(k.getTime())){const C=k.toLocaleDateString(void 0,{month:"long",day:"numeric"});_.textContent=`Monthly allotment resets ${C}. Banked credits don't expire.`,_.classList.remove("hidden")}else _.classList.add("hidden")}let w=!1,$="";if(u&&u<100){const E=d/u;t==="hobby"&&d>=1?(w=!0,$="You're moving fast, Standard would give you 6 recaps/mo."):(t==="standard"||t==="founding_gm")&&E>=.5&&(w=!0,$="Halfway through your cycle: Master adds 10 recaps/mo + AI Prep Suite.")}w?(r.textContent=$+" See Plans \u2192",r.classList.remove("hidden"),r.onclick=()=>{document.getElementById("profile-modal")?.classList.add("hidden"),_showPricingScreen()}):(r.classList.add("hidden"),r.onclick=null)}function _openTopupScreen(){document.getElementById("profile-modal")?.classList.add("hidden"),_showTopupScreen()}document.getElementById("profile-usage-fullsess-row")?.addEventListener("click",_openTopupScreen),document.getElementById("profile-usage-recap-row")?.addEventListener("click",_openTopupScreen),document.getElementById("profile-founding-claim-btn")?.addEventListener("click",async()=>{const e=document.getElementById("profile-founding-claim-btn");e&&(e.disabled=!0,e.textContent="Redirecting\u2026");try{const t=await api("POST","/api/billing/create-checkout-session",{tier:"founding_gm",interval:"monthly"});if(t&&t.url){window.location.href=t.url;return}showToast("Could not start checkout. Please try again.")}catch(t){await _tfCheckoutError(t)}finally{e&&(e.disabled=!1,e.textContent="Claim Founding GM")}});function _tfCopyText(e,t){if(!e)return;const n=()=>showToast(t||"Quote copied, paste it anywhere."),a=()=>showToast("Could not copy."),s=()=>{try{const i=document.createElement("textarea");i.value=e,i.setAttribute("readonly",""),i.style.position="fixed",i.style.opacity="0",document.body.appendChild(i),i.select();const o=document.execCommand("copy");document.body.removeChild(i),o?n():a()}catch{a()}};navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(e).then(n,s):s()}function _chrShareMenu(e,t){document.querySelectorAll(".tf-share-menu").forEach(p=>p.remove());const n=currentCampaign&&currentCampaign.id;if(!n||!e||!t)return;const a=`${location.origin}/c/${n}#chronicle/${n}/${e}`;let s=null,i=null;const o=api("GET",`/api/campaigns/${n}/sessions/${e}/discord-share-text`).then(p=>(s=p&&p.text||null,i=p&&p.plain_text||null,s)).catch(()=>null),r=typeof _isGm=="function"&&_isGm(),c=document.createElement("div");c.className="tf-share-menu",c.innerHTML='<button type="button" class="tf-share-opt tf-share-opt--primary" data-sh="copy">Copy to clipboard</button><button type="button" class="tf-share-opt" data-sh="copy-plain">Copy as plain text</button>'+(r?'<div class="tf-share-sep" aria-hidden="true"></div><button type="button" class="tf-share-opt" data-sh="discord">Post recap to Discord</button><p class="tf-share-hint">Inviting players? Use your invite link in Manage.</p>':""),document.body.appendChild(c);const l=t.getBoundingClientRect();c.style.top=`${Math.max(8,Math.min(l.bottom+6,window.innerHeight-c.offsetHeight-8))}px`,c.style.left=`${Math.max(8,Math.min(l.right-c.offsetWidth,window.innerWidth-c.offsetWidth-8))}px`;const d=()=>c.remove();c.querySelector('[data-sh="copy"]').onclick=async()=>{d();const p=s||await o||a;_tfCopyText(p,"Copied to clipboard, paste it into Discord or anywhere.")},c.querySelector('[data-sh="copy-plain"]').onclick=async()=>{d(),i||await o,_tfCopyText(i||a,"Copied as plain text.")};const u=c.querySelector('[data-sh="discord"]');u&&(u.onclick=()=>{d();const p=document.createElement("button");p.className="btn-publish-discord",p.dataset.id=String(e),p.textContent="Publish to Discord",p.style.cssText="position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none;",document.body.appendChild(p),setTimeout(()=>{try{p.remove()}catch{}},2e4),p.click()}),setTimeout(()=>document.addEventListener("click",function p(m){c.contains(m.target)||(d(),document.removeEventListener("click",p))}),0)}function _tfQuoteEngage(e,t){const n=e.closest&&e.closest("[data-quote-id]"),a=n&&n.dataset.quoteId,s=currentCampaign&&currentCampaign.id;if(!(!a||!s))try{api("POST",`/api/campaigns/${s}/quotes/${a}/engagement`,{kind:t})}catch{}}
1736document.addEventListener("click",e=>{const t=e.target.closest&&e.target.closest(".tf-quote-copy");t&&(e.preventDefault(),e.stopPropagation(),_tfCopyText(t.dataset.copy||""),_tfQuoteEngage(t,"copy"))}),document.addEventListener("click",e=>{const t=e.target.closest&&e.target.closest(".tf-quote-share");if(!t)return;e.preventDefault(),e.stopPropagation();const n=t.closest&&t.closest("[data-quote-id]"),a=n&&n.dataset.quoteId;a&&typeof _openQuoteCardModal=="function"&&(_openQuoteCardModal(a),_tfQuoteEngage(t,"share"))});const _qcCardedQuoteIds=new Set,_qcQotnQuoteIds=new Set;async function _qcLoadCardedSet(){const e=currentCampaign&&currentCampaign.id;if(_qcCardedQuoteIds.clear(),e)try{const t=await api("GET",`/api/campaigns/${e}/quote-cards/carded`);(t&&t.quote_ids||[]).forEach(n=>_qcCardedQuoteIds.add(String(n))),_qcQotnQuoteIds.clear(),(t&&t.qotn_quote_ids||[]).forEach(n=>_qcQotnQuoteIds.add(String(n)))}catch{}_qcApplyCardBadges()}const _QC_CARD_BADGE='<svg class="tf-quote-card-badge" viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="3" width="12" height="16.5" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M7.5 7h6M7.5 10.5h6M7.5 14h3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',_QC_QOTN_BADGE='<svg class="tf-quote-qotn-badge" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6c3.6 0 6.6 0.9 7.9 1.9v6c0 5.9-3.2 10.3-7.9 12.9-4.7-2.6-7.9-7-7.9-12.9v-6C5.4 3.5 8.4 2.6 12 2.6z" fill="none" stroke="currentColor" stroke-width="1.7"/><text x="12" y="15" text-anchor="middle" style="font:700 11px Georgia,serif; fill:currentColor;">&#8220;</text></svg>';function _qcApplyCardBadges(){document.querySelectorAll(".tf-quote-share").forEach(e=>{const t=e.closest&&e.closest("[data-quote-id]"),n=t&&t.dataset.quoteId,a=!!(n&&_qcCardedQuoteIds.has(String(n))),s=!!(n&&_qcQotnQuoteIds.has(String(n)));if(e.classList.toggle("is-carded",a),e.classList.toggle("is-qotn",s),a)e.title="This quote has a card, share opens it"+(s?" \xB7 Quote of the Night winner":""),e.querySelector(".tf-quote-card-badge")||e.insertAdjacentHTML("beforeend",_QC_CARD_BADGE);else{const i=e.querySelector(".tf-quote-card-badge");i&&i.remove(),s&&(e.title="Quote of the Night winner")}if(s)e.querySelector(".tf-quote-qotn-badge")||e.insertAdjacentHTML("beforeend",_QC_QOTN_BADGE);else{const i=e.querySelector(".tf-quote-qotn-badge");i&&i.remove()}})}function _qcTeaserClick(e){if(typeof _openQuoteCardModal!="function"||e.target.closest('.p2-react, .tf-quote-share, .tf-quote-copy, button, a, input, [role="button"], .entity-link'))return;const t=e.target.closest(".p2-quote[data-quote-pc]");if(t&&t._p2q&&t._p2q.id!=null){e.preventDefault(),e.stopPropagation(),_openQuoteCardModal(t._p2q.id);return}const n=e.target.closest(".pc-best-quote[data-entity-quotes]");if(n){const a=n.querySelector("em"),s=(a&&a.textContent||"").trim();let i=[];try{i=JSON.parse(n.dataset.entityQuotes)}catch{i=[]}const o=i.find(r=>(r.quote||"").trim()===s);o&&o.id!=null&&(e.preventDefault(),e.stopPropagation(),_openQuoteCardModal(o.id))}}document.addEventListener("click",_qcTeaserClick);const QC_ACCENT_HEX={light:{ember:"#C26A2E",teal:"#2F7C85",gold:"#A88A47",deep_teal:"#1C4D54",npc_purple:"#7B4FA0"},dark:{ember:"#D4753A",teal:"#4F9AA3",gold:"#C9A867",deep_teal:"#3E8891",npc_purple:"#A67AC8"}},QC_ACCENTS=["ember","teal","gold","deep_teal","npc_purple"],QC_ACCENT_LABELS={ember:"Ember",teal:"Teal",gold:"Gold",deep_teal:"Deep Teal",npc_purple:"NPC Purple"};function _qcAccentBorder(e){return`color-mix(in srgb, ${e} 45%, var(--paper-edge))`}function _qcAccentFrame(e){return`color-mix(in srgb, ${e} 40%, transparent)`}const _QC_LIGHT_TOKENS="--paper:#F3F5F7;--paper-2:#E9ECEF;--paper-3:#FBFCFD;--paper-edge:#D7DCE2;--rule:#C5CCD4;--ink:#15191F;--ink-2:#3A4049;--ink-3:#595E65;--ink-4:#666B72;--teal-2:#1C4D54;--teal-3:#2F7C85;--shear-fill:#1C4D54;--gold:#A88A47;--gold-deep:#7E6628;--ember:#C26A2E;--ember-deep:#8B3320;--card-shadow:0 18px 50px rgba(20,24,30,0.20);",_QC_DARK_TOKENS="--paper:#0E131A;--paper-2:#141A22;--paper-3:#11161D;--paper-edge:#1E2530;--rule:#283039;--ink:#E8EBF0;--ink-2:#B5BBC5;--ink-3:#8C9198;--ink-4:#7C8289;--teal-2:#6FAFB6;--teal-3:#4F9AA3;--shear-fill:#E8EEEC;--gold:#C9A867;--gold-deep:#B8954E;--ember:#D4753A;--ember-deep:#E26242;--card-shadow:0 22px 60px rgba(0,0,0,0.62);",_qcTokens=e=>e==="dark"?_QC_DARK_TOKENS:_QC_LIGHT_TOKENS;function _qcAt(e){return e=(e||"").trim().replace(/^@+/,"").trim(),e?"@"+e:""}function _qcPipeName(e,t,n,a,s){e=(e||"").trim();let i=escHtml(e);const o=e.indexOf(" ");return o>0&&(i=escHtml(e.slice(0,o))+`<span style="color:var(--teal-3); font-weight:400; padding:0 ${s};">|</span>`+escHtml(e.slice(o+1))),`<div style="font-family:'Source Serif 4',Georgia,serif; font-weight:${n}; font-size:${t}; line-height:${a}; color:var(--ink);">${i}</div>`}function _qcLogo(e){return`<span style="display:inline-flex; align-items:center; gap:6px; font-size:${e};"><svg viewBox="0 0 556 330" style="display:block; height:1em; aspect-ratio:556 / 330; width:auto; flex-shrink:0;"><use href="#tf-mark"></use></svg><span style="font-family:'Source Serif 4',Georgia,serif; font-weight:500; letter-spacing:-0.022em; line-height:1; color:var(--ink);">Thread<span style="color:var(--teal-3); font-weight:300; margin:0 2px;">|</span>fall</span></span>`}function _qcTfMark(e){return`<svg viewBox="0 0 556 330" style="display:block; height:${e||"1em"}; aspect-ratio:556 / 330; width:auto; flex-shrink:0;"><use href="#tf-mark"></use></svg>`}const _QC_TF_WORDMARK='Thread<span style="color:var(--teal-3); font-weight:300; margin:0 2px;">|</span>fall';function _qcBrandCol(e,t,n,a,s,i,o){return`<div style="display:flex; flex-direction:column; gap:5px; align-items:${s?"flex-end":"flex-start"}; text-align:${s?"right":"left"}; min-width:0;"><span style="font-family:ui-monospace,Menlo,monospace; font-size:8px; letter-spacing:0.12em; color:var(--ink-3);">${e}</span><span style="display:inline-flex; align-items:center; height:${i||"auto"}; gap:6px; font-size:15px;">${s?"":t}<span style="font-family:'Source Serif 4',Georgia,serif; font-weight:500; letter-spacing:-0.018em; line-height:1; color:var(--ink);">${n}</span>${s?t:""}</span><span style="font-family:ui-monospace,Menlo,monospace; font-size:8.5px; letter-spacing:0.04em; color:${o||"var(--teal-2)"};">${escHtml(a)}</span></div>`}function _qcLinkColors(e){const t=e.theme==="dark",n=e.gm_brand||{};return{hp:t?n.link_color_dark||"#A67AC8":n.link_color||"#7B4FA0",tf:t?"#3D9DAA":"var(--teal-2)"}}function _qcGmGlyph(e,t){return`<img src="${escHtml(e.logo_url)}" alt="" style="height:${t||"1.5em"}; width:auto; display:block; flex-shrink:0;">`}function _qcGmLockup(e,t,n){const a=e.gm_brand,s=_qcGmGlyph(a,t),i=e.theme==="dark"&&a.wordmark_url?`<img src="${escHtml(a.wordmark_url)}" alt="${escHtml(a.name)}" style="height:${n||"1em"}; width:auto; display:block;">`:escHtml(a.name);return{glyph:s,name:i}}function _qcGmEyebrow(e){return"Chronicled by"+(e.gm_name?" GM "+escHtml(e.gm_name):"")}function _qcGmSide(e,t,n,a){const s=e.gm_brand,i=_qcLinkColors(e);if(s){const r=_qcGmLockup(e,t,n);return{glyph:r.glyph,name:r.name,eyebrow:_qcGmEyebrow(s),link:s.link,linkC:i.hp,rowH:t}}const o=(e.gm_brand_logo_url||"").trim();return{glyph:o?_qcGmGlyph({logo_url:o},t):"",name:"GM "+escHtml(e.gm_credit_name||""),eyebrow:"Chronicled by",link:"",linkC:"",rowH:o?t:a}}function _qcCoBrandFoot(e,t){const n=_qcGmSide(e,"1.8em","0.64em","1.5em"),a=_qcLinkColors(e).tf;return`<div style="padding-top:16px; border-top:1px solid ${t}; display:flex; align-items:flex-start; justify-content:space-between; gap:14px;">`+_qcBrandCol(n.eyebrow,n.glyph,n.name,n.link,!1,n.rowH,n.linkC)+_qcBrandCol("Published by",_qcTfMark("1.5em"),_QC_TF_WORDMARK,"thread-fall.com",!0,n.rowH,a)+"</div>"}function _qcCoBrandFootC(e){const t=_qcGmSide(e,"2.04em","0.86em","1.7em"),n=_qcLinkColors(e).tf;return'<div style="position:relative; display:flex; align-items:center; justify-content:center; gap:20px;">'+_qcBrandCol(t.eyebrow,t.glyph,t.name,t.link,!1,t.rowH,t.linkC)+'<span style="width:1px; height:40px; background:var(--rule); flex:none;"></span>'+_qcBrandCol("Published by",_qcTfMark("1.7em"),_QC_TF_WORDMARK,"thread-fall.com",!0,t.rowH,n)+"</div>"}function _qcHasCredit(e){return!!(e&&(e.gm_brand||e.gm_credit_name))}function _qcRibbon(e,t,n,a,s,i){return`<span style="display:inline-block; background:${t}; color:#FBFCFD; font-family:'Inter',sans-serif; font-weight:700; font-size:${n}; letter-spacing:${s}; text-transform:uppercase; padding:${a}; white-space:nowrap; clip-path:polygon(0 0,100% 0,calc(100% - ${i}) 50%,100% 100%,0 100%);">${escHtml(e)}</span>`}const _QC_WM_CORNER='<svg viewBox="0 0 556 290" style="position:absolute; right:-40px; bottom:34px; width:380px; height:auto;
1736 opacity:0.05; color:var(--ink); pointer-events:none;"><polygon points="0,289 33,269 77,232 89,230 127,187 143,190 189,125 195,126 203,116 235,66 239,64 248,71 275,20 290,10 293,0 316,75 316,290 0,290" fill="currentColor"></polygon><polygon points="334,68 352,84 375,119 387,121 414,154 438,170 504,245 556,288 556,290 334,290" fill="currentColor"></polygon></svg>',_QC_WM_CENTER='<svg viewBox="0 0 556 290" style="position:absolute; left:50%; top:42%; transform:translate(-50%,-50%); width:520px; height:auto; opacity:0.05; pointer-events:none;"><polygon points="0,289 33,269 77,232 89,230 127,187 143,190 189,125 195,126 203,116 235,66 239,64 248,71 275,20 290,10 293,0 316,75 316,290 0,290" fill="currentColor"></polygon><polygon points="334,68 352,84 375,119 387,121 414,154 438,170 504,245 556,288 556,290 334,290" fill="currentColor" opacity="0.5"></polygon></svg>';function _qcArtImg(e){const t=e.crop_x!=null?e.crop_x:50,n=e.crop_y!=null?e.crop_y:50,a=e.crop_zoom!=null?e.crop_zoom:1;return`<img src="${encodeURI(e.image_url)}" alt="" style="width:100%; height:100%; object-fit:cover; object-position:${t}% ${n}%; transform:scale(${a}); transform-origin:${t}% ${n}%; display:block;">`}const _QC_QOTN_STAR='<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="flex:none;"><path d="M12 2.5l2.8 6 6.5.7-4.9 4.4 1.4 6.4L12 16.7 6.2 20l1.4-6.4L2.7 9.2l6.5-.7z"/></svg>';function _qcQotnShadow(){return'<div style="position:absolute; left:36px; bottom:0; width:460px; height:575px; overflow:hidden; z-index:5; pointer-events:none;"><div style="position:absolute; top:-25px; right:-29px; transform:rotate(4deg); filter:blur(4px); opacity:0.25;"><svg width="180" height="211" viewBox="0 0 106 124" xmlns="http://www.w3.org/2000/svg"><path d="M53,4 C74,4 92,10 98,16 L98,44 C98,78 82,104 53,120 C24,104 8,78 8,44 L8,16 C14,10 32,4 53,4 Z" fill="#000"/></svg></div></div>'}function _qcQotnChip(){return`<div style="position:absolute; top:7px; right:7px; z-index:6; transform:rotate(4deg);"><svg width="180" height="211" viewBox="0 0 106 124" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="tfQotnGold" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#B8954E"/><stop offset="50%" stop-color="#C9A867"/><stop offset="100%" stop-color="#8F7238"/></linearGradient></defs><path d="M53,4 C74,4 92,10 98,16 L98,44 C98,78 82,104 53,120 C24,104 8,78 8,44 L8,16 C14,10 32,4 53,4 Z" fill="url(#tfQotnGold)"/><path d="M53,9 C71,9 86,14 92,19 L92,44 C92,74 79,97 53,112 C27,97 14,74 14,44 L14,19 C20,14 35,9 53,9 Z" fill="#1B222B" stroke="#0E141B" stroke-width="1"/><circle cx="20" cy="18" r="2.5" fill="#C9A867" stroke="#7E6628" stroke-width="0.6"/><circle cx="86" cy="18" r="2.5" fill="#C9A867" stroke="#7E6628" stroke-width="0.6"/><circle cx="14" cy="68" r="2.5" fill="#C9A867" stroke="#7E6628" stroke-width="0.6"/><circle cx="92" cy="68" r="2.5" fill="#C9A867" stroke="#7E6628" stroke-width="0.6"/><circle cx="53" cy="42" r="20" fill="url(#tfQotnGold)" stroke="#7E6628" stroke-width="1.5"/><circle cx="53" cy="42" r="16" fill="none" stroke="#1B222B" stroke-width="1"/><text x="53" y="71" text-anchor="middle" style="font-family:'Source Serif 4',Georgia,serif; font-weight:700; font-size:56px; fill:#1B222B;">&#8220;</text><text x="53" y="78" text-anchor="middle" style="font-family:'Inter',sans-serif; font-weight:700; font-size:9.5px; letter-spacing:0.14em; fill:#C9A867;">QUOTE</text><text x="53" y="88" text-anchor="middle" style="font-family:'Inter',sans-serif; font-weight:600; font-size:6.5px; letter-spacing:0.14em; fill:#C9A867; opacity:0.72;">OF THE</text><text x="53" y="99" text-anchor="middle" style="font-family:'Inter',sans-serif; font-weight:700; font-size:9.5px; letter-spacing:0.14em; fill:#C9A867;">NIGHT</text></svg></div>`}function _qcVariantA(e){const t=e.accent_hex,n=_qcAccentBorder(t),a=e.show_portrait&&e.image_url?`<div style="position:relative; flex:none; width:86px; height:104px; overflow:hidden; border:1px solid var(--paper-edge); box-shadow:0 2px 8px rgba(20,24,30,0.18);">${_qcArtImg(e)}</div>
1736`:"",s=_qcAt(e.handle),i=s?`<div style="font-family:ui-monospace,Menlo,monospace; font-size:11px; letter-spacing:0.06em; color:var(--ink-3); margin-top:7px;">played by <span style="color:var(--teal-2);">${escHtml(s)}</span></div>`:"",o=e.context_line?`<div style="font-family:'Source Serif 4',Georgia,serif; font-style:italic; font-size:16px; line-height:1.45; color:var(--ink-2); margin-top:10px;">${escHtml(e.context_line)}</div>`:"",r=[e.session_title,e.campaign_name].filter(Boolean).map(escHtml).join("<br>");return`<div id="tf-card" style="position:relative; width:460px; height:575px; background:var(--paper-3); border:1px solid ${n}; box-shadow:var(--card-shadow); overflow:hidden; padding:38px 38px 26px; display:flex; flex-direction:column; font-family:'Inter',sans-serif;"><div style="position:absolute; top:0; left:0; width:0; height:0; border-top:34px solid ${t}; border-right:34px solid transparent; opacity:0.92;"></div>`+_QC_WM_CORNER+`<div style="position:relative; display:flex; justify-content:space-between; align-items:flex-start; gap:14px;">${_qcRibbon(e.qotn_winner?"Quote of the Night":"Memorable Quote",t,"11px","7px 22px 7px 14px","0.18em","9px")}${a}</div><div style="position:relative; margin-top:22px;"><div style="font-family:'Source Serif 4',Georgia,serif; font-weight:600; font-size:82px; line-height:0.5; color:${t}; opacity:0.28; height:38px;">&#8220;</div><p style="font-family:'Source Serif 4',Georgia,serif; font-style:italic; font-weight:400; font-size:27px; line-height:1.36; letter-spacing:0.002em; color:var(--ink); margin:6px 0 0; text-wrap:pretty;">${escHtml(e.quote)}</p></div><div style="margin-top:20px;">${_qcPipeName(e.character_name,"25px",600,"1.1","7px")}${i}${o}</div>`+(_qcHasCredit(e)?`<div style="margin-top:auto;">${r?`<div style="padding-bottom:12px; font-family:ui-monospace,Menlo,monospace; font-size:9px; letter-spacing:0.1em; text-transform:uppercase; color:var(--ink-3); text-align:center;">${r.replace(/<br>/g," \xB7 ")}</div>`:""}${_qcCoBrandFoot(e,"var(--paper-edge)")}</div>`:`<div style="margin-top:auto; padding-top:18px; border-top:1px solid var(--paper-edge); display:flex; align-items:center; justify-content:space-between; gap:12px;"><div style="display:flex; flex-direction:column; gap:8px;">${_qcLogo("21px")}<span style="font-family:ui-monospace,Menlo,monospace; font-size:8.5px; letter-spacing:0.08em; color:var(--ink-3);">made live at thread-fall.com</span></div><span style="font-family:ui-monospace,Menlo,monospace; font-size:9px; letter-spacing:0.1em; text-transform:uppercase; color:var(--ink-3); text-align:right;">${r}</span></div>`)+"</div>"}function _qcVariantB(e){const t=e.accent_hex,n=_qcAccentBorder(t),a=e.show_portrait&&e.image_url?`<div style="position:relative; height:236px; flex:none; overflow:hidden; -webkit-mask-image:linear-gradient(180deg,#000 78%,transparent 100%); mask-image:linear-gradient(180deg,#000 78%,transparent 100%);">${_qcArtImg(e)}</div>`:`<div style="height:74px; flex:none; background:linear-gradient(90deg,${t},color-mix(in srgb,${t} 60%,#000));"></div>`,s=e.eyebrow?`<div style="font-family:'Inter',sans-serif; font-weight:700; font-size:10px; letter-spacing:0.16em; text-transform:uppercase; color:var(--gold-deep);">${escHtml(e.eyebrow)}</div>`:"",i=e.session_title?`<div style="margin-top:14px;">${_qcRibbon(e.session_title,t,"10px","6px 18px 6px 12px","0.16em","8px")}</div>`:"",o=e.context_line?`<div style="font-family:'Source Serif 4',Georgia,serif; font-style:italic; font-size:16px; color:var(--ink-2); margin-top:12px; padding-left:24px;">${escHtml(e.context_line)}</div>`:"",r=_qcAt(e.handle),c=r?`<span style="font-family:ui-monospace,Menlo,monospace; font-size:9px; letter-spacing:0.06em; color:var(--ink-3);">${escHtml(r)}</span>`:"";return`<div id="tf-card" style="position:relative; width:460px; height:575px; background:var(--paper-3); border:1px solid ${n}; box-shadow:var(--card-shadow); overflow:hidden; display:flex; flex-direction:column; font-family:'Inter',sans-serif;"><div style="position:absolute; inset:9px; border:1px solid ${_qcAccentFrame(t)}; pointer-events:none; z-index:3;"></div>`+a+'<div style="position:relative;
1736 padding:0 34px 26px; margin-top:-12px; display:flex; flex-direction:column; flex:1; z-index:2;">'+s+_qcPipeName(e.character_name,"42px",700,"0.98","8px")+i+(e.event_text?`<p style="font-family:'Source Serif 4',Georgia,serif; font-size:21px; line-height:1.45; color:var(--ink); margin:20px 0 0; text-wrap:pretty;">${escHtml(e.event_text)}</p>`:`<div style="margin-top:20px; display:flex; gap:12px;"><div style="flex:none; font-family:'Source Serif 4',Georgia,serif; font-weight:600; font-size:46px; line-height:0.8; color:var(--teal-3); opacity:0.32;">&#8220;</div><p style="font-family:'Source Serif 4',Georgia,serif; font-style:italic; font-size:22px; line-height:1.4; color:var(--ink); margin:2px 0 0; text-wrap:pretty;">${escHtml(e.quote)}</p></div>`)+o+(_qcHasCredit(e)?`<div style="margin-top:auto;">${r?`<div style="padding-bottom:10px; text-align:right; font-family:ui-monospace,Menlo,monospace; font-size:9px; letter-spacing:0.06em; color:var(--ink-3);">played by <span style="color:var(--teal-2);">${escHtml(r)}</span></div>`:""}${_qcCoBrandFoot(e,"var(--rule)")}</div>`:`<div style="margin-top:auto; padding-top:14px; border-top:1px solid var(--rule); display:flex; align-items:center; justify-content:space-between;">${_qcLogo("21px")}<div style="display:flex; flex-direction:column; align-items:flex-end; gap:3px;"><span style="font-family:ui-monospace,Menlo,monospace; font-size:9.5px; letter-spacing:0.06em; color:var(--ink-2);">made live at <span style="color:var(--gold-deep); font-weight:700;">thread-fall.com</span></span>${c}</div></div>`)+"</div></div>"}function _qcVariantC(e){const t=e.accent_hex,n=_qcAccentBorder(t),a=[e.campaign_name,e.session_title].filter(Boolean).map(escHtml).join(" \xB7 "),s=_qcAt(e.handle),i=[s?"played by "+escHtml(s):"",e.context_line?escHtml(e.context_line):""].filter(Boolean).join(" \xB7 ");return`<div id="tf-card" style="position:relative; width:460px; height:575px; background:var(--paper); border:1px solid ${n}; box-shadow:var(--card-shadow); overflow:hidden; padding:42px 44px 32px; display:flex; flex-direction:column; justify-content:space-between; align-items:center; text-align:center; font-family:'Inter',sans-serif; color:var(--ink);">`+_QC_WM_CENTER+`<div style="position:relative; display:flex; flex-direction:column; align-items:center;">${e.qotn_winner?`<div style="display:flex; align-items:center; gap:5px; font-family:'Inter',sans-serif; font-weight:700; font-size:9px; letter-spacing:0.2em; text-transform:uppercase; color:var(--gold-deep); margin-bottom:12px;">${_QC_QOTN_STAR}Quote of the Night</div>`:""}<div style="font-family:ui-monospace,Menlo,monospace; font-size:10px; letter-spacing:0.2em; text-transform:uppercase; color:var(--ink-3);">${a}</div><div style="width:38px; height:1px; background:var(--rule); margin:16px 0 0;"></div></div><div style="position:relative; display:flex; flex-direction:column; align-items:center;"><div style="font-family:'Source Serif 4',Georgia,serif; font-weight:600; font-size:78px; line-height:0.4; color:${t}; opacity:0.3; height:40px;">&#8220;</div><p style="font-family:'Source Serif 4',Georgia,serif; font-style:italic; font-weight:400; font-size:26px; line-height:1.42; color:var(--ink); margin:16px 6px 0; text-wrap:balance;">${escHtml(e.quote)}</p><div style="width:44px; height:1px; background:var(--rule); margin-top:24px;"></div><div style="font-family:'Source Serif 4',Georgia,serif; font-weight:600; font-size:18px; letter-spacing:0.04em; color:var(--ink); margin-top:16px; font-variant:small-caps;">${escHtml(e.character_name)}</div><div style="font-family:ui-monospace,Menlo,monospace; font-size:10px; letter-spacing:0.08em; color:var(--ink-3); margin-top:6px;">${i}</div></div>`+(_qcHasCredit(e)?_qcCoBrandFootC(e):`<div style="position:relative; display:flex; flex-direction:column; align-items:center;">${_qcLogo("32px")}<span style="font-family:ui-monospace,Menlo,monospace; font-size:9px; letter-spacing:0.14em; text-transform:uppercase; color:var(--ink-3); margin-top:13px;">made live at thread-fall.com</span></div>`)+"</div>"}let _qcLegendSeq=0;function _qcLegendFrame(e,t){const n=e.replace("box-shadow:var(--card-shadow);","box-shadow:none;"),a=String(t).split("__UID__").join("q"+ ++_qcLegendSeq);return`<div id="tf-frame" style="position:relative; width:532px; height:611px; background:transparent;"><div style="position:absolute; left:36px; bottom:0;">${n}</div><div style="position:absolute; left:36px; bottom:0; width:460px; height:575px; overflow:hidden; z-index:5; pointer-events:none;"><div style="position:absolute; top:-12px; right:-26px; filter:blur(4px); opacity:0.25;"><svg width="180" height="180" viewBox="0 0 120 120"><circle cx="60" cy="64" r="58" fill="#000"></circle></svg></div></div><div style="position:absolute; top:0; right:-2px; z-index:6; transform:rotate(4deg);">${a}
1736</div></div>`}function _buildQuoteCardHtml(e){const n=({codex_plate:_qcVariantA,trading_card:_qcVariantB,broadside:_qcVariantC}[e.variant]||_qcVariantB)(e);return e.legend_pin_svg&&(e.variant||"trading_card")==="trading_card"?_qcLegendFrame(n,e.legend_pin_svg):e.qotn_winner&&(e.variant||"trading_card")==="trading_card"?`<div id="tf-frame" style="position:relative; width:532px; height:611px; background:transparent;"><div style="position:absolute; left:36px; bottom:0;">${n.replace("box-shadow:var(--card-shadow);","box-shadow:none;")}</div>${_qcQotnShadow()}${_qcQotnChip()}</div>`:n}function _qcRenderInline(e,t){if(!e)return"";t=t||.5;const n=!!((e.qotn_winner||e.legend_pin_svg)&&(e.variant||"trading_card")==="trading_card"),a=Math.round((n?532:460)*t),s=Math.round((n?611:575)*t),i=_buildQuoteCardHtml(e).replace('id="tf-card"','class="qc-card-el"').replace('id="tf-frame"','class="qc-frame-el"');return`<div class="qc-card-inline" style="width:${a}px; height:${s}px;"><div class="qc-card-inline-scale" style="${_qcTokens(e.theme)} transform:scale(${t});">${i}</div></div>`}document.addEventListener("click",e=>{const t=e.target.closest&&e.target.closest("[data-qc-open]");if(!t)return;const n=t.dataset.qcOpen;n&&typeof _openQuoteCardModal=="function"&&(e.preventDefault(),_openQuoteCardModal(n))});function _qcShareBtn(e){return`<button type="button" class="tf-quote-share" data-copy="${escHtml(e)}" title="Share this quote" aria-label="Share quote"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.3 13.4l7.4 4.2M15.7 6.4l-7.4 4.2"/></svg></button>`}function _qcComposeSpec(e,t){const n=t.overrides||{},a=t.theme==="dark"?"dark":"light",s=t.image_url||t.image_source==="portrait"&&e.portrait_url||null,i=!!(t.show_portrait&&s);return{variant:t.variant,accent:t.accent,theme:a,accent_hex:(QC_ACCENT_HEX[a]||QC_ACCENT_HEX.light)[t.accent]||QC_ACCENT_HEX[a].ember,show_portrait:i,image_url:i?s:null,crop_x:t.crop_offset_x!=null?t.crop_offset_x:50,crop_y:t.crop_offset_y!=null?t.crop_offset_y:0,crop_zoom:t.crop_zoom!=null?t.crop_zoom:1,quote:n.quote||e.quote||"",character_name:e.character_name||"",handle:n.handle||e.handle||"",context_line:t.show_context&&(n.context||e.context)||"",session_title:t.show_session_title&&e.session_title||"",campaign_name:e.campaign_name||"",eyebrow:n.eyebrow||e.eyebrow||"",link:"thread-fall.com",gm_brand:e.gm_brand||null,gm_credit_name:e.gm_credit_name||null,gm_brand_logo_url:e.gm_brand_logo_url||null,qotn_winner:!!e.qotn_winner}}let _qcState=null;function _qcRenderPreview(){const e=document.getElementById("qc-stage");if(!e||!_qcState)return;e.style.cssText=_qcTokens(_qcState.controls.theme),e.innerHTML=_buildQuoteCardHtml(_qcComposeSpec(_qcState.ctx,_qcState.controls));const t=document.getElementById("quote-card-modal");if(t){const n=t.querySelector(".qc-result");n&&n.classList.add("hidden");const a=t.querySelector(".qc-status");a&&!a.classList.contains("error")&&(a.textContent="")}}async function _openQuoteCardModal(e){const t=currentCampaign&&currentCampaign.id;if(!t||!e)return;const n=document.getElementById("quote-card-modal");if(!n)return;let a;try{a=await api("GET",`/api/campaigns/${t}/quotes/${e}/card-context`)}catch{showToast("Could not load this quote.");return}_qcState={quoteId:e,ctx:a,card:null,controls:{variant:"trading_card",accent:a.portrait_accent||"ember",theme:a.qotn_winner||document.documentElement.getAttribute("data-theme")==="dark"?"dark":"light",show_portrait:!0,show_context:!0,show_session_title:!!a.session_title,image_source:"portrait",image_url:null,crop_offset_x:a.focal_x!=null?a.focal_x:50,crop_offset_y:a.focal_y!=null?Math.max(0,Math.min(100,a.focal_y-10)):0,crop_zoom:a.focal_x!=null&&a.focal_y!=null?1.2:1,upload_status:null,overrides:{quote:"",handle:"",context:"",eyebrow:""}}};try{const s=await api("GET",`/api/campaigns/${t}/quote-cards/by-quote/${e}`);if(s&&s.spec){const i=s.spec,o=s.overrides||{},r=_qcState.controls;if(r.variant=s.variant||r.variant,r.accent=s.accent||r.accent,r.theme=i.theme||r.theme,r.image_source=s.image_source||r.image_source,r.image_url=s.image_url||null,r.crop_offset_x=i.crop_x!=null?i.crop_x:50,r.crop_offset_y=i.crop_y!=null?i.crop_y:0,r.crop_zoom=i.crop_zoom!=null?i.crop_zoom:1,r.show_portrait=!!i.show_portrait,r.show_context=!!i.context_line,r.show_session_title=!!i.session_title,r.overrides={quote:o.quote||"",handle:o.handle||"",context:o.context||"",eyebrow:o.eyebrow||""},s.image_source==="upload"&&(r.upload_status="pending"),s.status==="done"&&s.png_url){const c=_qcState.ctx,l=[c.character_name,c.campaign_name,c.session_title,r.image_source==="portrait"&&c.portrait_url||""].join("�"),d=[i.character_name,i.campaign_name,i.session_title,r.image_source==="portrait"&&i.image_url||""].join("�");l===d&&(_qcState.card=s,_qcState.renderedKey=JSON.stringify(_qcComposeSpec(_qcState.ctx,r)))}}}catch{}
1736if(n.querySelector(".qc-modal-body").innerHTML=_qcControlsHtml(_qcState),n.classList.remove("hidden"),_qcRenderPreview(),_qcWireControls(n),_qcState.card&&_qcState.card.png_url){const s=n.querySelector(".qc-result");s&&s.classList.remove("hidden");const i=n.querySelector(".qc-status");i&&(i.textContent="This quote already has a card, share it, or tweak to re-render.");const o=n.querySelector(".qc-download");o&&(o.onclick=()=>{_qcDownload(_qcState.card.png_url),_qcEngage("download")});const r=n.querySelector(".qc-share");r&&(r.onclick=c=>_qcCardShareMenu(_qcState.card,c.currentTarget))}}async function _qcRefreshPendingArt(){const e=document.getElementById("qc-pending-art-mount"),t=currentCampaign&&currentCampaign.id;if(!e)return;if(!t||!_isGm()){e.innerHTML="";return}let n=[];try{n=await api("GET",`/api/campaigns/${t}/quote-card-art/pending`)}catch{n=[]}if(!Array.isArray(n)||!n.length){e.innerHTML="";return}const a=n.map(s=>{const i=s.entity_name?escHtml(s.entity_name):"<em>unattributed</em>",o=s.uploader_name?` \xB7 by ${escHtml(s.uploader_name)}`:"",r=s.entity_id?`<button type="button" class="btn-ghost btn-xs qc-art-approve-codex" data-art-id="${s.id}">Approve + add to ${escHtml(s.entity_name||"codex")}</button>`:"",c=typeof thumbUrl=="function"?thumbUrl(s.image_url):s.image_url;return`
1737      <div class="pending-reveal-card qc-art-card" data-art-id="${s.id}">
1738        <img class="pending-reveal-thumb" src="${escHtml(c)}" alt="">
1739        <div class="pending-reveal-body">
1740          <div class="pending-reveal-type">Quote-card art</div>
1741          <div class="pending-reveal-name">${i}${o}</div>
1742        </div>
1743        <div class="qc-art-actions">
1744          <button type="button" class="btn-new btn-xs qc-art-approve" data-art-id="${s.id}">Approve</button>
1745          ${r}
1746          <button type="button" class="btn-ghost btn-xs qc-art-reject" data-art-id="${s.id}">Reject</button>
1747        </div>
1748      </div>`}).join("");e.innerHTML=`
1749    <div class="pending-reveal-section qc-pending-art">
1750      <div class="pending-reveal-hdr">
1751        <span class="form-section-label pending-reveal-eyebrow">Quote-card art to review: ${n.length} pending</span>
1752      </div>
1753      <div class="pending-reveal-grid">${a}</div>
1754    </div>`,_qcWirePendingArt(e)}function _qcWirePendingArt(e){const t=currentCampaign&&currentCampaign.id,n=async(a,s,i)=>{try{await api("POST",`/api/campaigns/${t}/quote-card-art/${a}/${s}`,i||{})}catch(o){showToast("Could not update: "+(o&&o.message||"try again"));return}_qcRefreshPendingArt()};e.querySelectorAll(".qc-art-approve").forEach(a=>a.onclick=()=>n(a.dataset.artId,"approve",{add_to_codex:!1})),e.querySelectorAll(".qc-art-approve-codex").forEach(a=>a.onclick=()=>n(a.dataset.artId,"approve",{add_to_codex:!0})),e.querySelectorAll(".qc-art-reject").forEach(a=>a.onclick=()=>n(a.dataset.artId,"reject"))}function _qcVariantOpts(){return[{k:"trading_card",n:"Trading Card",d:"collectible"},{k:"codex_plate",n:"Codex Plate",d:"faithful"},{k:"broadside",n:"Broadside",d:"literary"}]}function _qcControlsHtml(e){const t=e.controls,n=e.ctx,a=_qcVariantOpts().map(c=>`<button type="button" class="qc-variant-btn${t.variant===c.k?" is-active":""}" data-variant="${c.k}"><span class="qc-vb-name">${c.n}</span><span class="qc-vb-desc">${c.d}</span></button>`).join(""),s=QC_ACCENTS.map(c=>`<button type="button" class="qc-accent-btn${t.accent===c?" is-active":""}" data-accent="${c}" style="--qc-swatch:${QC_ACCENT_HEX.light[c]}" title="${QC_ACCENT_LABELS[c]}" aria-label="${QC_ACCENT_LABELS[c]} accent"></button>`).join(""),i=(c,l,d)=>`<label class="pref-toggle"><input type="checkbox" data-toggle="${c}"${d?" checked":""}><span class="pref-toggle-track"><span class="pref-toggle-thumb"></span></span><span class="pref-toggle-label">${l}</span></label>`,o=`<label class="pref-toggle"><input type="checkbox" data-theme-toggle${t.theme==="dark"?" checked":""}><span class="pref-toggle-track"><span class="pref-toggle-thumb"></span></span><span class="pref-toggle-label">${t.theme==="dark"?"Dark":"Light"}</span></label>`,r=(c,l,d,u,p)=>`<label class="qc-field"><span class="form-section-label">${l}</span>${p?`<textarea class="qc-input" data-edit="${c}" rows="2" placeholder="${escHtml(u)}">${escHtml(d)}</textarea>`:`<input class="qc-input" data-edit="${c}
1754" type="text" value="${escHtml(d)}" placeholder="${escHtml(u)}">`}</label>`;return`
1755    <div class="qc-layout">
1756      <div class="qc-preview-pane">
1757        <div class="qc-preview-frame"><div id="qc-stage" class="qc-stage" style="${_qcTokens(t.theme)}"></div><div class="qc-drag-hint"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 9l-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20"/></svg>Pinch/scroll to zoom, drag to move</div></div>
1758        <div class="qc-actions">
1759          <button type="button" class="btn-new btn-share qc-generate">Generate &amp; share</button>
1760          <div class="qc-result hidden">
1761            <button type="button" class="btn-new btn-share qc-share">\u2197 Share</button>
1762            <button type="button" class="btn-ghost btn-xs qc-download">Download</button>
1763          </div>
1764          <div class="qc-status placeholder"></div>
1765        </div>
1766        <div class="qc-discord-cta">Share to the community \u2192 <a href="https://discord.gg/Vyrvaq7QKu" target="_blank" rel="noopener noreferrer">Join the Threadfall Discord \u2197</a></div>
1767      </div>
1768      <div class="qc-controls">
1769        <div class="qc-group"><span class="form-section-label">Style</span><div class="qc-variant-row">${a}</div></div>
1770        <div class="qc-group"><span class="form-section-label">Accent &amp; Theme</span><div class="qc-at-row"><div class="qc-accent-band">${s}</div>${o}${i("show_session_title","Session title",t.show_session_title)}</div></div>
1771        <div class="qc-group">
1772          <span class="form-section-label">Image</span>
1773          <div class="qc-img-row">
1774            <button type="button" class="btn-ghost btn-xs qc-img-btn${t.image_source==="portrait"?" is-active":""}" data-img="portrait">Default portrait</button>
1775            <button type="button" class="btn-ghost btn-xs qc-img-btn" data-img="library">Pick from library</button>
1776            <button type="button" class="btn-ghost btn-xs qc-img-btn${t.image_source==="upload"?" is-active":""}" data-img="upload">Upload your own</button>
1777          </div>
1778          <input type="file" accept="image/jpeg,image/png,image/gif,image/webp" class="qc-upload-input hidden">
1779          <div class="qc-upload-consent hidden"></div>
1780          <div class="qc-upload-note form-helper-text${t.upload_status==="pending"?"":" hidden"}">\u23F3 Pending GM approval; you can still download &amp; share your own card; it won't hit the community wall or gallery until approved.</div>
1781        </div>
1782        <div class="qc-group">
1783          <span class="form-section-label">Edit Card Text</span>
1784          ${r("quote","Quote",t.overrides.quote||n.quote,n.quote,!0)}
1785          ${r("context","Context",t.overrides.context,n.context||"short scene context",!1)}
1786          ${r("handle","Handle",t.overrides.handle,n.handle||"your name",!1)}
1787        </div>
1788      </div>
1789    </div>`}function _qcWireControls(e){const t=e.querySelector(".qc-modal-body"),n=_qcState;t.querySelectorAll("[data-variant]").forEach(i=>i.onclick=()=>{n.controls.variant=i.dataset.variant,t.querySelectorAll("[data-variant]").forEach(o=>o.classList.toggle("is-active",o===i)),_qcRenderPreview()}),t.querySelectorAll("[data-accent]").forEach(i=>i.onclick=()=>{n.controls.accent=i.dataset.accent,t.querySelectorAll("[data-accent]").forEach(o=>o.classList.toggle("is-active",o===i)),_qcRenderPreview()});const a=t.querySelector("[data-theme-toggle]");a&&(a.onchange=()=>{n.controls.theme=a.checked?"dark":"light";const i=a.closest(".pref-toggle").querySelector(".pref-toggle-label");i&&(i.textContent=a.checked?"Dark":"Light"),_qcRenderPreview()}),t.querySelectorAll("[data-toggle]").forEach(i=>i.onchange=()=>{n.controls[i.dataset.toggle]=i.checked,_qcRenderPreview()}),t.querySelectorAll("[data-edit]").forEach(i=>i.oninput=()=>{n.controls.overrides[i.dataset.edit]=i.value,_qcRenderPreview()}),t.querySelectorAll("[data-img]").forEach(i=>i.onclick=()=>{if(i.dataset.img==="portrait"){n.controls.image_source="portrait",n.controls.image_url=null;const o=n.ctx&&n.ctx.focal_x!=null&&n.ctx.focal_y!=null;n.controls.crop_offset_x=o?n.ctx.focal_x:50,n.controls.crop_offset_y=o?Math.max(0,Math.min(100,n.ctx.focal_y-10)):0,n.controls.crop_zoom=o?1.2:1,t.querySelectorAll("[data-img]").forEach(r=>r.classList.toggle("is-active",r.dataset.img==="portrait")),_qcRenderPreview()}
1789else if(i.dataset.img==="library"&&typeof _campLibOpen=="function")_campLibOpen({autoAssign:!1,title:"Pick card art",includeEntityArt:!0,subtitle:"Pick from the cast\u2019s codex art, or a campaign library image.",onPick:o=>{n.controls.image_source="library",n.controls.image_url=o.url,n.controls.show_portrait=!0,n.controls.crop_offset_x=50,n.controls.crop_offset_y=0,n.controls.crop_zoom=1;const r=t.querySelector('[data-toggle="show_portrait"]');r&&(r.checked=!0),t.querySelectorAll("[data-img]").forEach(c=>c.classList.toggle("is-active",c.dataset.img==="library")),_qcRenderPreview()}});else if(i.dataset.img==="upload"){const o=t.querySelector(".qc-upload-input");o&&o.click()}});const s=t.querySelector(".qc-upload-input");s&&(s.onchange=()=>{const i=s.files&&s.files[0];i&&_qcShowUploadConsent(e,i),s.value=""}),_qcInitCropDrag(e),t.querySelector(".qc-generate").onclick=()=>_qcGenerate(e)}function _qcInitCropDrag(e){const t=e.querySelector("#qc-stage");!t||t._qcCropBound||(t._qcCropBound=!0,t.addEventListener("pointerdown",n=>{const a=n.target.closest&&n.target.closest("#tf-card img"),s=_qcState;if(!a||!s)return;n.preventDefault();const i=t.closest(".qc-preview-frame");i&&i.classList.add("qc-dragging");const o=a.parentElement.getBoundingClientRect(),r=n.clientX,c=n.clientY,l=s.controls.crop_offset_x,d=s.controls.crop_offset_y;try{a.setPointerCapture(n.pointerId)}catch{}const u=m=>{const f=(m.clientX-r)/Math.max(1,o.width)*100,g=(m.clientY-c)/Math.max(1,o.height)*100;s.controls.crop_offset_x=Math.max(0,Math.min(100,l-f)),s.controls.crop_offset_y=Math.max(0,Math.min(100,d-g)),a.style.objectPosition=s.controls.crop_offset_x+"% "+s.controls.crop_offset_y+"%"},p=()=>{a.removeEventListener("pointermove",u),a.removeEventListener("pointerup",p),i&&i.classList.remove("qc-dragging"),_qcRenderPreview()};a.addEventListener("pointermove",u),a.addEventListener("pointerup",p)}),t.addEventListener("wheel",n=>{if(!(n.target.closest&&n.target.closest("#tf-card img"))||!_qcState)return;n.preventDefault();const s=Math.max(1,Math.min(3,(_qcState.controls.crop_zoom||1)-n.deltaY*.0015));_qcState.controls.crop_zoom=Math.round(s*100)/100,_qcRenderPreview()},{passive:!1}))}function _qcShowUploadConsent(e,t){const n=e.querySelector(".qc-modal-body"),a=_qcState,s=n.querySelector(".qc-upload-consent");if(!s)return;const i=typeof allEntities<"u"&&Array.isArray(allEntities)?allEntities.filter(d=>d&&d.name&&["pc","npc","faction","deity"].indexOf(d.type)!==-1):[],o=a.ctx.speaker_entity_id,r=['<option value="">(who is this art of?)</option>'].concat(i.map(d=>`<option value="${d.id}"${d.id===o?" selected":""}>${escHtml(d.name)}</option>`)).join("");s.innerHTML=`
1790    <div class="qc-upload-panel">
1791      <div class="form-helper-text">${escHtml(t.name)} \xB7 attribute it + confirm rights, then it goes to your GM for approval.</div>
1792      <label class="qc-field"><span class="form-section-label">Who is this art of?</span>
1793        <select class="qc-input qc-upload-entity">${r}</select></label>
1794      <label class="qc-consent"><input type="checkbox" class="qc-upload-consent-chk">
1795        <span>I have the right to use this image and confirm it doesn't infringe anyone's copyright, trademark, likeness, or privacy. I'm responsible for it.</span></label>
1796      <div class="qc-upload-actions">
1797        <button type="button" class="btn-new btn-xs qc-upload-go" disabled>Upload</button>
1798        <button type="button" class="btn-ghost btn-xs qc-upload-cancel">Cancel</button>
1799        <span class="qc-upload-status placeholder"></span>
1800      </div>
1801    </div>`,s.classList.remove("hidden");const c=s.querySelector(".qc-upload-consent-chk"),l=s.querySelector(".qc-upload-go");c.onchange=()=>{l.disabled=!c.checked},s.querySelector(".qc-upload-cancel").onclick=()=>{s.classList.add("hidden"),s.innerHTML=""},l.onclick=()=>_qcDoUpload(e,t,s)}async function _qcDoUpload(e,t,n){const a=currentCampaign&&currentCampaign.id,s=_qcState,i=n.querySelector(".qc-upload-status"),o=n.querySelector(".qc-upload-go"),r=n.querySelector(".qc-upload-entity");i.textContent="Uploading\u2026",i.classList.remove("error"),o.disabled=!0;let c;try{c=await _tfUploadDirect(t,{paramsPath:`/api/campaigns/${a}/upload-params`,kind:"quote",endpoint:`/api/campaigns/${a}/quote-card-art`,fields:{consent:"true",entity_id:r&&r.value||null}})}catch(p){i.textContent=p&&p.message||"Upload failed, try again.",i.classList.add("error"),o.disabled=!1;return}s.controls.image_source="upload",s.controls.image_url=c.url,s.controls.show_portrait=!0,s.c
1801ontrols.upload_status="pending",s.controls.crop_offset_x=50,s.controls.crop_offset_y=0,s.controls.crop_zoom=1;const l=e.querySelector(".qc-modal-body"),d=l.querySelector('[data-toggle="show_portrait"]');d&&(d.checked=!0),l.querySelectorAll("[data-img]").forEach(p=>p.classList.toggle("is-active",p.dataset.img==="upload"));const u=l.querySelector(".qc-upload-note");u&&u.classList.remove("hidden"),n.classList.add("hidden"),n.innerHTML="",_qcRenderPreview()}function _qcSpinnerHtml(){return'<span class="tf-spinner tf-spinner--inline" aria-hidden="true"><svg viewBox="0 0 556 580" preserveAspectRatio="xMidYMin meet"><use href="#tf-mark"/><g class="tf-thread-g tf-thread--a"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--b"><line x1="322" y1="210" x2="322" y2="270"/></g><g class="tf-thread-g tf-thread--c"><line x1="322" y1="210" x2="322" y2="270"/></g></svg></span>'}async function _qcGenerate(e){const t=currentCampaign&&currentCampaign.id,n=_qcState;if(!t||!n)return;const a=e.querySelector(".qc-status"),s=e.querySelector(".qc-result"),i=e.querySelector(".qc-generate"),o=e.querySelector(".qc-share"),r=JSON.stringify(_qcComposeSpec(n.ctx,n.controls));if(n.card&&n.card.png_url&&n.renderedKey===r){_qcCardShareMenu(n.card,o.offsetParent?o:i);return}a.innerHTML=_qcSpinnerHtml()+" Rendering your card\u2026",a.classList.remove("error"),s.classList.add("hidden"),i.disabled=!0;const c=n.controls;let l;try{l=await api("POST",`/api/campaigns/${t}/quote-cards`,{quote_id:n.quoteId,variant:c.variant,accent:c.accent,theme:c.theme,show_portrait:c.show_portrait,show_context:c.show_context,show_session_title:c.show_session_title,image_source:c.image_source,image_url:c.image_url,overrides:c.overrides,crop_offset_x:c.crop_offset_x,crop_offset_y:c.crop_offset_y,crop_zoom:c.crop_zoom})}catch{a.textContent="Could not start the render. Try again.",a.classList.add("error"),i.disabled=!1;return}if(!l||!l.id){a.textContent="Could not start the render. Try again.",a.classList.add("error"),i.disabled=!1;return}const d=l.id;let u=0;const p=async()=>{u++;let m;try{m=await api("GET",`/api/campaigns/${t}/quote-cards/${d}
1801`)}catch{m=null}if(m&&m.status==="done"&&m.png_url){n.card=m,n.renderedKey=r,i.disabled=!1,a.textContent="Card ready, pick where to share it.",a.classList.remove("error"),s.classList.remove("hidden"),o&&(o.classList.add("btn-share--pulse"),o.addEventListener("animationend",()=>o.classList.remove("btn-share--pulse"),{once:!0})),e.querySelector(".qc-download").onclick=()=>{_qcDownload(m.png_url),_qcEngage("download")},o.onclick=f=>_qcCardShareMenu(m,f.currentTarget),_qcCardShareMenu(m,o),_qcCardedQuoteIds.add(String(n.quoteId)),_qcApplyCardBadges();return}if(m&&m.status==="failed"){a.textContent="The render failed. Try again.",a.classList.add("error"),i.disabled=!1;return}if(u>40){a.textContent="Still rendering, check back in a moment.",i.disabled=!1;return}setTimeout(p,1500)};setTimeout(p,1500)}async function _qcDownload(e){try{const t=await fetch(e).then(a=>a.blob()),n=document.createElement("a");n.href=URL.createObjectURL(t),n.download="threadfall-quote-card.png",document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(n.href),4e3)}catch{window.open(e,"_blank","noopener")}}function _qcShareText(e){const t=_qcState;if(!t)return"";const n=_qcComposeSpec(t.ctx,t.controls),a=n.character_name+(_qcAt(n.handle)?" ("+_qcAt(n.handle)+")":""),s=e&&e.share_token?`/?qc=${e.share_token}`:"";return`"${n.quote}" \u2014 ${a} \xB7 made live at thread-fall.com${s}`}function _qcCardShareMenu(e,t){document.querySelectorAll(".tf-share-menu").forEach(l=>l.remove());const n=_qcShareText(e);let a=!1;try{a=!!(navigator.canShare&&navigator.canShare({files:[new File([new Blob],"c.png",{type:"image/png"})]}))}catch{a=!1}const s=document.createElement("div");s.className="tf-share-menu",s.innerHTML='<button type="button" class="tf-share-opt tf-share-opt--primary" data-sh="file">Share</button><button type="button" class="tf-share-opt" data-sh="copy-img">Copy image</button><button type="button" class="tf-share-opt" data-sh="x">Share on X</button><button type="button" class="tf-share-opt" data-sh="download">Download</button><div class="tf-share-sep" aria-hidden="true"></div><button type="button" class="tf-share-opt tf-share-opt--tf" data-sh="discord"><img class="tf-share-mark" src="/static/threadfall-mark-square.svg" alt="" aria-hidden="true">Post to Threadfall Discord</button>',document.body.appendChild(s);const o=(t||document.querySelector("#quote-card-modal .qc-share")||document.body).getBoundingClientRect();s.style.top=`${Math.max(8,Math.min(o.bottom+6,window.innerHeight-s.offsetHeight-8))}px`,s.style.left=`${Math.max(8,Math.min(o.left,window.innerWidth-s.offsetWidth-8))}px`;const r=()=>s.remove(),c=l=>s.querySelector(`[data-sh="${l}"]`);c("file").onclick=async()=>{if(a&&_tfNativeSharePreferred())try{const l=await fetch(e.png_url).then(u=>u.blob()),d=new File([l],"threadfall-quote-card.png",{type:"image/png"});await navigator.share({files:[d],text:n}),_qcEngage("native")}catch{}else{const l=e&&e.png_url?`${n}
1802${e.png_url}`:n;_tfCopyText(l,"Post copied. Paste into Discord. Text and card image included."),_qcEngage("copy_post")}r()},c("download").onclick=()=>{_qcDownload(e.png_url),_qcEngage("download"),r()},c("x").onclick=()=>{window.open("https://twitter.com/intent/tweet?text="+encodeURIComponent(n),"_blank","noopener"),_qcEngage("x"),r()},c("copy-img")&&(c("copy-img").onclick=async()=>{try{const l=await fetch(e.png_url).then(d=>d.blob());await navigator.clipboard.write([new ClipboardItem({[l.type||"image/png"]:l})]),showToast("Image copied, paste it into any chat or Discord.")}catch{_tfCopyText(e.png_url,"Image link copied, paste it anywhere.")}_qcEngage("copy"),r()}),c("discord").onclick=async()=>{r();const l=currentCampaign&&currentCampaign.id;if(!(!l||!e||!e.id)){showToast("Posting to the Threadfall Discord\u2026");try{await api("POST",`/api/campaigns/${l}/quote-cards/${e.id}/post-to-discord`,{}),showToast("Posted to the Threadfall Discord quotes wall! \u{1F3B4}"),_qcEngage("tf_discord")}catch(d){showToast("Could not post to Discord: "+(d&&d.message||"unknown error")+".")}}},setTimeout(()=>document.addEventListener("click",function l(d){s.contains(d.target)||(r(),document.removeEventListener("click",l))}),0)}function _qcEngage(e){const t=_qcState,n=currentCampaign&&currentCampaign.id;if(!(!t||!n))try{api("POST",`/api/campaigns/${n}/quotes/${t.quoteId}/engagement`,{target:e})}catch{}}
1802document.addEventListener("click",e=>{if(e.target.closest&&e.target.closest(".qc-modal-close")){const n=document.getElementById("quote-card-modal");n&&n.classList.add("hidden");return}const t=document.getElementById("quote-card-modal");t&&!t.classList.contains("hidden")&&e.target===t&&t.classList.add("hidden")}),document.getElementById("profile-nav-btn").addEventListener("click",openProfileModal),document.getElementById("cs-profile-btn").addEventListener("click",openProfileModal),document.getElementById("profile-modal-close").addEventListener("click",()=>{document.getElementById("profile-modal").classList.add("hidden")}),document.getElementById("profile-profanity-toggle").addEventListener("change",function(){localStorage.setItem("tf_profanity_filter",this.checked?"true":"false"),document.getElementById("profile-profanity-label").textContent=this.checked?"On":"Off"});function _syncPublicHideToggle(e){const t=document.getElementById("profile-public-hide-toggle"),n=document.getElementById("profile-public-hide-label");t&&(t.checked=!!e),n&&(n.textContent=e?"On":"Off")}document.getElementById("profile-public-hide-toggle")?.addEventListener("change",async function(){const e=this.checked;_syncPublicHideToggle(e);try{await api("PUT","/api/profile",{hide_from_public_pages:e}),currentUser&&(currentUser.hide_from_public_pages=e),typeof showToast=="function"&&showToast(e?"Your characters are now hidden from public pages":"Your characters can a
1802ppear on public pages")}catch(t){_syncPublicHideToggle(!e),typeof showToast=="function"&&showToast(t&&t.message||"Could not save that, try again")}}),document.getElementById("epd-close").addEventListener("click",_hideEntityPreview),document.getElementById("lrb-return-btn").addEventListener("click",_returnToLiveSession),document.getElementById("lrb-end-btn").addEventListener("click",()=>{_liveMinimized=!1,document.getElementById("live-session-screen").classList.remove("hidden"),document.getElementById("main-app").classList.add("hidden"),closeLiveSession()});let _profileModalMousedownOnBackdrop=!1;document.getElementById("profile-modal").addEventListener("mousedown",e=>{_profileModalMousedownOnBackdrop=e.target===document.getElementById("profile-modal")}),document.getElementById("profile-modal").addEventListener("click",e=>{e.target===document.getElementById("profile-modal")&&_profileModalMousedownOnBackdrop&&document.getElementById("profile-modal").classList.add("hidden")}),document.getElementById("profile-save-btn").addEventListener("click",async()=>{const e=document.getElementById("profile-display-name").value.trim();try{const t=await api("PUT","/api/profile",{display_name:e||null});currentUser.display_name=t.display_name,currentUser.avatar_url=t.avatar_url,updateNavAvatar(),document.getElementById("profile-modal").classList.add("hidden"),showToast("Profile saved.")}catch(t){showToast("Save failed: "+t.message)}}),document.getElementById("profile-avatar-edit-btn").addEventListener("click",()=>{document.getElementById("profile-avatar-file").click()}),document.getElementById("profile-discord-btn").addEventListener("click",()=>{const e=getStoredToken();if(!e){showToast("Please log in first.");return}window.location.href=`/api/auth/discord?token=${encodeURIComponent(e)}`}),document.getElementById("profile-avatar-file").addEventListener("change",async e=>{const t=e.target.files[0];if(!t)return;const n=document.getElementById("profile-avatar-edit-btn");n.textContent="Uploading\u2026",n.disabled=!0;try{const a=await _tfUploadDirect(t,{paramsPath:"/api/profile/upload-params",kind:"avatar",endpoint:"/api/profile/avatar",label:"Avatar"});currentUser.avatar_url=a.avatar_url,currentUser.display_name=a.display_name,updateNavAvatar(),document.getElementById("profile-avatar-lg").innerHTML=_avatarHtml(currentUser.avatar_url,currentUser.display_name||currentUser.email,88),showToast("Photo updated.")}catch(a){showToast("Upload failed: "+a.message)}finally{n.textContent="\u270E Change photo",n.disabled=!1,e.target.value=""}});async function doLogout(){if(_tfHeldRecording){if(!await confirmDialog({title:"Log out with an unsaved recording?",message:"It will be discarded. Download it from the banner first if you want to keep it.",confirmLabel:"Log out",cancelLabel:"Stay",danger:!0}))return;_liveTel("held_recording_discard",{bytes:_tfHeldRecording.blob?.size||0,on:"logout"}),_tfHeldRecording=null,_hideCaptureAlert()}_presStopAll(),await api("POST","/api/auth/logout").catch(()=>{}),setStoredToken(null),currentRole=null,currentCampaign=null;try{document.title=_tfDocTitleFor(!1)}catch{}document.getElementById("profile-nav-btn").classList.add("hidden"),document.getElementById("profile-modal").classList.add("hidden"),showAuthView("login"),showScreen("auth-screen"),document.getElementById("auth-email")?.focus()}document.getElementById("profile-logout-btn").addEventListener("click",doLogout);async function _initTestRecSettings(){const e=document.getElementById("profile-test-rec-row"),t=document.getElementById("profile-test-rec-toggle");if(!e||!t)return;let n=null;try{n=await api("GET","/api/test-recording/status")}catch{}e.classList.toggle("hidden",!(n&&n.eligible)),t.checked=!_testDismissed()}document.getElementById("profile-test-rec-toggle")?.addEventListener("change",e=>{try{e.target.checked?localStorage.removeItem("tf_test_rec_dismissed"):localStorage.setItem("tf_test_rec_dismissed","1")}catch{}_refreshTestAllowance()}),document.getElementById("profile-restore-helpers-btn")?.addEventListener("click",()=>{const e=document.getElementById("profile-restore-helpers-btn"),t=document.getElementById("profile-restore-helpers-msg");e&&(e.disabled=!0,e.textContent="Restoring\u2026");try{const n=[];for(let a=0;a<localStorage.length;a++){const s=localStorage.key(a);
1802s&&s.startsWith("tf_")&&s.toLowerCase().includes("dismiss")&&n.push(s)}n.forEach(a=>{try{localStorage.removeItem(a)}catch{}})}catch{}t&&t.classList.remove("hidden"),setTimeout(()=>{window.location.reload()},500)}),document.getElementById("profile-delete-account-btn").addEventListener("click",async()=>{if(await confirmDialog({title:"Delete Account",message:"Permanently delete your account? This removes all your data and cannot be undone.",confirmLabel:"Delete Account"}))try{await api("DELETE","/api/profile"),_tfHeldRecording&&(_liveTel("held_recording_discard",{on:"account_delete"}),_tfHeldRecording=null,_hideCaptureAlert()),setStoredToken(null),currentRole=null,currentCampaign=null,document.getElementById("profile-modal").classList.add("hidden"),showAuthView("login"),showScreen("auth-screen"),showToast("Your account has been deleted.")}catch(t){showToast("Delete failed: "+t.message)}}),document.getElementById("profile-change-pw-btn").addEventListener("click",async()=>{const e=document.getElementById("profile-pw-email"),t=document.getElementById("profile-pw-msg"),n=e.value.trim();if(!n){showToast("Enter your email to receive a reset link.");return}const a=document.getElementById("profile-change-pw-btn");a.disabled=!0,a.textContent="Sending\u2026",t.classList.add("hidden");try{await api("POST","/api/auth/forgot-password",{email:n}),t.classList.remove("hidden")}catch(s){showToast("Failed to send reset link: "+s.message)}finally{a.disabled=!1,a.textContent="Send Reset Link"}});async function _tfConsumePlanIntent(){let e=null;try{e=sessionStorage.getItem("tf_plan_intent")}catch{}if(!e)return!1;try{sessionStorage.removeItem("tf_plan_intent")}catch{}try{const t=await api("POST","/api/billing/create-checkout-session",{tier:e,interval:"monthly"});if(t&&t.url)return window.location.href=t.url,!0}catch{}return!1}async function doLogin(){const e=document.getElementById("auth-email").value.trim(),t=document.getElementById("auth-password").value,n=document.getElementById("auth-login-error");n.classList.add("hidden");try{const a=await api("POST","/api/auth/login",{email:e,password:t});if(console.log("[login] token in response:",!!a?.token,"token prefix:",a?.token?.substring(0,20)),a.token&&setStoredToken(a.token),console.log("[login] localStorage after save:",!!getStoredToken()),currentUser={id:a.id,email:a.email,role:a.role,is_admin:!!a.is_admin,display_name:a.display_name||null,avatar_url:a.avatar_url||null,verified:!!a.verified},document.getElementById("admin-panel-btn")?.classList.toggle("hidden",!currentUser.is_admin),updateNavAvatar(),initAdminPulse(),_tfMarkInternalBrowser(),_tfLoadBillingTier(),_tfaApplyPendingSource(),_tfMaybeRedeemGift(),document.getElementById("auth-password").value="",await _handleRoll20ImportLandingFromPath()||await _tfConsumePlanIntent()||await _adminHandleHash())return;if(!(_tfConsumePostLoginPath()&&await _handleCampaignDeepLink())){try{history.replaceState({type:"selector"},"","/")}catch{}showScreen("campaign-selector"),await loadCampaignSelector(),_tfWarnDeadDoor()}}catch(a){n.textContent=a.message.includes("verified")?'Email not verified. Check your inbox or use "Create account" to resend.':a.message||"Login failed.",n.classList.remove("hidden")}}document.getElementById("auth-login-btn").addEventListener("click",doLogin),document.getElementById("auth-email").addEventListener("keydown",e=>{e.key==="Enter"&&document.getElementById("auth-password").focus()}),document.getElementById("auth-password").addEventListener("keydown",e=>{e.key==="Enter"&&doLogin()}),document.getElementById("auth-goto-register").addEventListener("click",()=>{showAuthView("register"),document.getElementById("reg-email").focus()}),document.getElementById("auth-goto-forgot").addEventListener("click",()=>{showAuthView("forgot"),document.getElementById("forgot-email").focus()});function _tfFireSignupPixels(){try{typeof window.rdt=="function"&&window.rdt("track","SignUp")}catch{}try{typeof window.fbq=="function"&&window.fbq("track","CompleteRegistration")}catch{}try{typeof window.gtag=="function"&&window.gtag("event","sign_up",{method:"email"})}catch{}}async function doRegister(){const e=document.getElementById("reg-email").value.trim(),t=document.getElementById("reg-password").value,
1802n=document.getElementById("auth-reg-error");n.classList.add("hidden");let a="",s="";const o=(document.getElementById("reg-referral-code")?.value||"").trim().toUpperCase();let r="";try{r=(localStorage.getItem("tf_referral_code")||"").toUpperCase()}catch{}let c="";const l=(document.cookie||"").match(/(?:^|; )tf_ref=([^;]+)/);if(l&&(c=decodeURIComponent(l[1]).toUpperCase()),o&&/^[A-Z0-9]{6,16}$/.test(o))if(a=o,o===r)try{s=localStorage.getItem("tf_referral_source")||"ref_param"}catch{s="ref_param"}else o===c?s="ref_cookie":s="manual_code";else if(r&&/^[A-Z0-9]{6,16}$/.test(r)){a=r;try{s=localStorage.getItem("tf_referral_source")||"ref_param"}catch{s="ref_param"}}else c&&/^[A-Z0-9]{6,16}$/.test(c)&&(a=c,s="ref_cookie");const d={email:e,password:t};a&&(d.referral_code=a,s&&(d.referral_source=s));try{const u=localStorage.getItem("tf_vid");u&&(d.landing_visitor_id=u)}catch{}try{const u=await api("POST","/api/auth/register",d);_tfFireSignupPixels();try{localStorage.removeItem("tf_referral_code")}catch{}try{localStorage.removeItem("tf_referral_source")}catch{}try{document.cookie="tf_ref=; path=/; max-age=0; SameSite=Lax"}catch{}if(u&&u.token){if(setStoredToken(u.token),currentUser={id:u.id,email:u.email,role:u.role,is_admin:!!u.is_admin,display_name:u.display_name||null,avatar_url:u.avatar_url||null,verified:!!u.verified},document.getElementById("admin-panel-btn")?.classList.toggle("hidden",!currentUser.is_admin),updateNavAvatar(),initAdminPulse(),_tfMarkInternalBrowser(),_tfLoadBillingTier(),_tfaApplyPendingSource(),_tfMaybeRedeemGift(),document.getElementById("reg-password").value="",await _handleRoll20ImportLandingFromPath()||await _tfConsumePlanIntent())return;if(!(_tfConsumePostLoginPath()&&await _handleCampaignDeepLink())){try{history.replaceState({type:"selector"},"","/")}catch{}showScreen("campaign-selector"),await loadCampaignSelector(),_tfWarnDeadDoor()}u.existing_login?showToast("You already had an account, signed you in.",5e3):(showToast("Welcome to Threadfall. We sent a verification link to "+u.email+", verify it to record live sessions and generate recaps.",6500),u.trial_withheld&&u.trial_withheld_message&&setTimeout(()=>showToast(u.trial_withheld_message,15e3),7e3))}else document.getElementById("verify-email-display").textContent=e,showAuthView("verify-pending")}catch(u){if(/already has an account/i.test(u.message||"")){showAuthView("login");const p=document.getElementById("auth-email");p&&(p.value=e);const m=document.getElementById("auth-login-error");m&&(m.textContent=u.message,m.classList.remove("hidden")),document.getElementById("auth-password")?.focus();return}n.textContent=u.message,n.classList.remove("hidden")}}document.getElementById("auth-register-btn").addEventListener("click",doRegister),document.getElementById("reg-password").addEventListener("keydown",e=>{e.key==="Enter"&&doRegister()}),document.getElementById("auth-goto-login").addEventListener("click",()=>{showAuthView("login"),document.getElementById("auth-email").focus()}),document.getElementById("auth-resend-btn").addEventListener("click",async()=>{const e=document.getElementById("verify-email-display").textContent,t=document.getElementById("auth-resend-msg");t.classList.add("hidden");try{await api("POST","/api/auth/resend-verification",{email:e}),t.classList.remove("hidden")}catch{}}),document.getElementById("auth-goto-login-2").addEventListener("click",()=>{showAuthView("login"),document.getElementById("auth-email").focus()}),function(){const t=document.getElementById("signup-source-post");t&&t.addEventListener("change",()=>{const n=t.value||"";if(n){try{localStorage.setItem("tf_pending_source",n)}catch{}document.getElementById("signup-source-thanks")?.classList.remove("hidden"),t.disabled=!0}})}();async function _tfaApplyPendingSource(){let e="";try{e=localStorage.getItem("tf_pending_source")||""}catch{}if(e)try{await api("POST","/api/account/signup-source",{source:e});try{localStorage.removeItem("tf_pending_source")}catch{}}catch{}}function _tfaMaybeAskSource(){let e="";try{e=localStorage.getItem("tf_ask_source")||""}catch{}if(e!=="1")return;const t=document.getElementById("signup-source-banner"),n=document.getElementById("signup-source-inapp");if(!t||!n||t.dataset.wired)return;t.dataset.wired="1";
1802const a=()=>{t.classList.add("hidden");try{localStorage.removeItem("tf_ask_source")}catch{}};n.addEventListener("change",async()=>{const s=n.value||"";if(s){n.disabled=!0;try{await api("POST","/api/account/signup-source",{source:s})}catch{}a()}}),document.getElementById("signup-source-dismiss")?.addEventListener("click",a),t.classList.remove("hidden")}async function doForgotPassword(){const e=document.getElementById("forgot-email").value.trim(),t=document.getElementById("auth-forgot-error"),n=document.getElementById("auth-forgot-msg");t.classList.add("hidden"),n.classList.add("hidden");try{await api("POST","/api/auth/forgot-password",{email:e}),n.classList.remove("hidden")}catch(a){t.textContent=a.message,t.classList.remove("hidden")}}document.getElementById("auth-forgot-btn").addEventListener("click",doForgotPassword),document.getElementById("forgot-email").addEventListener("keydown",e=>{e.key==="Enter"&&doForgotPassword()}),document.getElementById("auth-goto-login-3").addEventListener("click",()=>{showAuthView("login"),document.getElementById("auth-email").focus()});async function doResetPassword(){const e=document.getElementById("reset-password").value,t=document.getElementById("reset-confirm").value,n=document.getElementById("auth-reset-error");n.classList.add("hidden");try{const a=await api("POST","/api/auth/reset-password",{token:window._resetToken,password:e});a?.token&&setStoredToken(a.token),window._resetToken=null,showScreen("campaign-selector"),await loadCampaignSelector()}catch(a){n.textContent=a.message,n.classList.remove("hidden")}}document.getElementById("auth-reset-btn").addEventListener("click",doResetPassword),document.getElementById("reset-confirm").addEventListener("keydown",e=>{e.key==="Enter"&&doResetPassword()});function _setRecBtnLabel(){const e=document.getElementById("rec-start-btn");if(!e)return;const t=(allSessions||[]).find(a=>a.id===(liveSession&&liveSession.id)),n=!!(typeof liveTranscriptAccum=="string"&&liveTranscriptAccum.trim()||t&&(t.transcript_chars>0||typeof t.raw_transcript=="string"&&t.raw_transcript.trim()));e.textContent=n?"\u23FA Resume":"\u23FA Start Recording"}function resetRecordingUI(){_tfReleaseWakeLock(),_tfDimStop(),clearInterval(liveTimerInterval),liveTimerInterval=null,_cancelChime(),_hideCaptureAlert(),typeof _stopRecLevelMeter=="function"&&_stopRecLevelMeter(),document.getElementById("rec-ready").classList.remove("hidden"),document.getElementById("rec-active").classList.add("hidden"),_setRecBtnLabel(),document.getElementById("rec-clock").textContent="00:00:00";const e=document.getElementById("rec-indicator"),t=document.getElementById("rec-state-lbl");e.className="rec-indicator recording",t.className="rec-state-lbl recording",t.textContent="REC",document.getElementById("live-header-rec-dot").classList.add("hidden")}let _lastSavedSessionForCta=null,_recapArmTimer=null;const _POST_STOP_SHORT_SEC=120;function _recapStatsFromTranscript(e){const t=new Set;(e||"").split(`
1803`).forEach(a=>{const s=a.trim().match(/^([^:]{1,30}):\s+\S/);s&&t.add(s[1].trim().toLowerCase())});const n=(e||"").split(/\s+/).filter(Boolean).length;return{speakers:t.size,words:n}}function _fmtDur(e){e=Math.max(0,Math.round(e||0));const t=Math.floor(e/3600),n=Math.floor(e%3600/60);return t?`${t}h ${n}m`:n?`${n} min`:`${e}s`}async function _micCheckDiscard(e,t){try{await api("DELETE",`/api/campaigns/${e}/sessions/${t}`)}catch{return!1}currentCampaign?.id===e&&(allSessions=allSessions.filter(n=>n.id!==t)),liveSession?.id===t&&(liveSession=null);try{localStorage.removeItem(_transcriptBackupKey(e,t))}catch{}return!0}function _resetPostStopView(){document.getElementById("rec-post-stop-actions")?.classList.remove("hidden"),document.getElementById("rec-post-stop-progress")?.classList.add("hidden"),document.getElementById("rec-post-stop-view-btn")?.classList.add("hidden"),document.getElementById("rec-post-stop-cancel-btn")?.classList.remove("hidden");const e=document.getElementById("rec-post-stop-status");e&&(e.textContent="")}function _showPostStopCta(){const e=document.getElementById("rec-post-stop-cta");if(!e||!_lastSavedSessionForCta)return;const t=_lastSavedSessionForCta,n=[];t.durationSec&&n.push(_fmtDur(t.durationSec)),t.speakers&&n.push(`${t.speakers} speaker${t.speakers!==1?"s":""}`);const a=document.getElementById("rec-post-stop-title");a&&(a.textContent="\u2713 Transcript saved"+(n.length?" \xB7 "+n.join(" \xB7 "):".")),e.classList.toggle("is-short",(t.durationSec||0)<_POST_STOP_SHORT_SEC),_resetPostStopView(),e.classList.remove("hidden");const s=document.getElementById("rec-post-stop-promo");s&&(_isGm()&&(t.txWords||0)>1500?(s.innerHTML=`<div class="referral-promo-slot referral-promo-slot--post-live" data-se
1803ssion-id="${t.id}" data-surface="post_live"></div>`,typeof _mountReferralPromoSlots=="function"&&_mountReferralPromoSlots()):s.innerHTML="")}function _hidePostStopCta(){_recapArmTimer&&(clearTimeout(_recapArmTimer),_recapArmTimer=null),document.getElementById("rec-post-stop-cta")?.classList.add("hidden");const e=document.getElementById("rec-post-stop-promo");e&&(e.innerHTML=""),_resetPostStopView(),_lastSavedSessionForCta=null}async function _armRecapFire(e){if(!e){_hidePostStopCta();return}if(typeof _recapPreflight=="function"&&!await _recapPreflight(e.id,e)){_resetPostStopView();return}document.getElementById("rec-post-stop-actions")?.classList.add("hidden"),document.getElementById("rec-post-stop-progress")?.classList.remove("hidden"),document.getElementById("rec-post-stop-view-btn")?.classList.add("hidden"),document.getElementById("rec-post-stop-cancel-btn")?.classList.remove("hidden");const t=document.getElementById("rec-post-stop-status");t&&(t.textContent=`Generating recap for \u201C${e.title}\u201D\u2026 \xB7 tap Cancel to stop`),_recapArmTimer&&clearTimeout(_recapArmTimer),_recapArmTimer=setTimeout(()=>{_recapArmTimer=null,_commitRecapFire(e)},8e3)}function _cancelRecapFire(){_recapArmTimer&&(clearTimeout(_recapArmTimer),_recapArmTimer=null),_resetPostStopView()}async function _commitRecapFire(e){const t=document.getElementById("rec-post-stop-status");if(document.getElementById("rec-post-stop-cancel-btn")?.classList.add("hidden"),t&&(t.textContent="Writing the recap\u2026 you can leave, we\u2019ll notify you when it\u2019s ready."),typeof _fireRecapForSession!="function"){_goToSavedRecap(e);return}const n=a=>{const s=document.getElementById("rec-post-stop-view-btn");s&&(s.textContent=a,s.classList.remove("hidden"),s.onclick=()=>_goToSavedRecap(e))};try{await _fireRecapForSession(e.id,{statusEl:t||null,skipPreflight:!0,onComplete:a=>{if(a&&a.unchanged){showToast("Nothing changed since the last recap.",4e3);return}t&&(t.textContent=`\u2713 Recap ready for \u201C${e.title}\u201D.`),n("View recap \u2192"),showToast(`\u2713 Recap ready for \u201C${e.title}\u201D.`,6e3)}})}catch{t&&(t.textContent="Recap failed, open the session on Chronicle to retry."),n("Open on Chronicle \u2192")}}async function _goToSavedRecap(e){if(!e){_hidePostStopCta();return}const t=e;if(_hidePostStopCta(),typeof _lastActiveTab<"u"&&(_lastActiveTab="sessions"),typeof _minimizeLiveSession=="function"?_minimizeLiveSession():(document.getElementById("live-session-screen")?.classList.add("hidden"),document.getElementById("main-app")?.classList.remove("hidden"),document.getElementById("tab-btn-sessions")?.click()),expandedSessionId=t.id,typeof sessionEntries<"u"&&!sessionEntries[t.id])try{await loadSessionEntries(t.id)}catch{}}document.getElementById("rec-post-stop-recap-btn")?.addEventListener("click",()=>_armRecapFire(_lastSavedSessionForCta)),document.getElementById("rec-post-stop-cancel-btn")?.addEventListener("click",_cancelRecapFire),document.getElementById("rec-post-stop-dismiss-btn")?.addEventListener("click",_hidePostStopCta);async function startSessionRecording(e,t,n){if(_tfHeldRecording)throw _tfShowHeldRecordingBanner(),new Error("A recording is still unsaved. Download or discard it first.");liveAudioChunks=[],livePublishing=!0,_userInitiatedStop=!1,liveAudioSession=e,liveRecordingStartTime=Date.now(),_liveAaiBilledSeconds=0,_liveAaiVendorModel=null,_liveAaiOpenAtMs=0,_liveAaiEstimatedSeconds=0,_captureState=_CSTATE.IDLE,_liveTel("recording_start",{sessionId:e?.id});const a=currentCampaign?.id??e?.campaign_id,s=new AudioContext;liveAudioContext=s,await s.resume();const i=s.createMediaStreamDestination();if(s.addEventListener("statechange",()=>{_liveTel("audio_ctx_state_change",{state:s.state}),s.state==="suspended"&&liveMediaRecorder?.state==="recording"&&(_liveTel("audio_ctx_auto_suspended"),s.resume().catch(d=>_liveTel("audio_ctx_resume_failed",{error:d.message})))}),liveAnalyser=s.createAnalyser(),liveAnalyser.fftSize=512,liveAnalyser.smoothingTimeConstant=.6,liveAnalyser.connect(i),n){const d=n.getAudioTracks();d.length?(s.createMediaStreamSource(n).connect(liveAnalyser),d.forEach(u=>{u.addEventListener("ended",()=>{_liveTel("display_track_ended",{label:u.label}),liveMediaRecorder?.state==="recording"&&showToast("System audio track ended, continuing with mic only.",4e3)})}
1803)):showToast("No system audio track, capturing mic only."),n.getVideoTracks().forEach(u=>u.stop())}const o=t??await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1,channelCount:1}}).catch(()=>null);liveMicStream=o,liveDisplayStream=n??null,o&&(s.createMediaStreamSource(o).connect(liveAnalyser),_attachMicTrackHandlers(o),_liveTel("mic_stream_started",{tracks:o.getAudioTracks().length}));const r=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm";let c=null;try{i.channelCount=1}catch(d){c="mono: "+d.message}try{liveMediaRecorder=new MediaRecorder(i.stream,{mimeType:r,audioBitsPerSecond:TF_RECORDER_AUDIO_BPS})}catch(d){c=(c?c+"; ":"")+"bitrate: "+d.message,liveMediaRecorder=new MediaRecorder(i.stream,{mimeType:r})}liveMediaRecorder.ondataavailable=d=>{d.data.size>0?(liveAudioChunks.push(d.data),_wdLastChunkMs=Date.now(),_wdSilentChunks=0):(_wdSilentChunks++,_wdSilentChunks===5&&_liveTel("silent_chunks_run",{count:_wdSilentChunks}))},liveMediaRecorder.onerror=d=>{_liveTel("recorder_error",{message:d.error?.message||"unknown"}),_handleCaptureFailure("MediaRecorder error: "+(d.error?.message||"unknown"))},liveMediaRecorder.onstop=async()=>{const d=liveAudioSession,u=a??null,p=Date.now()-liveRecordingStartTime,m=_liveAutoCreatedSessionId;_liveAutoCreatedSessionId=null;const f=_userInitiatedStop;_userInitiatedStop=!1,_stopWatchdog(),_hideCaptureAlert(),_setCaptureState(_CSTATE.IDLE),_tfReleaseWakeLock(),_liveTelDeferStart(),_liveTel("recording_stop",{sessionId:d?.id,durationMs:Date.now()-liveRecordingStartTime,chunks:liveAudioChunks.length,segments:liveTranscriptSegments.length,recoveryCount:_wdRecoveryCount}),_usingAAI&&liveWsRetiring&&await _waitForRetiringDrain(),_usingAAI&&liveWs&&await _waitForAaiTermination();const g=liveWsSessionComplete;_stopLiveScan(),stopLiveTranscription(),resetRecordingUI(),liveMediaRecorder=null,liveAudioSession=null,liveAudioContext=null,s.close().catch(()=>{}),liveMicStream?.getTracks().forEach(b=>b.stop()),liveMicStream=null,liveDisplayStream?.getAudioTracks().forEach(b=>b.stop()),liveDisplayStream=null;let h="unknown";const v=_testRecordingMode;if(_testRecordingMode=!1,clearTimeout(_testClipTimer),_setTestRecBadge(!1),!livePublishing)liveAudioChunks=[],h="discarded",u&&d?.id&&m===d.id&&p<_MIC_CHECK_MAX_MS&&!_priorTranscriptBaseline&&liveLogEntries.length===0&&await _micCheckDiscard(u,d.id)?showToast("Recording discarded \u2014 mic check, no session created."):showToast("Recording discarded.");else if(!u||!d?.id)liveAudioChunks=[],showToast("Recording lost, could not determine session. Please check the session transcript manually.",8e3),h="lost";else if(livePublishing=!1,_stopLiveLogPoll(),_stopTranscriptPoll(),_usingAAI&&liveTranscriptSegments.length>0){let b=liveTranscriptAccum.trim()||_buildLiveTranscriptAccum().trim();g||(_liveTel("aai_termination_timeout"),console.warn("[AAI] SessionTerminated not received in time \u2014 saving partial transcript (last few seconds may be missing)"));const y=_wdRecoveryCount>0?`
1804
1805--- [Session diagnostics: ${_wdRecoveryCount} recovery attempt(s) during this recording] ---`:"";if(y&&(b+=y),v){showToast("Saving test recording\u2026"),liveAudioChunks=[];try{const _=typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0,w=await api("POST",`/api/campaigns/${u}/sessions/${d.id}/test-transcribe`,{segment:b,seconds:_});_testAllowance={eligible:w.eligible,clips_left:w.clips_left,seconds_left:w.seconds_left,clip_seconds_cap:w.clip_seconds_cap,clip_cap:w.clip_cap,total_seconds:w.total_seconds},currentCampaign?.id===u&&(allSessions=allSessions.map(E=>E.id===d.id?{...E,test_transcript:w.test_transcript}:E));const $=Math.max(0,Math.round((w.seconds_left||0)/60));showToast(w.eligible?`Test recording saved. That's how it transcribes your table. ~${$} min of free test left.`:"Test recording saved. Your free test time is used up, real recordings save to the session.",7e3),h="test_saved"}catch(_){showToast("Failed to save test recording: "+_.message,8e3),h="failed"}}else if(!_priorTranscriptBaseline&&p<_MIC_CHECK_MAX_MS&&b.length<_MIC_CHECK_MAX_CHARS&&liveLogEntries.length===0){liveAudioChunks=[],h="mic_check";const _=Math.max(1,Math.round(p/1e3));m===d.id&&await _micCheckDiscard(u,d.id)?showToast(`Mic check (${_}s) \u2014 recording discarded, no session created.`,6e3):showToast(`Mic check (${_}s) \u2014 nothing saved to the session.`,6e3)}else{const _=_priorTranscriptBaseline?_priorTranscriptBaseline+`
1806
1807--- Recording continued ---
1808
1809`+b:b;showToast("Saving transcript\u2026"),liveAudioChunks=[];try{_liveAaiSettleUnreported();const w=_liveAaiBilledSeconds+_liveAaiEstimatedSeconds,$=_liveAaiEstimatedSeconds>0;$&&_liveTel("aai_billed_estimated",{vendor:Math.round(_liveAaiBilledSeconds),estimated:Math.round(_liveAaiEstimatedSeconds)});const E=w>0?{voice_billed_seconds:Math.round(w),voice_model:_liveAaiVendorModel||void 0,...$?{voice_stats:{billed_estimated:!0}}:{}}:{},k=await _tfSaveLiveTranscript(u,d.id,{raw_transcript:_,is_live_transcript:!0,recording_stopped:!0,...E}),C=k&&typeof k.raw_transcript=="string"?k.raw_transcript:_,S=Math.abs(C.length-_.length)>5,q=k&&k.title||d.title;currentCampaign?.id===u&&(allSessions=allSessions.map(T=>T.id===d.id?{...T,...k||{},raw_transcript:C,is_live_transcript:!0}:T)),_priorTranscriptBaseline=C,S?(console.warn("[REC] final save: server transcript differs from what was sent (guard reject / concurrent write?)",{sent:_.length,server:C.length}),showToast(`Saved "${q}", but the stored transcript differs from this device, reload to verify before recap.`,9e3)):showToast(`Transcript saved for "${q}", review and run recap.`),h="saved";
1809const I=_recapStatsFromTranscript(_);_lastSavedSessionForCta={id:d.id,title:q,campaignId:u,durationSec:typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0,speakers:I.speakers,txWords:I.words},typeof _showPostStopCta=="function"&&_showPostStopCta()}catch(w){showToast("Failed to save transcript: "+w.message,8e3),console.error("[save-transcript]",w),h="failed"}}}else if(v)liveAudioChunks=[],h="test_saved",showToast("Test recording needs live transcription (AssemblyAI). Nothing was saved to the session.",7e3);else if(!_priorTranscriptBaseline&&p<_MIC_CHECK_MAX_MS&&liveTranscriptSegments.length===0&&liveLogEntries.length===0){liveAudioChunks=[],h="mic_check";const b=Math.max(1,Math.round(p/1e3));d?.id&&m===d.id&&await _micCheckDiscard(u,d.id)?showToast(`Mic check (${b}s) \u2014 recording discarded, no session created.`,6e3):showToast(`Mic check (${b}s) \u2014 nothing saved to the session.`,6e3)}else{const b=new Blob(liveAudioChunks,{type:r});if(liveAudioChunks=[],b.size<1e3)showToast("Recording too short, no audio captured."),h="too_short",d?.id&&m===d.id&&p<_MIC_CHECK_MAX_MS&&!_priorTranscriptBaseline&&liveLogEntries.length===0&&await _micCheckDiscard(u,d.id);else{showToast("Transcribing audio\u2026 this may take a minute.");let y=allSessions.find(_=>_.id===d.id)?.transcript_chars??(_priorTranscriptBaseline||"").length;try{const _=new AbortController,w=setTimeout(()=>_.abort(),8e3),$=getStoredToken(),E=await fetch(`/api/campaigns/${u}/sessions/${d.id}`,{credentials:"include",headers:$?{Authorization:"Bearer "+$}:{},signal:_.signal});clearTimeout(w);const k=E.ok?await E.json():null;k&&Number.isFinite(Number(k.transcript_chars))&&(y=Number(k.transcript_chars))}catch{}try{const _=getStoredToken(),w=await _tfSendRecordingForTranscript(b,u,d.id,liveTranscriptSegments,_);currentCampaign?.id===u&&(allSessions=allSessions.map(E=>E.id===w.session.id?w.session:E)),showToast(`Transcript saved for "${d.title}", review and run recap.`),h="saved";const $=_recapStatsFromTranscript(w.session?.raw_transcript||"");_lastSavedSessionForCta={id:d.id,title:d.title,campaignId:u,durationSec:typeof liveElapsedSeconds=="number"?liveElapsedSeconds:0,speakers:$.speakers,txWords:$.words},typeof _showPostStopCta=="function"&&_showPostStopCta()}catch(_){showToast("Transcription failed: "+_.message,8e3),console.error("[transcribe]",_),h="failed",_tfHoldFailedRecording(b,u,d,liveTranscriptSegments,y,_)}}}if((h==="saved"||h==="mic_check")&&u&&d?.id)try{localStorage.removeItem(_transcriptBackupKey(u,d.id))}catch{}!f&&h!=="discarded"&&(h==="saved"?_showCaptureAlert("Recording stopped unexpectedly, captured audio has been saved.","warning","Mic stream likely ended (Bluetooth disconnect, OS mic mute, or audio device switch). Click Start to continue recording."):h==="failed"&&_tfHeldRecording?_tfShowHeldRecordingBanner():h==="failed"?_showCaptureAlert("Recording stopped unexpectedly and the save failed.","error","A localStorage backup of the transcript was kept: open this session next time and we will offer to restore it."):h==="lost"?_showCaptureAlert("Recording stopped unexpectedly. The session reference was lost during the stop.","error","Check the session transcript manually for any partial saves."):h==="too_short"?_showCaptureAlert("Recording stopped unexpectedly. Less than a second of audio was captured, nothing to save.","warning","Click Start to try again."):h==="mic_check"&&_showCaptureAlert("Recording stopped almost immediately, nothing was saved.","warning","Click Start to try again.")),_liveTelDeferEnd(h)};const l=liveLogEntries.reduce((d,u)=>u.timestamp_seconds!=null?Math.max(d,u.timestamp_seconds):d,-1);liveElapsedSeconds=l>=0?l+30:0,document.getElementById("rec-clock").textContent=fmtTime(liveElapsedSeconds),clearInterval(liveTimerInterval),liveTimerInterval=setInterval(()=>{liveElapsedSeconds++,document.getElementById("rec-clock").textContent=fmtTime(liveElapsedSeconds),_liveMinimized&&_updateLiveRecordBar()},1e3),liveMediaRecorder.start(2e3),_liveTelCarry={t:Date.now(),event:"recorder_config",mimeType:liveMediaRecorder.mimeType,audioBitsPerSecond:liveMediaRecorder.audioBitsPerSecond,channels:i.channelCount,refused:c},_setCaptureState(_CSTATE.HEALTHY),_tfAcquireWakeLock(),_tfDimStart(),_startWatchdog(),startLiveTranscription(),_scheduleChime("first"),document.getElementById("rec-ready").classList.add("hidden"),document.getElementById("rec-active").classList.remove("hidden"),document.getElementById("live-header-rec-dot").classList.remove("hidden"),_startRecLevelMeter(),showToast("Recording started: capturing all audio."),_liveTel("recorder_started",{mimeType:r,micTracks:o?.getAudioTracks().length??0,hasSysAudio:!!n})}const _REC_LEVEL_NUM_SEGS=14;let _recLevelRaf=null;function _initRecLevelMeter(){const e=document.getElementById("rec-level-meter");if(!(!e||e.dataset.initialized==="1")){e.innerHTML="";for(let t=0;t<_REC_LEVEL_NUM_SEGS;t++){const n=document.createElement("span");n.className="rec-level-seg",t>=12?n.classList.add("zone-red"):t>=9?n.classList.add("zone-amber"):n.classList.add("zone-green"),e.a
1809ppendChild(n)}e.dataset.initialized="1"}}function _startRecLevelMeter(){if(!liveAnalyser)return;_initRecLevelMeter();const e=Array.from(document.querySelectorAll("#rec-level-meter .rec-level-seg"));if(!e.length)return;const t=new Uint8Array(liveAnalyser.fftSize||512);let n=0;function a(){if(!liveAnalyser||!liveRecording){e.forEach(l=>l.classList.remove("is-on")),_recLevelRaf=null;return}liveAnalyser.getByteTimeDomainData(t);let s=0;for(let l=0;l<t.length;l++){const d=(t[l]-128)/128;s+=d*d}const i=Math.sqrt(s/t.length),o=20*Math.log10(i+1e-9),r=Math.max(0,Math.min(1,(o+60)/60));n=n*.7+r*.3;const c=Math.round(n*_REC_LEVEL_NUM_SEGS);e.forEach((l,d)=>l.classList.toggle("is-on",d<c)),_recLevelRaf=requestAnimationFrame(a)}cancelAnimationFrame(_recLevelRaf),a()}function _stopRecLevelMeter(){_recLevelRaf&&cancelAnimationFrame(_recLevelRaf),_recLevelRaf=null,document.querySelectorAll("#rec-level-meter .rec-level-seg").forEach(e=>e.classList.remove("is-on"))}let _tfSafariNoSysAudio=!1;(function(){_tfSafariNoSysAudio=/^((?!chrome|android).)*safari/i.test(navigator.userAgent)})();const _REC_MODES=["inperson","virtual","discord"];let _tfRecModeSel="virtual";function _recModeStorageKey(){return currentCampaign?`tf_camp_${currentCampaign.id}_rec_mode`:null}function _tfDiscordVoiceConfigured(){const e=_campDiscordConfig;return!!(e&&e.guild_id&&(e.voice_channel_id||e.recap_channel_id))}function _restoreRecModePreference(){const e=_recModeStorageKey();let t=e?localStorage.getItem(e):null;!t&&currentCampaign&&(t=localStorage.getItem(`tf_camp_${currentCampaign.id}_sys_audio`)==="false"?"inperson":"virtual");const n=currentCampaign?.game_type_default;_REC_MODES.includes(n)&&(t=n),_REC_MODES.includes(t)||(t="virtual"),t==="discord"&&!_tfDiscordVoiceConfigured()&&(t="virtual"),t==="virtual"&&_tfSafariNoSysAudio&&(t="inperson"),_tfRecModeSel=t,_renderRecModeRow()}function _renderRecModeRow(){const e=document.getElementById("rec-mode-row");if(!e)return;const t=_tfDiscordVoiceConfigured();e.querySelectorAll(".rec-mode-btn").forEach(s=>{const i=s.getAttribute("data-rec-mode"),o=i==="discord"&&!t||i==="virtual"&&_tfSafariNoSysAudio;s.classList.toggle("rec-mode-btn--active",i===_tfRecModeSel),s.classList.toggle("rec-mode-btn--locked",o),s.setAttribute("aria-checked",i===_tfRecModeSel?"true":"false"),s.querySelector(".rec-mode-lock")?.classList.toggle("hidden",!(i==="discord"&&!t)),i==="discord"&&(s.title=t?"The Threadfall bot joins your voice channel and records server-side: every speaker identified by their Discord account, and your laptop can sleep. Best when each player is on their own mic/headset: people sharing one room and mic get attributed to whoever's mic picks them up (use In Person mode for same-room tables). Posts a 'Recording' notice in the channel when it joins.":"Set up Discord to enable server-side recording, click to configure (Manage \u2192 Discord: Server ID + Voice channel ID)."),i==="virtual"&&_tfSafariNoSysAudio&&(s.title="System audio capture is not supported in Safari. Use Chrome for online session recording.")});const n=document.getElementById("rec-bt-note");n&&(_tfRecModeSel==="discord"?n.classList.add("hidden"):localStorage.getItem(_BT_NOTE_DISMISSED_KEY)!=="1"&&n.classList.remove("hidden"));const a=document.getElementById("rec-discord-announce-label");a&&_tfRecModeSel==="discord"&&!document.getElementById("rec-discord-announce-chk")?.disabled&&(a.title="Also posts a 'session live now' TEXT announcement in your Discord channel, separate from the voice bot joining.")}function _jumpToDiscordSetup(){closeLiveSession(),document.querySelector('.tab-btn[data-tab="manage"], .nav-utility-btn[data-tab="manage"]')?.click(),setTimeout(()=>{document.getElementById("manage-discord-section")?.scrollIntoView({behavior:"smooth",block:"start"})},200)}document.getElementById("rec-mode-row")?.addEventListener("click",e=>{const t=e.target.closest(".rec-mode-btn");if(!t)return;const n=t.getAttribute("data-rec-mode");if(n==="discord"&&!_tfDiscordVoiceConfigured()){_jumpToDiscordSetup();return}if(n==="virtual"&&_tfSafariNoSysAudio||typeof liveRecording<"u"&&liveRecording||_tfDiscordRecActive)return;_tfRecModeSel=n;const a=_recModeStorageKey();a&&localStorage.setItem(a,_tfRecModeSel),_renderRecModeRow()});function _discordAnnounceStorageKey(){return currentCampaign?`tf_camp_${currentCampaign.id}_discord_announce`:null}function _restoreDiscordAnnouncePreference(){const e=_discordAnnounceStorageKey(),t=document.getElementById("rec-discord-announce-chk");
1809if(!e||!t||t.disabled)return;const n=localStorage.getItem(e);n==="true"&&(t.checked=!0),n==="false"&&(t.checked=!1)}function _applyDiscordAnnounceCapability(){const e=document.getElementById("rec-discord-announce-chk"),t=document.getElementById("rec-discord-announce-label");if(!e||!t)return;const n=_campDiscordConfig;!!(n&&(n.announce_channel_id||n.recap_channel_id))?(e.disabled=!1,t.title="Posts a 'is live now' announcement in your Discord channel when you start recording.",t.style.opacity="",t.style.cursor="",_restoreDiscordAnnouncePreference()):(e.disabled=!0,e.checked=!1,t.title="Set Server ID + Channel ID in Manage \u2192 Discord to enable.",t.style.opacity="0.45",t.style.cursor="not-allowed")}let _tfDiscordRecActive=!1,_tfDiscordRecTimer=null,_tfDiscordRecClockBase=0,_tfDiscordPriorHtml="",_tfDiscordMutedMap={},_tfDiscordAutoMute=!0;function _fmtRemoteClock(e){const t=Math.max(0,Math.floor(e/1e3)),n=Math.floor(t/3600),a=Math.floor(t%3600/60),s=t%60;return`${n}:${String(a).padStart(2,"0")}:${String(s).padStart(2,"0")}`}async function _startDiscordRecording(){const e=currentCampaign?.id;if(!e||_tfDiscordRecActive)return;const t=document.getElementById("rec-start-btn"),n=t?.textContent;t?.setAttribute("disabled",""),t&&(t.textContent="Summoning the bot\u2026");try{const a=document.getElementById("rec-discord-announce-chk"),s=!!(a&&!a.disabled&&a.checked);if(a&&!a.disabled){const c=_discordAnnounceStorageKey();c&&localStorage.setItem(c,String(a.checked))}const i=new Date,o=`${i.getFullYear()}-${String(i.getMonth()+1).padStart(2,"0")}-${String(i.getDate()).padStart(2,"0")}`,r=await api("POST",`/api/campaigns/${e}/discord/record/start`,{announce:s,session_id:typeof liveSession<"u"&&liveSession?.id?liveSession.id:void 0,date:o});if(_campaignStale(e))return;_enterDiscordRecActive(r.session_id,r.bot?.clock_ms||0),showToast("Recording on Discord: the bot is in your voice channel.")}catch(a){if(a.status===402){_showUpgradeModal({scenario:"cap_exhausted"});return}showToast(`${a.message||"The bot could not start recording."} Try again, or switch to In Person / Browser Audio for this session.`,9e3)}finally{t?.removeAttribute("disabled"),t&&n&&(t.textContent=n)}}const _TF_REC_ENDED_COPY={budget:"Recording stopped because this campaign's free recording time ran out. Everything up to that point is saved.",kicked:"Recording stopped because the Threadfall bot was removed from the voice channel. Everything up to that point is saved. Press Record to start again.",voice_lost:"Recording stopped because the Threadfall bot lost its connection to the voice channel and couldn't get it back. Everything up to that point is saved. Press Record to start again."};function _tfRecEndedMessage(e){return!e||!e.reason||e.reason==="gm"?null:_TF_REC_ENDED_COPY[e.reason]||"The Discord recording stopped. Everything up to that point is saved. Press Record to start again."}function _tfShowRecEndedBanner(e){const t=document.getElementById("rec-ended-banner"),n=document.getElementById("rec-ended-banner-text");if(!t||!n){showToast(e,12e3);return}n.textContent=e,t.classList.remove("hidden")}document.getElementById("rec-ended-banner-dismiss")?.addEventListener("click",()=>{document.getElementById("rec-ended-banner")?.classList.add("hidden")});function _enterDiscordRecActive(e,t,n){document.getElementById("rec-ended-banner")?.classList.add("hidden"),_tfDiscordRecActive=!0,_tfDiscordRecClockBase=Date.now()-(t||0),_loadDiscordPriorTranscript(e,!!n),document.getElementById("rec-ready")?.classList.add("hidden"),document.getElementById("rec-remote-active")?.classList.remove("hidden"),document.getElementById("live-header-rec-dot")?.classList.remove("hidden"),currentCampaign?.id&&_loadDiscordSpeakers(currentCampaign.id),_updateSpeakersTabVisibility(),_tfDiscordChunkLines=[],_watcherLastSeq=0,_voicesReset(),_startDiscordRecPoll(),_bindDiscordSession(e)}async function _bindDiscordSession(e){if(!e)return;const t=currentCampaign?.id;if(t)try{const n=await api("GET",`/api/campaigns/${t}/sessions/${e}`);if(_campaignStale(t)||!_tfDiscordRecActive)return;liveSession=n,_liveTranscriptPollLastText="",document.getElementById("live-transcript-panel")?.classList.remove("hidden"),_startTranscriptPoll(),typeof loadSessions=="function"&&loadSessions()}catch(n){console.warn("[discord-rec] session bind failed:",n.message)}}function _exitDiscordRecActive(){_tfDiscordRecActive=!1,_tfDiscordPriorHtml="",clearInterval(_tfDiscordRecTimer),_tfDiscordRecTimer=null,_tfDiscordSpeakers=[],_tfDiscordSpkBarKey="",document.getElementById("rec-remote-active")?.classList.add("hidden"),document.getElementById("live-header-rec-dot")?.classList.add("hidden"),_isGm()&&document.getElementById("rec-ready")?.classList.remove("hidden"),_updateSpeakersTabVisibility(),_renderRecModeRow()}function _startDiscordRecPoll(){clearInterval(_tfDiscordRecTimer);const e=currentCampaign?.id;let t=0,n=-1,a=Date.now();const s=async()=>{const i=document.getElementById("rec-remote-clock");if(i&&(i.textContent=_fmtRemoteClock(Date.now()-_tfDiscordRecClockBase)),liveElapsedSeconds=Math.max(0,Math.floor((Date.now()-_tfDiscordRecClockBase)/1e3)),t++%15===0)try{const o=await api("GET",`/api/campaigns/${e}/discord/record/status`);if(_campaignStale(e)||!_tfDiscordRecActive)return;if(!o.live){const l=_tfRecEndedMessage(o.ended);l?_tfShowRecEndedBanner(l):showToast("The Discord recording has ended.",6e3),_exitDiscordRecActive(),typeof loadSessions=="function"&&loadSessions();return}_tfDiscordRecClockBase=Date.now()-(o.clock_ms||0),Array.isArray(o.speakers)&&(_tfDiscordSpeakers=o.speakers,_updateSpeakersTabVisibility(),_refreshLiveSpkBar()),(o.lines??0)!==n&&(n=o.lines??0,a=Date.now());const r=Date.now()-a>10*60*1e3&&(o.clock_ms||0)>0,c=document.getElementById("rec-remote-status");c&&(c.textContent=`${o.lines||0} lines \xB7 ${(o.speakers_seen||[]).length} speakers heard \xB7 ${o.crosstalk_turns||0} crosstalk`+(o.last_error?` \xB7 warning: ${o.last_error}`:r?" \xB7 warning: no new lines in a while, check the bot is in the channel":""))}catch{}};_tfDiscordRecTimer=setInterval(s,1e3),s()}async function _stopDiscordRecording(){const e=currentCampaign?.id,t=document.getElementById("rec-remote-stop-btn");t?.setAttribute("disabled","");try{const n=await api("POST",`/api/campaigns/${e}/discord/record/stop`);showToast(`Discord recording saved: ${n.bot?.lines??0} transcript lines.`,6e3),_exitDiscordRecActive(),!_campaignStale(e)&&typeof loadSessions=="function"&&loadSessions()}catch(n){showToast(n.message||"Stop failed, try again.",6e3)}finally{t?.removeAttribute("disabled")}}document.getElementById("rec-remote-stop-btn")?.addEventListener("click",_stopDiscordRecording);let _tfDiscordSpeakers=[],_tfDiscordSpeakerMap={},_tfDiscordSpkBarKey="",_tfDiscordChunkLines=[];async function _loadDiscordPriorTranscript(e,t){_tfDiscordPriorHtml="";const n=currentCampaign?.id;if(!(!n||!e))try{const a=await api("GET",`/api/campaigns/${n}/sessions/${e}`);if(_campaignStale(n)||!_tfDiscordRecActive)return;let s=(a.raw_transcript||"").trim();if(t){const i=s.lastIndexOf("--- Recording continued ---");s=i===-1?"":s.slice(0,i).trim()}s&&(_tfDiscordPriorHtml=_buildPriorTranscriptHTML(s)+'<div class="transcript-divider">--- Recording continued ---</div>',_renderDiscordLiveTranscript())}catch{}}function _renderDiscordLiveTranscript(){const e=document.getElementById("live-transcript-box");if(!e)return;const t=_tfDiscordChunkLines.join(`
1810`),n=e.scrollHeight-e.scrollTop-e.clientHeight<32;e.innerHTML=(_tfDiscordPriorHtml||"")+_buildTranscriptHTML(t),document.getElementById("live-transcript-panel")?.classList.remove("hidden");const a=document.getElementById("live-transcript-wordcount");
1810if(a){const s=t.trim().split(/\s+/).filter(Boolean).length;a.textContent=`${s} word${s!==1?"s":""}`}n&&(e.scrollTop=e.scrollHeight),liveTranscriptAccum=t;try{_voicesAbsorbPolled(t)}catch{}}async function _loadDiscordSpeakers(e){const t=e;try{const n=await api("GET",`/api/campaigns/${t}/discord/speakers`);if(_campaignStale(t))return;_tfDiscordSpeakerMap={},_tfDiscordMutedMap={},_tfDiscordAutoMute=n.auto_mute!==!1;for(const a of n.speakers||[])_tfDiscordSpeakerMap[a.discord_user_id]=a.display_name,(a.muted===!0||a.muted===!1)&&(_tfDiscordMutedMap[a.discord_user_id]=a.muted)}catch{}}function _tfDiscordSpeakersPayload(){const e=new Set([...Object.keys(_tfDiscordSpeakerMap),...Object.keys(_tfDiscordMutedMap)]),t={};for(const n of _tfDiscordSpeakers)t[n.id]=n;return[...e].map(n=>({discord_user_id:n,display_name:_tfDiscordSpeakerMap[n]||t[n]?.discord_name||t[n]?.name||"Speaker",muted:n in _tfDiscordMutedMap?_tfDiscordMutedMap[n]:null}))}function _tfMicSvg(e){return`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><rect x="6" y="1.5" width="4" height="8" rx="2"/><path d="M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12v2.5M5.5 14.5h5"/>${e?'<path d="M2.5 2.5l11 11"/>':""}</svg>`}function _refreshDiscordSpkBar(e){if(!_tfDiscordSpeakers.length){_tfDiscordSpkBarKey!==""&&(_tfDiscordSpkBarKey="",e.innerHTML='<p class="placeholder placeholder--padded">Listening for speakers\u2026</p>');return}const t=JSON.stringify(_tfDiscordSpeakers)+"|"+JSON.stringify(_tfDiscordSpeakerMap)+"|"+JSON.stringify(_tfDiscordMutedMap)+"|"+_tfDiscordAutoMute+"|"+[..._liveAwayIds()].join(",");if(t===_tfDiscordSpkBarKey)return;_tfDiscordSpkBarKey=t;const n={map:"saved",identity:"linked account",auto:"auto-matched",discord:"Discord name"},a=_tfDiscordSpeakers.map(r=>{const c=_tfDiscordSpeakerMap[r.id]||"",l=_liveSpkPcOptions(c),d=r.discord_name||r.name,u=r.id in _tfDiscordMutedMap?_tfDiscordMutedMap[r.id]:!!r.is_bot,p=_tfDiscordAutoMute?u?"spk-mic--auto-muted":"spk-mic--auto-live":u?"spk-mic--manual-muted":"spk-mic--manual-live",m=_tfDiscordAutoMute?u?"Auto-muted (bot/source): turn Auto Mute off to change":"Recording: locked while Auto Mute is on":u?"Muted: click to resume transcribing":"Recording: click to mute (no lines, no cost)",f=[u?"muted":"",n[r.source]||"",r.heard===!1?"in channel":""].filter(Boolean).join(" \xB7 ");return`<div class="live-spk-quote-row${u?" live-spk-row--muted":""}">
1811      <button type="button" class="spk-mic-btn ${p}" data-mic-did="${escHtml(r.id)}" title="${m}" ${_tfDiscordAutoMute?'aria-disabled="true"':""}>${_tfMicSvg(u)}</button>
1812      <span class="live-spk-quote-text">${escHtml(d)} <span class="live-spk-quote-more">${f}</span></span>
1813      <select class="live-spk-inline-select" data-did="${escHtml(r.id)}">
1814        <option value="" ${c?"":"selected"}>(use Discord name)</option>
1815        ${l}
1816      </select>
1817    </div>`}).join(""),s=`<div class="spk-mute-hdr">
1818    <button type="button" id="spk-automute-btn" class="spk-automute-btn ${_tfDiscordAutoMute?"is-on":"is-off"}"
1819      title="${_tfDiscordAutoMute?"Auto Mute is ON: bots are muted automatically and the mic column is locked. Click to manage mics manually.":"Auto Mute is OFF: click any mic to toggle it. Click to re-lock, your choices are remembered; new arrivals follow the default."}">Auto Mute</button>
1820  </div>`,i=_tfDiscordSpeakers.filter(r=>!(r.id in _tfDiscordMutedMap?_tfDiscordMutedMap[r.id]:!!r.is_bot)&&!r.is_bot&&!_tfDiscordSpeakerMap[r.id]&&r.source!=="identity").length,o=i?`<p class="form-helper-text form-helper-text--warn live-spk-nudge">${i} voice${i===1?"":"s"} not matched to a character yet. Pick them below and their lines will read as the character, tonight and every session after.</p>`:"";e.innerHTML=`${s}${o}<div class="live-spk-body">${a}</div>`,document.getElementById("spk-automute-btn")?.addEventListener("click",async()=>{_tfDiscordAutoMute=!_tfDiscordAutoMute;const r=currentCampaign?.id;try{if(await api("PUT",`/api/campaigns/${r}/discord/speakers`,{speakers:_tfDiscordSpeakersPayload(),auto_mute:_tfDiscordAutoMute}),_campaignStale(r))return;showToast(_tfDiscordAutoMute?"Auto Mute on: bots muted automatically; your manual choices are remembered.":"Auto Mute off: click any mic to toggle it.",5e3)}catch(c){_tfDiscordAutoMute=!_tfDiscordAutoMute,showToast(c.message||"Could not update Auto Mute.",5e3)}_tfDiscordSpkBarKey="",_refreshLiveSpkBar()}),e.querySelectorAll(".spk-mic-btn").forEach(r=>{r.addEventListener("click",async()=>
1820{if(_tfDiscordAutoMute)return;const c=r.dataset.micDid,l=_tfDiscordSpeakers.find(m=>m.id===c),d=c in _tfDiscordMutedMap?_tfDiscordMutedMap[c]:!!l?.is_bot,u=_tfDiscordMutedMap[c];_tfDiscordMutedMap[c]=!d;const p=currentCampaign?.id;try{if(await api("PUT",`/api/campaigns/${p}/discord/speakers`,{speakers:_tfDiscordSpeakersPayload()}),_campaignStale(p))return;showToast(_tfDiscordMutedMap[c]?"Muted: the bot stops transcribing this source within a minute.":"Unmuted: transcription resumes within a minute.",5e3)}catch(m){u===void 0?delete _tfDiscordMutedMap[c]:_tfDiscordMutedMap[c]=u,showToast(m.message||"Could not update mute.",5e3)}_tfDiscordSpkBarKey="",_refreshLiveSpkBar()})}),e.querySelectorAll(".live-spk-inline-select").forEach(r=>{r.addEventListener("change",async()=>{const c=r.dataset.did,l=r.value;l?_tfDiscordSpeakerMap[c]=l:delete _tfDiscordSpeakerMap[c];const d=currentCampaign?.id;try{if(await api("PUT",`/api/campaigns/${d}/discord/speakers`,{speakers:_tfDiscordSpeakersPayload()}),_campaignStale(d))return;const u=_tfDiscordSpeakers.find(p=>p.id===c);u&&l&&(u.name=l,u.source="map"),showToast("Speaker saved: new lines use it within a minute; the full transcript updates at the next save.",5e3),_tfDiscordSpkBarKey="",_refreshLiveSpkBar()}catch(u){showToast(u.message||"Could not save speaker.",5e3)}})})}async function _resumeDiscordRecIfLive(){const e=currentCampaign?.id;if(!(!e||!_isGm()||_tfDiscordRecActive||!_tfDiscordVoiceConfigured()))try{const t=await api("GET",`/api/campaigns/${e}/discord/record/status`);if(_campaignStale(e)||!t.live||_tfDiscordRecActive)return;_enterDiscordRecActive(t.session_id,t.clock_ms||0,!0),showToast("The Discord bot is recording this campaign: reattached to the live session.",5e3)}catch{}}const _BT_NOTE_DISMISSED_KEY="tf_btmic_warning_dismissed";function _applyBtNoteDismissed(){const e=document.getElementById("rec-bt-note");e&&localStorage.getItem(_BT_NOTE_DISMISSED_KEY)==="1"&&e.classList.add("hidden")}document.addEventListener("DOMContentLoaded",_applyBtNoteDismissed),document.getElementById("rec-bt-note-close")?.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();try{localStorage.setItem(_BT_NOTE_DISMISSED_KEY,"1")}
1820catch{}document.getElementById("rec-bt-note")?.classList.add("hidden")}),document.getElementById("rec-start-btn").addEventListener("click",async()=>{if(_tfHeldRecording){showToast("A recording is still unsaved. Download or discard it first.",6e3),_tfShowHeldRecordingBanner(),_liveTel("held_recording_blocks_start",{sessionId:_tfHeldRecording.sessionId});return}if(console.log("[REC] button clicked"),!_testRecordingMode&&!await _maybeTestTransitionConfirm())return;if(!await _hasRecordingConsent()){_pendingRecStart=!0;const l=document.getElementById("recording-consent-modal");l&&l.classList.remove("hidden");return}{const l=_recModeStorageKey();l&&localStorage.setItem(l,_tfRecModeSel)}if(_tfRecModeSel==="discord"){_startDiscordRecording();return}try{const l=typeof liveSession<"u"&&liveSession?.id?`?session_id=${liveSession.id}`:"";await api("GET",`/api/campaigns/${currentCampaign.id}/quota/peek${l}`)}catch(l){if(l.status===402){_showUpgradeModal({scenario:"cap_exhausted"});return}console.warn("[REC] quota peek failed (non-402, continuing):",l.message)}typeof _hidePostStopCta=="function"&&_hidePostStopCta();const e=_tfRecModeSel==="virtual",t=document.getElementById("rec-discord-announce-chk"),n=!!(t&&!t.disabled&&t.checked),a=_discordAnnounceStorageKey();a&&t&&!t.disabled&&localStorage.setItem(a,n?"true":"false"),window._tfNextBroadcastAnnounce=n;let s=null;if(e){const l={echoCancellation:!1,noiseSuppression:!1,sampleRate:44100,suppressLocalAudioPlayback:!1};try{s=await navigator.mediaDevices.getDisplayMedia({video:!1,audio:l}).catch(()=>null),(!s||!s.getAudioTracks().length)&&(s=await navigator.mediaDevices.getDisplayMedia({video:!0,audio:l}),s.getVideoTracks().forEach(d=>d.stop())),s.getAudioTracks().length||(showToast('No system audio captured, check "Share system audio" in the picker.',7e3),s=null)}catch(d){!(navigator.mediaDevices&&navigator.mediaDevices.getDisplayMedia)||d.name==="TypeError"||d.name==="NotSupportedError"?showToast("This device cannot capture system audio. Use a computer for Browser Audio, or record through the Discord bot.",9e3):d.name!=="AbortError"&&showToast("System audio cancelled, recording mic only.",4e3),s=null}}const i={audio:{echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1,channelCount:1}};let o=null;try{console.log("[REC] requesting mic..."),o=await navigator.mediaDevices.getUserMedia(i),console.log("[REC] mic granted:",o)}catch(l){console.error("[REC] mic error:",l),s&&s.getTracks().forEach(d=>d.stop()),showToast("Microphone access denied, please allow microphone access in your browser and try again.",6e3);return}e&&s&&showToast("Tip: if your video call mic stops working, pause and restart recording to re-acquire the mic.",8e3),console.log("[REC] liveSession:",liveSession);let r=liveSession;_liveAutoCreatedSessionId=null;const c=(()=>{const l=new Date;return`${l.getFullYear()}-${String(l.getMonth()+1).padStart(2,"0")}-${String(l.getDate()).padStart(2,"0")}`})();if(!r){const l=allSessions.filter(d=>!d.is_session_zero).length+1;try{if(console.log("[REC] creating session..."),r=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions`,{title:`Session ${l}`,date:c}),allSessions=[r,...allSessions],_liveAutoCreatedSessionId=r.id,_recConsentAcknowledged&&_consentedSessionId!==r.id){try{await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${r.id}/consent`)}catch{}_consentedSessionId=r.id}await openLiveSession(r),applyRoleUI();const d=allSessions.find(p=>p.id!==r.id&&!p.is_session_zero),u=d?.date?(()=>{const p=Date.parse(c)-Date.parse(d.date.slice(0,10)),m=Math.round(p/(1e3*60*60*24));return m<=0?"":m===1?" (last session was yesterday)":m<7?` (last session was ${m} days ago)`:m<30?` (last session was ${Math.round(m/7)} week${m>=14?"s":""} ago)`:""})():"";showToast(`Started Session ${l} for today${u}.`,5e3)}catch(d){console.error("[REC] session create error:",d),o.getTracks().forEach(u=>u.stop()),s&&s.getTracks().forEach(u=>u.stop()),showToast("Failed to create session: "+d.message);return}}console.log("[REC] starting recording for session:",r);try{await startSessionRecording(r,o,s),console.log("[REC] startSessionRecording returned OK")}catch(l){console.error("[REC] startSessionRecording threw:",l),o.getTracks().forEac
1820h(d=>d.stop()),s&&s.getTracks().forEach(d=>d.stop()),showToast("Failed to start recording: "+l.message),resetRecordingUI()}});function _testDismissed(){try{return localStorage.getItem("tf_test_rec_dismissed")==="1"}catch{return!1}}async function _refreshTestAllowance(){try{_testAllowance=await api("GET","/api/test-recording/status")}catch{_testAllowance=null}const e=document.getElementById("rec-test-wrap"),t=document.getElementById("rec-test-left");if(!e)return;const n=!!(_testAllowance&&_testAllowance.eligible)&&!liveRecording&&!_testDismissed();if(e.classList.toggle("hidden",!n),n&&t){const a=Math.max(0,Math.round((_testAllowance.seconds_left||0)/60)),s=Math.max(0,_testAllowance.clips_left||0);t.textContent=`Free \xB7 ${s} test${s===1?"":"s"} \xB7 ~${a} min left`}}document.getElementById("rec-test-dismiss")?.addEventListener("click",e=>{e.stopPropagation();try{localStorage.setItem("tf_test_rec_dismissed","1")}catch{}document.getElementById("rec-test-wrap")?.classList.add("hidden"),showToast("Hidden. Bring it back anytime in Settings \u2192 Restore tips & banners.",5e3)});async function _maybeTestTransitionConfirm(){let e=!1;try{e=localStorage.getItem("tf_test_transition_seen")==="1"}catch{}if(e)return!0;if(_testAllowance===null)try{_testAllowance=await api("GET","/api/test-recording/status")}catch{}if(!_testAllowance||_testAllowance.eligible)return!0;try{localStorage.setItem("tf_test_transition_seen","1")}catch{}return await confirmDialog({title:"You've used your free test recordings",message:"Recording now works normally: when you fire a recap on it, it uses one of your session credits. Ready to record for real?",confirmLabel:"Start recording"})}function _setTestRecBadge(e){document.querySelectorAll(".rec-test-badge").forEach(t=>t.classList.toggle("hidden",!e))}document.getElementById("rec-test-btn")?.addEventListener("click",()=>{if(!_testAllowance||!_testAllowance.eligible){_refreshTestAllowance();return}_testRecordingMode=!0,_setTestRecBadge(!0);const e=(_testAllowance.clip_seconds_cap||300)*1e3;document.getElementById("rec-start-btn")?.click(),clearTimeout(_testClipTimer),_testClipTimer=setTimeout(()=>{if(_testRecordingMode&&liveRecording&&liveMediaRecorder&&liveMediaRecorder.state==="recording"){showToast("Test clip reached its limit, saving so you can see the transcription.",6e3),_userInitiatedStop=!0,livePublishing=!0;try{liveMediaRecorder.stop()}catch{}}},e)}),document.addEventListener("click",async e=>{const t=e.target.closest?.(".test-promote-btn");if(!t)return;const n=parseInt(t.dataset.sid,10);if(!(!n||!currentCampaign)&&await confirmDialog({title:"Add test recording to the session?",message:"This moves your test recording into the real session transcript so your recap will include it. It stops being a free test.",confirmLabel:"Add to session"})){t.disabled=!0,t.textContent="Adding\u2026";try{const a=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions/${n}/test-recording/promote`,{});a.session&&(allSessions=allSessions.map(s=>s.id===n?{...s,...a.session}:s)),showToast("Test recording added to the session; it will be included in your recap.",6e3)}catch(a){showToast("Could not add it: "+a.message,7e3),t.disabled=!1,t.textContent="Add to session"}}}),document.getElementById("live-create-first-session-btn").addEventListener("click",async()=>{const e=new Date().toISOString().split("T")[0];try{const t=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions`,{title:"Session 1",date:e});allSessions=[t,...allSessions],await openLiveSession(t),applyRoleUI(),showToast("Session 1 created. Click \u23FA to start recording.")}catch(t){showToast("Failed to create session: "+t.message)}}),document.getElementById("live-session-select").addEventListener("change",async e=>{const t=e.target.value;if(t==="__new__"){const s=allSessions.filter(o=>!o.is_session_zero).length+1,i=new Date().toISOString().split("T")[0];try{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/sessions`,{title:`Session ${s}`,date:i});allSessions=[o,...allSessions],await openLiveSession(o),applyRoleUI(),showToast(`Session ${s} created. Click \u23FA to start recording.`)}catch(o){showToast("Failed to create session: "+o.message)}return}const n=parseInt(t);if(!n)return;const a=allSessions.find(s=>s.id===n);a&&await openLiveSession(a)}),document.getElementById("rec-reset-timer-btn").addEventListener("click",async()=>{await confirmDialog({title:"Reset Timer",message:"Reset session timer to 0:00:00? Timestamps on 
1820new entries will restart from zero.",confirmLabel:"Reset",danger:!1})&&(liveElapsedSeconds=0,document.getElementById("rec-clock").textContent="00:00:00")}),document.getElementById("rec-discard-btn").addEventListener("click",async()=>{!liveMediaRecorder||liveMediaRecorder.state==="inactive"||await confirmDialog({title:"Discard Recording",message:"Stop and discard this recording? The session will remain but no transcript will be saved.",confirmLabel:"Discard"})&&(_userInitiatedStop=!0,livePublishing=!1,liveMediaRecorder.stop())});function _fmt$(e){return`$${(+e||0).toFixed(2)}`}function _fmtK(e){return(+e||0)>=1e3?`${(+e/1e3).toFixed(1)}k`:String(+e||0)}function _adminTab(e){document.querySelectorAll(".admin-tab").forEach(t=>t.classList.toggle("active",t.dataset.tab===e)),document.querySelectorAll(".admin-tab-panel").forEach(t=>t.classList.toggle("hidden",t.id!==`admin-tab-${e}`)),document.querySelectorAll(".adm-rail").forEach(t=>t.classList.toggle("hidden",t.dataset.tab!==e))}const _adminRailState={};function _adminRailCfg(e){const t=document.getElementById(`admin-rail-${e}`),n=document.getElementById(`admin-tab-${e}`),a=document.getElementById("admin-screen"),s=document.querySelector("#admin-screen .admin-sticky-header"),i=_adminRailState[e]||(_adminRailState[e]={pinned:null,active:null}),o=()=>s?s.getBoundingClientRect().bottom:0;return{rail:t,panel:n,scroller:a,state:i,sections:()=>n?[...n.querySelectorAll(".manage-section[id]:not(.hidden)")]:[],bandTop:o,scrollOffset:()=>o()-(a?a.getBoundingClientRect().top:0),onSelect:r=>_adminSetHash(e,r.id)}}function _adminSectionSlug(e,t){return String(t||"").replace(`admin-sec-${e}-`,"")}function _adminSetHash(e,t){const n=t?_adminSectionSlug(e,t):"";try{history.replaceState(null,"",`#admin/${e}${n?"/"+n:""}`)}catch{}}function _adminClearHash(){try{/^#admin\//.test(location.hash||"")&&history.replaceState(null,"",location.pathname+location.search)}catch{}}function _adminParseHash(){const e=/^#admin\/([a-z]+)(?:\/([a-z0-9-]+))?/.exec(location.hash||"");return!e||!document.getElementById(`admin-tab-${e[1]}`)?null:{tab:e[1],section:e[2]?`admin-sec-${e[1]}-${e[2]}`:null}}function _adminRailInit(e,t){const n=_adminRailCfg(e);_tfRailInit(n);const a=t&&document.getElementById(t);a?_tfRailScrollTo(n,a,"auto"):(n.state.pinned=null,_tfRailMarkActive(n))}async function _adminHandleHash(){return!currentUser?.is_admin||!/^#admin\//.test(location.hash||"")?!1:(await openAdminPanel(),!0)}async function _adminSwitchTab(e,t){_adminTab(e);const n=t&&t.section;_adminSetHash(e,n),_adminRailInit(e,null);try{if(e==="pulse"&&await adminLoadPulse(),e==="people"){await adminLoadSignupQueue(),await adminLoadUsers(),await adminLoadCampaigns();const a=document.getElementById("admin-ai-usage-btn");a&&(a.onclick=()=>adminLoadAiUsage())}if(e==="growth"){const s=(await Promise.allSettled([adminLoadLifecycle(),adminLoadUsage(),adminLoadTrends(),adminLoadCosts(),adminLoadSearchConsole()])).find(i=>i.status==="rejected");if(s)throw s.reason}e==="ops"&&(await adminLoadAffiliates(),await adminLoadComms(),await adminLoadReleaseNotes())}catch(a){showToast("Tab load error: "+(a&&a.message||a),5e3)}_adminRailInit(e,n)}window._adminSwitchTab=_adminSwitchTab;async function openAdminPanel(){["auth-screen","campaign-selector","main-app"].forEach(o=>{document.getElementById(o)?.classList.add("hidden")}),_adminPulseState=2;const e=document.getElementById("admin-pulse-panel");e&&(e.dataset.state=2);const t=document.getElementById("admin-pulse-collapse-btn");t&&(t.textContent="");const n=document.getElementById("admin-screen");n&&(n.style.display="flex");const a=document.getElementById("admin-user-email");a&&(a.textContent=currentUser?.email||"");const s=document.getElementById("admin-readonly-badge");if(s){const o=(()=>{try{return!!localStorage.getItem("tf_admin_token")}catch{return!1}})();s.classList.toggle("hidden",o)}n&&!n.dataset.hintWired&&(n.dataset.hintWired="1",n.addEventListener("click",_admHintClickHandler));const i=_adminParseHash();await _adminSwitchTab(i?i.tab:"pulse",i&&i.section?{section:i.section}:void 0)}const _TIER_LABEL={founding_gm:"Founding",hobby:"Hobby",standard:"Standard",master:"Master",pro:"Pro"};function _pulseTierPills(e){if(!e||!e.length)return'<span class="pulse-tier-pill pulse-tier-pill--empty">No paid subs yet</span>';const t=["founding_gm","hobby","standard","master","pro"];return[...e].sort((a,s)=>t.indexOf(a.tier)-t.indexOf(s.tier)).map(a=>{const s=_TIER_LABEL[a.tier]||a.tier;return`<span class="pulse-tier-pill pulse-tier-pill--${a.tier}">${a.count} ${s}</span>`}).join("")}const _PULSE_FUNNEL_STAGES={gm:[{key:"new_gms",label:"New GMs",drill:"funnel_gm_no_campaign",drillLabel:"Paid GMs who haven't created a campaign"},{key:"first_campaign",label:"1st Campaign",drill:"funnel_gm_no_session",drillLabel:"GMs with a campaign but no session"},{key:"first_session",label:"1st Session",drill:"funnel_gm_no_recap",drillLabel:"GMs with a session but no recap"},{key:"first_recap",label:"1st Recap",drill:"funnel_gm_no_paid",drillLabel:"GMs with a recap who haven't paid"},{key:"paid",label:"Paid",drill:null}],player:[{key:"signups",label:"Signups",drill:"funnel_player_no_campaign",drillLabel:"Player signups not in any campaign"},{key:"joined_campaign",label:"Joined a campaign",drill:"funnel_player_no_session",drillLabel:"Players in a campaign with no session yet"},{key:"attended_session",label:"Attended a session",drill:null}]};function _pulseFunnelBars(e,t){const n=e[t[0].key]||1;return`
1821    <div class="pulse-funnel">
1822      ${t.map((a,s)=>{const i=e[a.key]||0,o=n>0?Math.round(i/n*100):0,r=s>0?e[t[s-1].key]||0:null,c=r!==null&&r>0?Math.round(i/r*100):null;return`
1823          <div${a.drill?` data-drill="${a.drill}" title="${a.drillLabel}" class="pulse-funnel-stage pulse-funnel-stage--drill"`:' class="pulse-funnel-stage"'}>
1824            <div class="pulse-funnel-label">${a.label}</div>
1825            <div class="pulse-funnel-bar-wrap">
1826              <div class="pulse-funnel-bar-fill" style="width:${o}%"></div>
1827              <div class="pulse-funnel-bar-count">${i}</div>
1828            </div>
1829            ${c!==null?`<div class="pulse-funnel-conv">${c}%</div>`:'<div class="pulse-funnel-conv"></div>'}
1830          </div>`}).join("")}
1831    </div>`}const _SIGNUP_SOURCE_LABELS={fellow_gm:"A fellow GM",friend:"A friend or player",discord:"Discord",facebook:"Facebook",reddit:"Reddit",twitter:"Twitter / X",youtube:"YouTube",search:"Search engine",other:"Other",untagged:"Untagged (pre-tagging)"};function _pulseHealthDot(e){return`<span class="pulse-health-dot pulse-health-dot--${e}"></span>`}function _pulseRelativeTime(e){if(!e)return"never";const t=Date.now()-new Date(e).getTime(),n=Math.round(t/6e4);return n<1?"just now":n<60?`${n}m ago`:n<1440?`${Math.round(n/60)}h ago`:`${Math.round(n/1440)}d ago`}
1831function _ckPromiseKind(e){return{no_recap:"no recap",empty_recap:"empty recap",transcript_tail_gap:"tail gap",stale_cron:"stale cron",webhook_error:"webhook"}[e]||(e?String(e).replace(/_/g," "):"unknown")}function _ckErrOf(e){return e==null?"No data returned for this tile.":typeof e=="object"&&!Array.isArray(e)&&e.error?String(e.error):null}function _ckErrBody(e){return`<p class="placeholder--padded error">${escHtml(e)}</p>`}function _ckEmpty(e){return`<p class="placeholder--padded">${escHtml(e)}</p>`}function _ckSeq(){return _ckSeq.n=(_ckSeq.n||0)+1,`ck${_ckSeq.n}`}function _ckAllErr(e){const t=e&&e.message||"Cockpit endpoint unavailable",n={};return["broken_promises","first_fortnight","quitters","money_leaks","cleanup","search","same_night","reader_reach","codex_consulted","cost_trend","signup_queue","people"].forEach(a=>{n[a]={error:t}}),n}function _ckTile(e,t,n,a,s,i){return`<section class="pulse-card pulse-card--full ck-tile" id="ck-${escHtml(String(e))}">
1832      <div class="ck-tile-hdr">
1833        <span class="pulse-card-label form-section-label ck-tile-name">${escHtml(t)}</span>
1834        <span class="ck-tile-ctl">
1835          <span class="ck-badge ck-badge--${a==="alarm"?"alarm":"gauge"}">${a==="alarm"?"ALARM":"GAUGE"}</span>
1836          ${s?'<button type="button" class="ck-hint-btn" data-ck-hint aria-expanded="false" title="How to read this tile">?</button>':""}
1837        </span>
1838      </div>
1839      <div class="ck-tile-q">${escHtml(n)}</div>
1840      ${s?`<div class="ck-tile-hint hidden">${escHtml(s)}</div>`:""}
1841      <div class="ck-tile-body">${i}</div>
1842    </section>`}function _admSec(e,t,n,a){return`<section class="manage-section" id="admin-sec-${e}-${t}"><div class="manage-section-hdr"><span class="form-section-label">${escHtml(n)}</span></div><p class="form-helper-text">${escHtml(a)}</p>`}function _admSecEnd(){return"</section>"}function _admHintClickHandler(e){const t=e.target.closest("[data-ck-hint]");if(!t)return;const n=t.closest(".ck-tile"),a=n&&n.querySelector(".ck-tile-hint");if(!a)return;const s=!a.classList.toggle("hidden");t.setAttribute("aria-expanded",s?"true":"false")}function _ckCollapseClear(e){e.querySelectorAll(".ck-tile").forEach(t=>{const n=t.querySelector(".ck-badge--alarm"),a=t.querySelector(".ck-tile-body");if(!n||!a)return;const s=a.querySelector(".ck-search .ck-state-word--healthy"),i=s?a.querySelector(".ck-page-list, .ck-acked")?null:s:a.querySelector(".placeholder--padded:not(.error)"),o=s?null:a.querySelector("table, ul:not(.ck-acked-list), ol, svg, .ck-sparkwrap");!i||o||(t.classList.add("ck-tile--clear"),t.querySelector(".ck-hint-btn")?.classList.add("hidden"),n.textContent="CLEAR",n.classList.remove("ck-badge--alarm"),n.classList.add("ck-badge--clear"),i.classList.add("ck-clear-msg"))})}function _ckRailDots(e){const t=document.getElementById("admin-rail-pulse");t&&e.querySelectorAll(".manage-section[id]").forEach(n=>{let a=0,s=0,i=0;n.querySelectorAll(".ck-tile").forEach(c=>{c.querySelector(".ck-tile-body .placeholder--padded.error")?s++:c.querySelector(".ck-badge--alarm")?a++:c.querySelector(".ck-badge--clear")&&i++});const o=a?"alarm":s?"watch":i?"clear":null,r=a?`${a} alarm${a===1?"":"s"}`:s?"A tile could not load":i?"Clear":"";_tfRailDot(t,n.id,o,r)})}function _ckCanWrite(){try{return!!localStorage.getItem("tf_admin_token")}catch{return!1}}function _ckAckCtrl(e,t){return e?t?`<span class="ck-ack" data-ack-key="${escHtml(e)}">
1843      <button type="button" class="btn-new btn-xs ck-ack-btn" data-ck-ack="open">Acknowledge</button>
1844      <span class="ck-ack-form hidden">
1845        <input type="text" class="form-input ck-ack-input" maxlength="240" placeholder="Why this one is fine">
1846        <button type="button" class="btn-new btn-xs" data-ck-ack="save">Save</button>
1847        <button type="button" class="btn-ghost btn-xs" data-ck-ack="cancel">Cancel</button>
1848      </span>
1849    </span>`:'<span class="ck-ack"><button type="button" class="btn-new btn-xs ck-ack-btn" disabled title="Read-only on this device">Acknowledge</button></span>':""}function _ckAckedFooter(e,t,n){if(!e||!e.length)return"";const a=_ckSeq();return`<div class="ck-acked">
1850      <button type="button" class="ck-acked-toggle" data-ck-ack="toggle" data-target="${a}">${e.length} acknowledged \xB7 show</button>
1851      <ul class="ck-acked-list hidden" id="${a}">${e.map(s=>{const i=s.acked||{};return`<li class="ck-acked-row">
1852            <span class="ck-acked-l">${t(s)}</span>
1853            <span class="ck-muted">${escHtml(i.note||"no note")} \xB7 ${escHtml(_pulseRelativeTime(i.acked_at))}${i.acked_by?` \xB7 ${escHtml(i.acked_by)}`:""}</span>
1854            ${n&&s.ack_key?`<button type="button" class="ck-unack" data-ck-ack="unack" data-ack-key="${escHtml(s.ack_key)}">Un-acknowledge</button>`:""}
1855          </li>`}).join("")}</ul>
1856    </div>`}function _ckSplitAcked(e){const t=Array.isArray(e)?e:[];return{live:t.filter(n=>!(n&&n.acked)),acked:t.filter(n=>n&&n.acked)}}async function _ckAckClickHandler(e){const t=e.target.closest("[data-ck-ack]");if(!t)return;const n=t.getAttribute("data-ck-ack");if(n==="toggle"){const s=document.getElementById(t.getAttribute("data-target")||"");if(!s)return;const i=s.classList.toggle("hidden");t.textContent=t.textContent.replace(i?"hide":"show",i?"show":"hide");return}const a=t.closest(".ck-ack");if(n==="open"){if(!a)return;a.querySelector(".ck-ack-btn")?.classList.add("hidden");const s=a.querySelector(".ck-ack-form");s?.classList.remove("hidden"),s?.querySelector("input")?.focus();return}if(n==="cancel"){if(!a)return;a.querySelector(".ck-ack-form")?.classList.add("hidden"),a.querySelector(".ck-ack-btn")?.classList.remove("hidden");return}if(n==="save"){const s=a&&a.getAttribute("data-ack-key");if(!s)return;const i=((a.querySelector("input")||{}).value||"").trim();t.disabled=!0,t.textContent="Saving\u2026";try{await api("POST","/api/admin/cockpit/ack",{ack_key:s,note:i}),showToast("Acknowledged",3e3),await adminLoadPulse()}catch(o){showToast("Acknowledge failed: "+o.message,5e3),t.disabled=!1,t.textContent="Save"}return}if(n==="unack"){const s=t.getAttribute("data-ack-key");if(!s)return;t.disabled=!0;try{await api("DELETE","/api/admin/cockpit/ack/"+encodeURIComponent(s)),showToast("Un-acknowledged",3e3),await adminLoadPulse()}catch(i){showToast("Un-acknowledge failed: "+i.message,5e3),t.disabled=!1}}}function _ckAckKeyHandler(e){if(e.key!=="Enter")return;const t=e.target.closest(".ck-ack-input");t&&(e.preventDefault(),t.closest(".ck-ack")?.querySelector('[data-ck-ack="save"]')?.click())}function _ckSpark(e,t,n,a){const s=Array.isArray(e)?e.map(b=>Number(b)||0):[],i=s.length;if(!i)return'<span class="ck-spark-empty">no history yet</span>';const o=a||(b=>String(b)),r=200,c=44,l=5,d=r-l*2,u=c-l*2,p=Math.max.apply(null,s.concat([0]))||1,m=b=>escHtml(`${(t||[])[b]||""} ${o(s[b])}`.trim());if(n==="bar"){const b=d/i,y=s.map((_,w)=>{const $=Math.max(1,u*(_/p));return`<rect class="ck-spark-bar" x="${(l+b*w+b*.18).toFixed(1)}" y="${(l+u-$).toFixed(1)}" width="${(b*.64).toFixed(1)}" height="${$.toFixed(1)}"><title>${m(w)}</title></rect>`}).join("");return`<svg class="trends-chart ck-spark" viewBox="0 0 ${r} ${c}" role="img" aria-label="eight-week trend">${y}</svg>`}const f=s.map((b,y)=>i===1?l+d/2:l+d*(y/(i-1))),g=s.map(b=>l+u*(1-b/p)),h=f.map((b,y)=>`${y?"L":"M"}${b.toFixed(1)} ${g[y].toFixed(1)}`).join(" "),v=f.map((b,y)=>`<circle class="trends-dot" cx="${b.toFixed(1)}" cy="${g[y].toFixed(1)}" r="2.4"><title>${m(y)}</title></circle>`).join("");return`<svg class="trends-chart ck-spark" viewBox="0 0 ${r} ${c}" role="img" aria-label="eight-week trend"><path class="trends-line trends-line--teal" d="${h}"/>${v}</svg>`}function _ckWeekLabel(e){if(!e)return"";const t=new Date(e);return isNaN(t.getTime())?String(e):`${t.getMonth()+1}/${t.getDate()}`}function _ckSparkEnds(e){if(!Array.isArray(e)||!e.length)return"";const t=_ckWeekLabel(e[0].week_start),n=_ckWeekLabel(e[e.length-1].week_start);return`<div class="ck-sparklabels"><span>${escHtml(t)}</span><span>${escHtml(n)}</span></div>`}function _ckWhen(e){return e?`<span title="${escHtml(String(e))}">${escHtml(_pulseRelativeTime(e))}</span>`:'<span class="ck-muted">\u2014</span>'}function _ckWhenFuture(e){if(!e)return'<span class="ck-muted">\u2014</span>';const t=new Date(e).getTime();if(isNaN(t))return'<span class="ck-muted">\u2014</span>';const n=Math.round((t-Date.now())/864e5),a=n>1?`in ${n}d`:n===1?"tomorrow":n===0?"today":`${Math.abs(n)}d ago`;return`<span title="${escHtml(String(e))}">${escHtml(a)}</span>`}function _ckMin(e){if(e==null||e==="")return"\u2014";const t=Number(e);return isNaN(t)?"\u2014":`${t.toFixed(0)}m`}function _ckPctOfFraction(e){if(e==null||e==="")return null;const t=Number(e);return isNaN(t)?null:t*100}function _ckPctOfHundred(e){if(e==null||e==="")return null;const t=Number(e);return isNaN(t)?null:t}function _ckCost(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=Array.isArray(e.weeks)?e.weeks:[],s=e.latest_week_usd===null||e.latest_week_usd===void 0?null:Number(e.latest_week_usd);if(!a.length&&s===null)return _ckEmpty("No live sessions in the last 8 weeks.");const i=e.trailing4_mean_usd===null||e.trailing4_mean_usd===void 0?null:Number(e.trailing4_mean_usd),o=s!==null&&i?(s-i)/i*100:null,r=a.map(u=>Number(u.avg_cost_usd)||0),c=a.map(u=>`${_ckWeekLabel(u.week_start)} \xB7 ${Number(u.live_sessions)||0} live`),l=Array.isArray(e.top3)?e.top3:[],d=e.acked?[e]:[];return`<div class="ck-cost">
1857      <div class="ck-bignum">
1858        <span class="pulse-card-value">${s===null?"\u2014":escHtml(_fmt$(s))}</span>
1859        <span class="pulse-stat-l">latest complete week, per live session</span>
1860        ${o===null?"":`<span class="ck-delta ${o>0?"ck-delta--bad":o<0?"ck-delta--good":"ck-delta--flat"}">${o>0?"\u25B2":o<0?"\u25BC":"\xB1"}${escHtml(Math.abs(o).toFixed(0))}% vs 4-week mean${i===null?"":` ${escHtml(_fmt$(i))}`}</span>`}
1861        ${e.alarm&&!e.acked?`<span class="admin-pill admin-pill--blocked">ALARM</span>${_ckAckCtrl(e.ack_key,n)}`:""}
1862      </div>
1863      <div class="ck-sparkwrap">${_ckSpark(r,c,"line",u=>_fmt$(u))}${_ckSparkEnds(a)}</div>
1864      <div class="ck-sub">
1865        <span class="form-section-label">Three most expensive sessions, latest week</span>
1866        ${l.length?`<ul class="ck-plain-list">${l.map(u=>`<li><span class="ck-ff-name">${escHtml(u.campaign_name||"\u2014")}</span> <span class="ck-muted">${escHtml(_fmt$(u.cost_usd))} \xB7 ${escHtml(_pulseRelativeTime(u.created_at))}</span></li>`).join("")}</ul>`:'<p class="placeholder--padded">No live sessions in the latest week.</p>'}
1867      </div>
1868      ${_ckAckedFooter(d,()=>"Cost alarm",n)}
1869    </div>`}function _ckBrokenPromises(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=_ckSplitAcked(e.rows),s=d=>escHtml(`${_ckPromiseKind(d.kind)} \xB7 ${d.campaign_name||"no campaign"} \xB7 ${d.session_title||"no session"}`);if(!a.live.length)return _ckEmpty("No broken promises.")+_ckAckedFooter(a.acked,s,n);const i=d=>!!(d.first_session||d.gm_first_30d),o=a.live.filter(i),r=a.live.filter(d=>!i(d)),c=d=>`<div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1870        <thead><tr><th>Cause</th><th>Campaign</th><th>GM</th><th>Session</th><th>What broke</th><th>Since</th><th></th></tr></thead>
1871        <tbody>${d.map(u=>`<tr>
1872          <td>
1872<span class="admin-pill admin-pill--blocked">${escHtml(_ckPromiseKind(u.kind))}</span></td>
1873          <td>${escHtml(u.campaign_name||"\u2014")}</td>
1874          <td>${escHtml(u.gm_email||"\u2014")}</td>
1875          <td>${escHtml(u.session_title||"\u2014")}</td>
1876          <td>${escHtml(u.detail||"")}</td>
1877          <td>${_ckWhen(u.since)}</td>
1878          <td>${_ckAckCtrl(u.ack_key,n)}</td>
1879        </tr>`).join("")}</tbody></table></div>`;let l="";return o.length&&(l+=`<div class="ck-hotblock">
1880        <span class="form-section-label">First session, or a GM inside their first 30 days</span>
1881        ${c(o)}
1882      </div>`),r.length&&(l+=`${o.length?'<span class="form-section-label ck-subhdr">Everything else</span>':""}${c(r)}`),l+_ckAckedFooter(a.acked,s,n)}function _ckChannelChip(e){const t=e&&e.channel?String(e.channel):"unknown",n=t.toLowerCase()==="unknown",a=[e&&e.channel_source,e&&e.channel_detail].filter(Boolean),s=a.length?a.join(": "):"no first-touch data recorded for this account";return`<span class="admin-pill ${n?"admin-pill--muted":"admin-pill--teal"} ck-chan" title="${escHtml(s)}">${escHtml(t)}</span>`}function _ckByChannelLine(e){const t=e&&!_ckErrOf(e)?e:null,n=t&&t.by_channel_28d;if(!n||typeof n!="object")return"";const a=Object.keys(n).map(s=>[s,Number(n[s])||0]).filter(s=>s[1]>0).sort((s,i)=>i[1]-s[1]||s[0].localeCompare(i[0]));return a.length?`<div class="ck-chan-line ck-muted">By channel, last 28 days: ${escHtml(a.map(s=>`${s[0]} ${s[1]}`).join(" \xB7 "))}</div>`:""}function _ckStepChips(e,t){const n=e||{},s=(t||{}).players_read||{},i=s.read!==void 0||s.joined_after!==void 0?` (${Number(s.read)||0} read \xB7 ${Number(s.joined_after)||0} joined after)`:"",o=[["recorded","Recorded",n.recorded,""],["same_night","Recap posted same night",n.same_night,""],["players_read","Players came in",n.players_read,i],["came_back","Came back",n.came_back,""]];let r=!1;return o.map(c=>{const l=c[1],d=c[2],u=c[3];return r?`<span class="ck-chip ck-chip--after">${escHtml(l)} \u2014</span>`:d===!0?`<span class="ck-chip ck-chip--done">${escHtml(l)}${escHtml(u)} \u2713</span>`:d==null?`<span class="ck-chip ck-chip--unknown">${escHtml(l)}${escHtml(u)}: ${escHtml(s.pending?"still counting (window open)":"not measured yet")}</span>`:(r=!0,`<span class="ck-chip ck-chip--stall">${escHtml(l)}${escHtml(u)} \u2715 <span class="ck-muted">stalled here</span></span>`)}).join("")}function _ckFirstFortnight(e,t){const n=_ckErrOf(e);if(n)return _ckErrBody(n);const a=e.header||{},s=Number(a.independent_28d)||0,i=Number(a.independent_prior_28d)||0,o=s-i,r=Array.isArray(a.weekly)?a.weekly:[],c=`<div class="ck-ff-head">
1883      <div class="ck-bignum">
1884        <span class="pulse-card-value">${s}</span>
1885        <span class="pulse-stat-l">independent signups, last 28 days</span>
1886      </div>
1887      <div class="ck-delta ${o>0?"ck-delta--good":o<0?"ck-delta--bad":"ck-delta--flat"}">${o>0?"\u25B2":o<0?"\u25BC":"\xB1"}${Math.abs(o)} vs the 28 days before (${i})</div>
1888      <div class="ck-sparkwrap">${_ckSpark(r.map(h=>h.n),r.map(h=>_ckWeekLabel(h.week_start)),"bar")}${_ckSparkEnds(r)}</div>
1889    </div>${_ckByChannelLine(t)}`,l=Array.isArray(e.rows)?e.rows:[],d=l.length?`<ul class="ck-ff-list">${l.map(h=>{const v=[h.campaign_name?h.campaign_name:"no campaign yet",h.next_session_at?`next session ${new Date(h.next_session_at).toLocaleDateString()}`:""].filter(Boolean).join(" \xB7 ");return`<li class="ck-ff-row">
1890            <div class="ck-ff-who">
1891              <span class="ck-ff-name">${escHtml(h.display_name||h.email||"\u2014")} ${_ckChannelChip(h)}</span>
1892              <span class="ck-muted">${escHtml(h.email||"")} \xB7 signed up ${escHtml(_pulseRelativeTime(h.signed_up))}</span>
1893              <span class="ck-muted">${escHtml(v)}</span>
1894            </div>
1895            <div class="ck-boxes">${_ckStepChips(h.boxes,h.boxes_detail)}</div>
1896          </li>`}).join("")}</ul>`:_ckEmpty("No independent signups in the last 28 days."),u=e.invited_28d||{},p=Number(u.n)||0,f=(Array.isArray(u.by_inviter)?u.by_inviter:[]).map(h=>h.inviter_email).filter(Boolean),g=p?`<div class="ck-ff-foot ck-muted">${escHtml(`${p}
1896 more signups this month were invited into existing tables${f.length?` (by ${f.join(", ")})`:""} \u2014 invited players rarely become GMs, 2 of 73 so far.`)}</div>`:"";return c+d+g}function _ckQuitters(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=_ckSplitAcked(e.rows),s=o=>escHtml(`${o.campaign_name||"no campaign"} \xB7 ${o.gm_email||"no GM"}`);return a.live.length?`<div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1897      <thead><tr><th>Campaign</th><th>GM</th><th>State</th><th>Gap</th><th>Next session</th><th>Recent activity</th><th></th></tr></thead>
1898      <tbody>${a.live.slice().sort((o,r)=>(r.paying?1:0)-(o.paying?1:0)||(Number(r.days_since_last_live)||0)-(Number(o.days_since_last_live)||0)).map(o=>{const r=o.state==="dropped_product",c=r?"Message them: ask whether recording gave them trouble.":"Message them: ask whether there is a session this week.",l=o.median_gap_days===null||o.median_gap_days===void 0?null:Number(o.median_gap_days),d=o.days_since_last_live===null||o.days_since_last_live===void 0?"\u2014":`${o.days_since_last_live}d`;return`<tr class="${o.paying?"ck-row--paying":""}" title="${escHtml(c)}">
1899          <td>${escHtml(o.campaign_name||"\u2014")}</td>
1900          <td>${escHtml(o.gm_email||"\u2014")}
1900${o.paying?` <span class="admin-pill admin-pill--gold">${escHtml(o.tier||"paying")}</span>`:""}</td>
1901          <td><span class="admin-pill ${r?"admin-pill--blocked":"admin-pill--warn"}">${r?"still playing, stopped recording":"table gone quiet"}</span></td>
1902          <td>${escHtml(d)}${l?` <span class="ck-muted">vs ${escHtml(l.toFixed(0))}d median</span>`:""}</td>
1903          <td>${_ckWhenFuture(o.next_session_at)}</td>
1904          <td>${escHtml(o.recent_activity||"\u2014")}</td>
1905          <td>${_ckAckCtrl(o.ack_key,n)}</td>
1906        </tr>`}).join("")}</tbody></table></div>${_ckAckedFooter(a.acked,s,n)}`:_ckEmpty("No table has stopped recording.")+_ckAckedFooter(a.acked,s,n)}function _ckMoneyLeaks(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=_ckSplitAcked(e.cancellations),s=_ckSplitAcked(e.unpaid_subscription_checkouts),i=Array.isArray(e.margin)?e.margin:[],o=_ckSplitAcked(i);if(!a.live.length&&!a.acked.length&&!s.live.length&&!s.acked.length&&!i.length)return _ckEmpty("No leaks.");const r=(p,m)=>`<div class="ck-sub"><span class="form-section-label">${escHtml(p)}</span>${m}</div>`,c=p=>`<p class="placeholder--padded">${escHtml(p)}</p>`,l=a.live.length?`<ul class="ck-plain-list">${a.live.map(p=>`<li><span class="ck-ff-name">${escHtml(p.email||"\u2014")}</span> <span class="ck-muted">${escHtml(p.tier||"")} \xB7 ${escHtml(p.kind||"cancelled")} \xB7 ${escHtml(_pulseRelativeTime(p.at))}</span> ${_ckAckCtrl(p.ack_key,n)}</li>`).join("")}</ul>`:c("No cancellations."),d=s.live.length?`<ul class="ck-plain-list">${s.live.map(p=>`<li><span class="ck-ff-name">${escHtml(p.email||"\u2014")}</span> <span class="ck-muted">checkout ${escHtml(_pulseRelativeTime(p.at))}, no invoice \xB7 ${escHtml(p.event_id||"")}</span> ${_ckAckCtrl(p.ack_key,n)}</li>`).join("")}</ul>`:c("Every completed checkout was invoiced."),u=o.live.length?`<div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1907        <thead><tr><th>Account</th><th>Tier</th><th>Cost \xF7 what they pay</th><th>Cost 90d</th><th>Paid 90d, net</th><th>Live sessions 90d</th><th></th></tr></thead>
1908        <tbody>${o.live.map(p=>{const m=p.level==="red"?"admin-pill--blocked":p.level==="amber"?"admin-pill--warn":"admin-pill--muted",f=p.level==="comped";return`<tr class="${p.level==="ok"||f?"ck-row--muted":""}">
1909            <td>${escHtml(p.email||"\u2014")}</td>
1910            <td>${escHtml(p.tier||"\u2014")}${f?' <span class="ck-muted">comped</span>':""}</td>
1911            <td>
1911${f?'<span class="admin-pill admin-pill--muted" title="A comped seat pays nothing, so there is no ratio \u2014 this is the cost it carries">no revenue</span>':`<span class="admin-pill ${m}">${escHtml((Number(p.ratio)||0).toFixed(2))}\xD7</span>`}</td>
1912            <td>${escHtml(_fmt$(p.cost_90d_usd))}</td>
1913            <td>${escHtml(_fmt$(p.net_rev_90d_usd))}</td>
1914            <td>${escHtml(String(p.live_sessions_90d===null||p.live_sessions_90d===void 0?"\u2014":p.live_sessions_90d))}</td>
1915            <td>${_ckAckCtrl(p.ack_key,n)}</td>
1916          </tr>`}).join("")}</tbody></table></div>`:c("No paying accounts yet.");return r("Cancellations, last 7 days",l+_ckAckedFooter(a.acked,p=>escHtml(p.email||"account"),n))+r("Checkouts that took money and were never invoiced, last 90 days",d+_ckAckedFooter(s.acked,p=>escHtml(p.email||"account"),n))+r("Every paying account \u2014 cost vs what it pays, 90 days",u+_ckAckedFooter(o.acked,p=>escHtml(p.email||"account"),n))}function _ckCleanup(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=_ckSplitAcked(e.rows),s=o=>escHtml(o.gm_email||"GM");return a.live.length?`<ul class="ck-clean-list">${a.live.slice().sort((o,r)=>(r.rising?1:0)-(o.rising?1:0)).map(o=>{const r=Array.isArray(o.weekly)?o.weekly:[],c=r.map(u=>Number(u.per_session)||0),l=c.length?c[c.length-1]:null,d=Array.isArray(o.campaigns)?o.campaigns:[];return`<li class="ck-clean-row">
1917          <div class="ck-clean-who">
1918            <span class="ck-ff-name">${escHtml(o.gm_email||"\u2014")}</span>
1919            <span class="ck-muted">${escHtml(d.length?d.join(" \xB7 "):"no campaigns")}</span>
1920          </div>
1921          <div class="ck-sparkwrap">${_ckSpark(c,r.map(u=>`${_ckWeekLabel(u.week_start)} \xB7 ${Number(u.merges)||0} merges / ${Number(u.live_sessions)||0} live`),"line",u=>u.toFixed(2))}${_ckSparkEnds(r)}</div>
1922          <div class="ck-clean-val">
1923            <span class="pulse-stat-n">${l===null?"\u2014":escHtml(l.toFixed(2))}</span>
1924            <span class="pulse-stat-l">merges / session</span>
1925          </div>
1926          <div class="ck-clean-flag">
1926${o.rising?'<span class="admin-pill admin-pill--warn">rising</span>':""}${_ckAckCtrl(o.ack_key,n)}</div>
1927        </li>`}).join("")}</ul>${_ckAckedFooter(a.acked,s,n)}`:_ckEmpty("No merges recorded in the last 8 weeks.")+_ckAckedFooter(a.acked,s,n)}function _ckSearchStateWord(e){return{healthy:"Healthy",watch:"Watch",regression:"Regression",not_instrumented:"Not instrumented",measured:"Measured"}[e]||(e?String(e).replace(/_/g," "):"Unknown")}function _ckSearch(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=_ckCanWrite(),a=e.state||"not_instrumented",s=e.signals||{},i=(o,r)=>{const c=r&&r.state||"not_instrumented";let l=r&&r.detail||"";!l&&r&&(r.hours!==null&&r.hours!==void 0?l=`last pull ${Number(r.hours).toFixed(0)}h ago`:r.last_pull_at?l=`last pull ${_pulseRelativeTime(r.last_pull_at)}`:r.submit_failed_at?l=`last submit failed ${_pulseRelativeTime(r.submit_failed_at)}`:r.last_read_at?l=`last read by Google ${_pulseRelativeTime(r.last_read_at)}`:r&&("hours"in r||"last_pull_at"in r)?l="no pull on record":r&&"last_read_at"in r&&(l="never read by Google"));const d=r&&Array.isArray(r.rows)&&r.rows.length?r.rows:r&&Array.isArray(r.pages)?r.pages.map(f=>f&&typeof f=="object"?f:{path:String(f)}):[],u=d.filter(f=>!f.acked),p=u.length?`<ul class="ck-plain-list ck-page-list">${u.map(f=>`<li>${escHtml(f.path||f.page||f.url||"\u2014")} ${_ckAckCtrl(f.ack_key,n)}</li>`).join("")}</ul>`:"",m=_ckAckedFooter(d.filter(f=>f.acked),f=>escHtml(f.path||f.page||f.url||"page"),n);return`<li class="ck-sig">
1928        <span class="ck-state-dot ck-state-dot--${escHtml(c)}"></span>
1929        <span class="ck-sig-l">${escHtml(o)}</span>
1930        <span class="ck-muted">${escHtml(l)||"\u2014"}</span>
1931        ${r&&r.ack_key&&!r.acked?_ckAckCtrl(r.ack_key,n):""}
1932        ${p}
1933        ${m}
1934      </li>`};return`<div class="ck-search">
1935      <div class="ck-state-word ck-state-word--${escHtml(a)}">${escHtml(_ckSearchStateWord(a))}</div>
1936      <ul class="ck-sig-list">
1937        ${i("Google index",s.index_drop)}
1938        ${i("Search Console pull",s.cron_fresh)}
1939        ${i("Sitemap",s.sitemap)}
1940      </ul>
1941    </div>`}function _ckSameNight(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);const n=Number(e.n)||0;if(!n)return _ckEmpty(e.note||"Nothing measured yet \u2014 needs a session with a Generate click and a Discord post.");const a=e.median_click_to_post_min===null||e.median_click_to_post_min===void 0?null:Number(e.median_click_to_post_min),s=_ckPctOfFraction(e.pct_click_to_post_within_90),i=e.by_capture_mode||{},o=Object.keys(i).map(l=>{const d=i[l]||{},u=d.median_click_to_post_min===null||d.median_click_to_post_min===void 0?null:Number(d.median_click_to_post_min);return`<div>
1942        <span class="pulse-stat-n${u!==null&&u<60?" ck-ok":""}">${escHtml(_ckMin(u))}</span>
1943        <span class="pulse-stat-l">${escHtml(l)} \xB7 n=${Number(d.n)||0}</span>
1944      </div>`}).join(""),r=Array.isArray(e.sessions)?e.sessions:[],c=r.length?`<div class="ck-sub">
1945        <span class="form-section-label">Last ${r.length} sessions</span>
1946        <div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1947          <thead><tr><th>Campaign</th><th>Generate \u2192 ready</th><th>Ready \u2192 post</th><th>Total</th></tr></thead>
1948          <tbody>${r.map(l=>`<tr>
1949            <td>${escHtml(l.campaign_name||"\u2014")}</td>
1950            <td>${escHtml(_ckMin(l.click_to_ready_min))}</td>
1951            <td>${escHtml(_ckMin(l.ready_to_post_min))}</td>
1952            <td>${escHtml(_ckMin(l.click_to_post_min))}</td>
1953          </tr>`).join("")}</tbody></table></div>
1954      </div>`:"";return`<div class="pulse-clock-grid">
1955        <div>
1956          <span class="pulse-stat-n">${escHtml(_ckMin(e.median_click_to_ready_min))}</span>
1957          <span class="pulse-stat-l">Generate \u2192 recap ready</span>
1958        </div>
1959        <div>
1960          <span class="pulse-stat-n">${escHtml(_ckMin(e.median_ready_to_post_min))}</span>
1961          <span class="pulse-stat-l">recap ready \u2192 Discord post</span>
1962        </div>
1963        <div>
1964          <span class="pulse-stat-n${a!==null&&a<60?" ck-ok":""}">${escHtml(_ckMin(a))}</span>
1965          <span class="pulse-stat-l">total, Generate \u2192 post</span>
1966        </div>
1967        <div>
1968          <span class="pulse-stat-n">${s===null?"\u2014":escHtml(`${s.toFixed(0)}%`)}</span>
1969          <span class="pulse-stat-l">posted within 90 min of Generate</span>
1970        </div>
1971        <div>
1972          <span class="pulse-stat-n">${n}</span>
1973          <span class="pulse-stat-l">sessions measured</span>
1974        </div>
1975        ${o}
1976      </div>
1977      ${c}
1978      ${e.note?`<div class="ck-muted ck-tile-note">${escHtml(e.note)}</div>`:""}`}function _ckNotInstrumented(e,t){const n=_ckErrOf(e);if(n)return _ckErrBody(n);const a=e.since?new Date(e.since):null,s=a&&!isNaN(a.getTime())?a.toLocaleDateString():null,i=e.note||t||"";return`<div class="ck-uninstr">
1979      <div class="ck-uninstr-state">${escHtml(s?`Instrumenting from ${s} \u2014 no data yet`:"Not instrumented yet \u2014 no data")}</div>
1980      ${i?`<div class="ck-muted ck-uninstr-note">${escHtml(i)}</div>`:""}
1981    </div>`}function _ckProvenance(e){const t=e.since?new Date(e.since):null,n=t&&!isNaN(t.getTime())?t.toLocaleDateString():null,a=e.granularity?`${e.granularity} granularity`:"day granularity";return`<div class="ck-muted ck-tile-note">${escHtml(`from tab presence, ${a}${n?` \xB7 since ${n}`:""}`)}${e.note?escHtml(` \xB7 ${e.note}`):""}</div>`}function _ckReaderReach(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);if(e.state!=="measured")return _ckNotInstrumented(e,"Needs the recap-open signal in session_events.");const n=Array.isArray(e.rows)?e.rows:[],a=_ckPctOfHundred(e.median_pct),s=_ckPctOfHundred(e.median_accounts_pct),i=Number(e.campaigns_below_30)||0,o=l=>l===null?"\u2014":escHtml(`${l.toFixed(0)}%`),r=`<div class="ck-bignum">
1982      <span class="pulse-card-value">${o(s)}</span>
1983      <span class="pulse-stat-l">of TF players, median</span>
1984      <span class="pulse-card-value">${o(a)}</span>
1985      <span class="pulse-stat-l">of players at the table, median</span>
1986      <span class="ck-delta ${i?"ck-delta--bad":"ck-delta--good"}">${i} campaign${i===1?"":"s"} below 30% of the table</span>
1987    </div>`,c=n.length?`<div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1988        <thead><tr><th>Campaign</th><th>GM</th><th>Sessions</th><th>Readers</th><th>TF players</th><th>% of TF players</th><th>At the table</th><th>% of the table</th></tr></thead>
1989        <tbody>${n.map(l=>{const d=_ckPctOfHundred(l.reach_pct),u=_ckPctOfHundred(l.reach_accounts_pct),p=l.audience===null||l.audience===void 0?l.table_seats:l.audience,m=f=>f==null?"\u2014":String(f);return`<tr>
1990            <td>${escHtml(l.campaign_name||"\u2014")}</td>
1991            <td>${escHtml(l.gm_email||"\u2014")}</td>
1992            <td>${escHtml(m(l.sessions_n))}</td>
1993            <td>${escHtml(m(l.readers))}</td>
1994            <td>${escHtml(m(l.players_accounts))}</td>
1995            <td>${o(u)}</td>
1996            <td>${escHtml(m(p))}</td>
1997            <td class="${d!==null&&d<30?"ck-cell-bad":""}">${o(d)}</td>
1998          </tr>`}).join("")}</tbody></table></div>`:_ckEmpty("No campaign has a measurable recap audience yet.");return r+c+_ckProvenance(e)}function _ckCodexConsulted(e){const t=_ckErrOf(e);if(t)return _ckErrBody(t);if(e.state!=="measured")return _ckNotInstrumented(e,"Needs the codex-view signal in session_events.");const n=Array.isArray(e.rows)?e.rows:[];if(!n.length)return _ckEmpty("No codex lookups recorded in the last 8 weeks.")+_ckProvenance(e);const a=s=>{if(s==null||s==="")return"\u2014";const i=String(s).toLowerCase();if(i==="up")return'<span class="ck-delta ck-delta--good">\u25B2 up</span>';if(i==="down")return'<span class="ck-delta ck-delta--bad">\u25BC down</span>';if(i==="flat")return'<span class="ck-delta ck-delta--flat">\xB1 flat</span>';const o=Number(s);return isNaN(o)?escHtml(String(s)):`<span class="ck-delta ${o>0?"ck-delta--good":o<0?"ck-delta--bad":"ck-delta--flat"}">${o>0?"\u25B2":o<0?"\u25BC":"\xB1"}${escHtml(Math.abs(o).toFixed(2))}</span>`};return`<div class="admin-table-wrap"><table class="pulse-mini-table pulse-mini-table--lined">
1999      <thead><tr><th>Campaign</th><th>GM</th><th>Live sessions 8w</th><th>Codex days 8w</th><th>At or before a session</th><th>Per session</th><th>Last 4 vs prior 4</th></tr></thead>
2000      <tbody>${n.map(s=>`<tr>
2001        <td>${escHtml(s.campaign_name||"\u2014")}</td>
2002        <td>${escHtml(s.gm_email||"\u2014")}</td>
2003        <td>${escHtml(String(s.live_sessions_8w===null||s.live_sessions_8w===void 0?"\u2014":s.live_sessions_8w))}</td>
2004        <td>${escHtml(String(s.codex_user_days_8w===null||s.codex_user_days_8w===void 0?"\u2014":s.codex_user_days_8w))}</td>
2005        <td>${escHtml(String(s.at_or_before_session===null||s.at_or_before_session===void 0?"\u2014":s.at_or_before_session))}</td>
2006        <td>${escHtml(s.per_session===null||s.per_session===void 0?"\u2014":(Number(s.per_session)||0).toFixed(2))}</td>
2007        <td>${a(s.last4_vs_prior4)}</td>
2008      </tr>`).join("")}</tbody></table></div>${_ckProvenance(e)}`}function _ckHeaderStrip(e,t){const n=(i,o,r,c)=>`<span class="ck-hs-item${r?" pulse-stat-drill":""}
2008"${r?` data-drill="${escHtml(r)}"`:""}${c?` title="${escHtml(c)}"`:""}>
2009        <span class="ck-hs-l">${escHtml(i)}</span><span class="ck-hs-v">${escHtml(o)}</span>
2010      </span>`,a=[],s=t&&!_ckErrOf(t)?t:null;if(s){const i=Number(s.new_7d)||0,o=Number(s.new_prev_7d)||0,r=`${i} new in the last 7 days (the 7 before: ${o})`+(s.new_28d!==null&&s.new_28d!==void 0?` \xB7 ${s.new_28d} in 28 days (the 28 before: ${s.new_prev_28d??"\u2014"})`:"")+(Number(s.returned_28d)>0?` \xB7 ${s.returned_28d} came back after deleting an account`:"");a.push(n("Users",`${Number(s.users)||0}${i?` (+${i} this week)`:""}`,null,r)),a.push(n("GMs",String(Number(s.gms)||0),null,null)),a.push(n("Players",String(Number(s.players)||0),null,null))}if(e){const i=e.mrr||{},o=e.deltas_7d||{},r=(o.new_subs||0)-(o.cancels||0),c=e.margin_30d_pct,l=c==null?"\u2014":`${c>=0?"+":""}${Number(c).toFixed(0)}%`,d=i.stripe||null;let u="Click for the subscriber list",p=`$${(i.current_dollars||0).toFixed(2)}`;if(i.source==="stripe"&&d){const h=[];d.duplicates&&d.duplicates.length&&h.push(`${d.duplicates.length} customer${d.duplicates.length===1?"":"s"} with more than one live subscription`),d.unknown_customers&&d.unknown_customers.length&&h.push(`${d.unknown_customers.length} Stripe customer${d.unknown_customers.length===1?"":"s"}
2010 with no account here`),d.missing_in_stripe&&d.missing_in_stripe.length&&h.push(`${d.missing_in_stripe.length} paying here with no live subscription in Stripe`),d.amount_mismatches&&d.amount_mismatches.length&&h.push(`${d.amount_mismatches.length} amount${d.amount_mismatches.length===1?"":"s"} differ`),d.unpriceable&&h.push(`${d.unpriceable} not in USD (not counted)`),u=`From Stripe: $${((d.mrr_cents||0)/100).toFixed(2)} across ${d.live_subscriptions||0} live subscriptions. Our records: $${((d.local_cents||0)/100).toFixed(2)}.`+(h.length?" "+h.join("; ")+".":" The two agree.")+" Click for the subscriber list.",d.clean||(p+=" \u26A0")}else i.source==="users"&&(u="From our own records; Stripe did not answer"+(d&&d.error?` (${d.error})`:"")+". Click for the subscriber list.");a.push(n("MRR",p,"subscribers",u)),a.push(n("Paying",String(i.paid_total||0),"subscribers","Click for the subscriber list")),a.push(n("Net new (7d)",`${r>0?"+":""}${r}`,null,null)),a.push(n("Active GMs (7d)",String(e.active_gms_7d||0),"active_gms","Click for the GM list"));const m=e.vendors_30d||{},f=["anthropic","openai"].filter(h=>m[h]&&m[h].scope==="account"&&(m[h].usd||0)>0),g=f.length?`Account-wide ${f.join(" + ")} spend (shared with Dead Ringer) until the next cost sync re-scopes it to Threadfall. Click for the AI cost breakdown`:"Threadfall-scoped vendor spend. Click for the AI cost breakdown";a.push(n("Gross margin (30d)",f.length?`${l} \xB7 account-wide`:l,"ai_cost_endpoint",g))}return a.length?`<div class="ck-headstrip">${a.join("")}</div>`:'<div class="ck-headstrip ck-muted">Headline numbers unavailable.</div>'}async function adminLoadPulse(){const e=document.getElementById("admin-pulse-wrap");if(!e)return;let t;try{const[a,s]=await Promise.all([api("GET","/api/admin/cockpit").then(o=>({ok:!0,v:o}),o=>({ok:!1,e:o})),api("GET","/api/admin/business-pulse").then(o=>({ok:!0,v:o}),o=>({ok:!1,e:o}))]);if(!a.ok&&!s.ok)throw a.e;t=s.ok?s.v:null;const i=a.ok?a.v||{}:_ckAllErr(a.e);e.innerHTML=`
2011      ${_ckHeaderStrip(t,i.people)}
2012      <div class="ck-grid">
2013        ${_admSec("pulse","unit","Unit economics","Cost per live session, its trend, and the most expensive sessions")}
2014        ${_ckTile("cost","Cost per live session","Cost per live session, last 8 weeks, and the three most expensive sessions",i.cost_trend&&i.cost_trend.alarm&&!i.cost_trend.acked?"alarm":"gauge","Latest week more than 20% above the 8-week mean for two weeks running: break the week down by AI endpoint and find the stage that moved.",_ckCost(i.cost_trend))}
2015        ${_admSecEnd()}
2016        ${_admSec("pulse","reliability","Reliability","Recordings that produced no recap, and the cleanup work extraction leaves behind")}
2017        ${_ckTile("promises","Broken promises","Live sessions that produced no recap, and pipeline failures","alarm","Every row is a GM who recorded and did not get a recap. Fix it the same hour, then message them. A repeat on the same campaign is an outreach candidate.",_ckBrokenPromises(i.broken_promises))}
2018        ${_ckTile("cleanup","Cleanup load","Duplicate-entity merges per recorded session, by GM","alarm","Merges per session rising two weeks running means extraction is splitting one name into several, and that GM is doing correction work the automation was supposed to do.",_ckCleanup(i.cleanup))}
2019        ${_admSecEnd()}
2020        ${_admSec("pulse","acquisition","Acquisition","Whether Google can still find us")}
2021        ${_ckTile("search","Search \u2014 worst row","Google index, sitemap and Search Console pull status","alarm","The overall state is the worst single row, never an average. A stale Search Console pull makes every other search number read green while blind.",_ckSearch(i.search))}
2022        ${_admSecEnd()}
2023        ${_admSec("pulse","activation","Activation","New signups reaching their first recap, and how fast a recap lands")}
2024        ${_ckTile("fortnight","First-fortnight board","Independent signups in the last 28 days, and how far each one got","gauge","Red at zero independent signups in 21 days. The step where a signup stalled names the fix: no recording, a slow recap, Discord not wired, or no second session.",_ckFirstFortnight(i.first_fortnight,i.people))}
2025        ${_ckTile("samenight","Same-night delivery","Minutes from Generate recap to the Discord post","gauge","Green at a total median under 60 minutes.",_ckSameNight(i.same_night))}
2026        ${_admSecEnd()}
2027        ${_admSec("pulse","retention","Retention","Tables going quiet, and whether anyone reads the recap or the Codex")}
2028        ${_ckTile("quitters","Silent quitters & quiet payers","Tables still active that have stopped recording, and paying GMs gone quiet","alarm","Any paying GM on this list gets a direct message inside 48 hours.",_ckQuitters(i.quitters))}
2029        ${_ckTile("reach","Reader Reach","Players who opened the recap within 7 days, as a share of TF players and of everyone at the table","gauge","Two rates. % of TF players is readers over the players with a Threadfall account, one per person, two accounts collapsed. % of the table is readers over everyone at the table: the active PCs, one seat per player however many PCs they run, the GM's own left out, or the account count where that is larger. At or above 60% of the table the recap is reaching the table. Below 30% it is only being read by the GM.",_ckReaderReach(i.reader_reach))}
2030        ${_ckTile("codex","Codex consulted","Codex lookups at the table or in the two days before a session, per campaign","gauge","Rising per campaign over its life. Flat or zero on a long-lived campaign means nothing would be lost by leaving.",_ckCodexConsulted(i.codex_consulted))}
2031        ${_admSecEnd()}
2032        ${_admSec("pulse","revenue","Revenue","Cancellations, failed checkouts, and cost against what each account pays")}
2033        ${_ckTile("leaks","Money leaks","Cancellations, failed subscription checkouts, and every paying account's cost against what it pays","alarm","Cancellation: contact them the same day. Failed checkout: open Stripe and email them today. Cost at 0.7x of what they pay: a named conversation. Two months over 1.0x: reprice, with a date.",_ckMoneyLeaks(i.money_leaks))}
2034        ${_admSecEnd()}
2035      </div>
2036      ${i.generated_at?`<div class="ck-gen ck-muted">Cockpit generated ${escHtml(_pulseRelativeTime(i.generated_at))}</div>`:""}
2037    `,_ckCollapseClear(e),_ckRailDots(e)}catch(a){_adminRenderLoadError(e,a,adminLoadPulse);return}e.addEventListener("click",_pulseDrillClickHandler),e.addEventListener("click",_ckAckClickHandler),e.addEventListener("keydown",_ckAckKeyHandler);const n=document.getElementById("admin-cockpit-banner");if(n&&(n.innerHTML=""),t)try{_cockpitInjectSinceLastLooked(t)}catch(a){console.warn("cockpit banner inject failed",a)}try{_cockpitInjectAlerts()}catch(a){console.warn("cockpit alerts inject failed",a)}}function _ckQueueStatus(e,t){if(e==="subscribed"){const n=_TIER_LABEL[t]||(t?String(t):"");return{label:n?`Subscribed \xB7 ${n}
2037`:"Subscribed",cls:"admin-pill--ok",tip:"Paying subscriber. Nothing to chase."}}return{proved_it:{label:"Recorded and got a recap",cls:"admin-pill--teal",tip:"Independent signup who has watched the product work on their own table."},date_on_calendar:{label:"Session booked",cls:"admin-pill--teal",tip:"Campaign created and a session on the calendar, no recording yet. One dated message converts them."},activated:{label:"Activated",cls:"admin-pill--ok",tip:"Recorded and got a recap, or a player using the codex and recaps."},pending:{label:"Waiting",cls:"admin-pill--muted",tip:"Signed up in the last 7 days, nothing yet. Watch, do not chase."},dormant:{label:"Nothing after 7 days",cls:"admin-pill--warn",tip:"One last message, then leave them."},invited:{label:"Invited",cls:"admin-pill--muted",tip:"Came in through another GM. Invited players rarely become GMs."}}[e]||{label:e?String(e):"unknown",cls:"admin-pill--muted",tip:""}}function _ckQueueBand(e){return(e.origin||"")==="independent"?e.status==="proved_it"||e.status==="date_on_calendar"?0:e.status==="dormant"?1:2:3}async function adminLoadSignupQueue(){const e=document.getElementById("admin-signup-queue-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t;try{const i=await api("GET","/api/admin/cockpit");t=(i||{}).signup_queue,adminLoadSignupQueue.people=(i||{}).people}catch(i){e.innerHTML=_ckErrBody(i.message||"Signup queue failed to load");return}const n=_ckErrOf(t);if(n){e.innerHTML=_ckErrBody(n);return}const a=Array.isArray(t.rows)?t.rows:[];if(!a.length){e.innerHTML=_ckByChannelLine(adminLoadSignupQueue.people)+_ckEmpty("No signups yet.");return}const s=a.slice().sort((i,o)=>{const r=_ckQueueBand(i)-_ckQueueBand(o);return r||new Date(o.signed_up||0).getTime()-new Date(i.signed_up||0).getTime()});e.innerHTML=_ckByChannelLine(adminLoadSignupQueue.people)+`<div class="admin-table-wrap"><table class="admin-table ck-queue">
2038      <thead><tr>
2039        <th>Origin</th><th>Channel</th><th>When</th><th>Who</th><th>Became</th>
2040        <th>What they did</th><th>Next session</th><th>Status</th>
2041      </tr></thead>
2042      <tbody>${s.map(i=>{const o=(i.origin||"")==="independent",r=_ckQueueStatus(i.status,i.tier),c={gm:"GM",player:"Player",none:"Neither yet"}
2042[i.became]||"Neither yet",l=o?`<span class="admin-pill admin-pill--teal" title="Found Threadfall on their own, not invited into someone else's table.">Independent</span>`:`<span class="admin-pill admin-pill--muted" title="Invited into an existing table.">Invited</span>${i.inviter_email?` <span class="ck-muted">by ${escHtml(i.inviter_email)}</span>`:""}`;return`<tr class="${o?"":"ck-row--muted"}">
2043          <td>${l}</td>
2044          <td>${_ckChannelChip(i)}</td>
2045          <td>${_ckWhen(i.signed_up)}</td>
2046          <td><span class="ck-ff-name">${escHtml(i.display_name||i.email||"\u2014")}</span><br><span class="ck-muted">${escHtml(i.email||"")}</span></td>
2047          <td>${escHtml(c)}</td>
2048          <td>${escHtml(i.what||"\u2014")}</td>
2049          <td>${_ckWhenFuture(i.next_session_at)}</td>
2050          <td><span class="admin-pill ${r.cls}"${r.tip?` title="${escHtml(r.tip)}"`:""}>${escHtml(r.label)}</span></td>
2051        </tr>`}).join("")}</tbody></table></div>`}function _ckLcDur(e){if(e==null||e==="")return"\u2014";const t=Number(e);return isNaN(t)?"\u2014":t<1?"<1 min":t<90?`${Math.round(t)} min`:t<2880?`${(t/60).toFixed(1)} h`:`${(t/1440).toFixed(1)} d`}function _ckLcPct(e){if(e==null||e==="")return null;const t=Number(e);return isNaN(t)?null:t}function _ckLcPctLabel(e){const t=_ckLcPct(e);return t===null?"\u2014":`${Math.round(t)}%`}function _ckLcW(e){const t=Number(e);return!isFinite(t)||t<=0?.2:Math.max(.2,Math.log10(1+t))}function _ckLcClock(e){if(!e)return"not recorded";const t=new Date(e);return isNaN(t.getTime())?String(e):t.toLocaleString()}function _ckLcSpans(){return[{key:"length",cls:"rec",label:"Recording",from:"recording_start",to:"recording_stop"},{key:"stop_to_click",cls:"wait",label:"Wait to Generate",from:"recording_stop",to:"generate_click"},{key:"generating",cls:"gen",label:"Generating",from:"generate_click",to:"recap_ready"},{key:"editing_window",cls:"edit",label:"Editing",from:"recap_ready",to:"last_edit"},{key:"ready_to_discord",cls:"disc",label:"To Discord",from:"recap_ready",to:"discord_post"},{key:"recap_to_qotn",cls:"qotn",label:"To quote of the night",from:"recap_ready",to:"qotn_sent"}]}function _ckLcStage(e,t,n,a,s){return`<div class="ck-lc-stage${s?" "+s:""}" style="--w:${a}">
2052      <span class="ck-lc-stage-n">${escHtml(e)}</span>
2053      <span class="ck-lc-stage-v">${escHtml(t)}</span>
2054      ${n?`<span class="ck-lc-stage-r ck-muted">${escHtml(n)}</span>`:""}
2055    </div>`}function _ckLcStrip(e){const t=e.medians_min&&!_ckErrOf(e.medians_min)?e.medians_min:{},n=e.medians_prep&&!_ckErrOf(e.medians_prep)?e.medians_prep:{},a=e.rates_pct&&!_ckErrOf(e.rates_pct)?e.rates_pct:{},s=d=>d==null||isNaN(Number(d))?null:Number(d),i=d=>{const u=s(d);return u===null?"\u2014":`${u.toFixed(1)} d`},o=`${i(n.gm_days_2d)} GM \xB7 ${i(n.player_days_2d)} player`,r=`GM present ${_ckLcPctLabel(a.gm_prep_presence)} \xB7 players present ${_ckLcPctLabel(a.player_prep_presence)}`+(s(n.player_users_2d)===null?"":` \xB7 ${s(n.player_users_2d).toFixed(1)} players typical`),c=`sent on ${_ckLcPctLabel(a.qotn_sent)} \xB7 crowned ${_ckLcPctLabel(a.qotn_crowned_of_sent)} of sent \xB7 card ${_ckLcPctLabel(a.qotn_card_of_crowned)} of crowned`;return`<div class="ck-lc-strip">${[_ckLcStage("Prep, 2 days before",o,r,1.6,"ck-lc-stage--fixed"),_ckLcStage("Recording",_ckLcDur(t.length),"",_ckLcW(t.length)),_ckLcStage("Wait to Generate",_ckLcDur(t.stop_to_click),"",_ckLcW(t.stop_to_click)),_ckLcStage("Generating",_ckLcDur(t.generating),"",_ckLcW(t.generating)),_ckLcStage("Editing",_ckLcDur(t.editing_window),`edited after on ${_ckLcPctLabel(a.edited_after)}`,_ckLcW(t.editing_window)),_ckLcStage("Released in app","the instant the recap is ready",`released within 90 min of the Generate click on ${_ckLcPctLabel(a.released_within_90_of_click)} of sessions`,1.4,"ck-lc-stage--mark"),_ckLcStage("Discord post",_ckLcDur(t.ready_to_discord),`posted on ${_ckLcPctLabel(a.discord_posted)} of sessions`,_ckLcW(t.ready_to_discord)),_ckLcStage("Quote of the night",_ckLcDur(t.recap_to_qotn),c,_ckLcW(t.recap_to_qotn))].join("")}</div>
2056    <div class="ck-lc-scale ck-muted">Widths are proportional to log(1 + median minutes), so a four-minute generation stays visible beside a three-hour session: they compare order of magnitude, not duration. Prep is a fixed-width marker because it is counted in days of presence, and the in-app release is a moment rather than a span. Every figure is a median over the window.</div>`}function _ckLcRates(e){const t=e.rates_pct&&!_ckErrOf(e.rates_pct)?e.rates_pct:{},n=(s,i,o,r)=>`<div class="ck-lc-rate${o?" ck-lc-rate--muted":""}
2056"${r?` title="${escHtml(r)}"`:""}>
2057        <span class="pulse-stat-n">${escHtml(i)}</span>
2058        <span class="pulse-stat-l">${escHtml(s)}</span>
2059      </div>`,a=e.since_presence?`Prep presence is only measurable from ${escHtml(new Date(e.since_presence).toLocaleDateString())}; sessions before that date have no prep figure.`:"Prep presence has no start date recorded yet, so the prep figures may cover only part of the window.";return`<div class="ck-lc-rates">
2060      ${n("released within 90 min of Generate",_ckLcPctLabel(t.released_within_90_of_click),!1,"Share of sessions whose recap reached the players inside 90 minutes of the Generate click.")}
2061      ${n("got a recap",_ckLcPctLabel(t.recapped),!1,"Share of sessions in the window that produced a recap at all.")}
2062      ${n("players notified in app",_ckLcPctLabel(t.in_app_notified),!1,"Share of sessions that sent the in-app notice.")}
2063      ${n("have a recorded length",_ckLcPctLabel(t.with_length),!0,"Data completeness, not a product measure: the share of sessions with a usable recording start and stop. Every median above is drawn from these.")}
2064    </div>
2065    <div class="ck-lc-scale ck-muted">${a}</div>`}function _ckLcFlagPills(e){const t=e||{},n=(a,s)=>`<span class="ck-lc-flag${s?" ck-lc-flag--on":""}">${escHtml(a)}</span>`;return`<span class="ck-lc-flags">
2066      ${n("recap",t.recapped)}${n("in-app",t.in_app_notified)}${n("Discord",t.discord)}${n("QotN",t.qotn_sent)}${n("card",t.card)}
2067    </span>`}function _ckLcBar(e){const t=e.t||{},n=e.durations_min||{};return`<div class="ck-lc-bar">${_ckLcSpans().map(a=>{const s=n[a.key],i=s==null||isNaN(Number(s))?null:Number(s);if(i===null)return`<span class="ck-lc-bseg ck-lc-bseg--gap" style="--w:0.35" title="${escHtml(`${a.label} \xB7 not measured`)}"></span>`;const o=a.key==="length"&&e.length_source==="log_entries"?" \xB7 length from log entries":"",r=`${a.label} \xB7 ${_ckLcDur(i)}${o}
2068${_ckLcClock(t[a.from])} \u2192 ${_ckLcClock(t[a.to])}`;return`<span class="ck-lc-bseg ck-lc-bseg--${a.cls}" style="--w:${_ckLcW(i)}" title="${escHtml(r)}"></span>`}).join("")}</div>`}function _ckLcModeLabel(e){return{discord:"Discord",virtual:"Virtual",inperson:"In person"}[String(e||"").toLowerCase().replace(/[^a-z]/g,"")]||(e?String(e):"not set")}function _ckLcSessions(e,t){const n=Array.isArray(e.sessions)?e.sessions:[],a=o=>String(o||"").toLowerCase().replace(/[^a-z]/g,""),s=t==="all"?n:n.filter(o=>a(o.capture_mode)===t);return n.length?s.length?`<div class="ck-lc-legend">${_ckLcSpans().map(o=>`<span class="ck-lc-key"><span class="ck-lc-swatch ck-lc-bseg--${o.cls}"></span>${escHtml(o.label)}</span>`).join("")+'<span class="ck-lc-key"><span class="ck-lc-swatch ck-lc-bseg--gap"></span>not measured</span>'}</div>
2069    <div class="ck-lc-scale ck-muted">Each bar is one session, stages laid end to end and scaled the same way as the strip above. In real time the editing window, the Discord post and the quote of the night all start the moment the recap is ready, so a bar is a shape to compare between rows, not a wall clock. Hover any segment for its minutes and both timestamps.</div>
2070    <div class="admin-table-wrap"><table class="admin-table ck-lc-table">
2071      <thead><tr>
2072        <th>Campaign</th><th>GM</th><th>Mode</th><th>Prep</th><th>Timeline</th><th>Delivered</th>
2073      </tr></thead>
2074      <tbody>${s.map(o=>{const r=o.prep||{},c=o.scheduled===!0?"scheduled":o.scheduled===!1?"impromptu":"";return`<tr>
2075          <td><span class="ck-ff-name">${escHtml(o.campaign_name||"\u2014")}</span>${c?`<br><span class="ck-muted">${escHtml(c)}</span>`:""}</td>
2076          <td>${escHtml(o.gm_email||"\u2014")}</td>
2077          <td>${escHtml(_ckLcModeLabel(o.capture_mode))}</td>
2078          <td class="ck-lc-prep" title="Days in Threadfall in the two days before the session, and how many distinct players were among them.">
2079            <span class="ck-lc-prep-n">${Number(r.gm_days)||0}</span><span class="ck-muted">GM d</span>
2080            <span class="ck-lc-prep-n">${Number(r.player_days)||0}</span><span class="ck-muted">plyr d</span>
2081            <span class="ck-lc-prep-n">${Number(r.player_users)||0}</span><span class="ck-muted">plyrs</span>
2082          </td>
2083          <td class="ck-lc-barcell">${_ckLcBar(o)}</td>
2084          <td>${_ckLcFlagPills(o.flags)}</td>
2085        </tr>`}).join("")}</tbody></table></div>`:_ckEmpty("No sessions in this window with that capture mode."):_ckEmpty("No sessions in this window.")}function _ckLcModeOptions(e){const t=o=>String(o||"").toLowerCase().replace(/[^a-z]/g,""),n=[["all","All"],["discord","Discord"],["virtual","Virtual"],["inperson","In person"]],a={};
2085n.forEach(o=>{a[o[0]]=!0});const s=[],i=e&&e.by_capture_mode&&!_ckErrOf(e.by_capture_mode)?e.by_capture_mode:{};return Object.keys(i).forEach(o=>s.push(o)),(Array.isArray(e&&e.sessions)?e.sessions:[]).forEach(o=>s.push(o.capture_mode)),s.forEach(o=>{const r=t(o);!r||a[r]||(a[r]=!0,n.push([r,_ckLcModeLabel(o)]))}),n}function _ckLcControls(e,t,n){const a=_ckLcModeOptions(e),s=[4,12,26],i=Number(e.n);return`<div class="ck-lc-controls">
2086      <span class="form-section-label">Capture mode</span>
2087      ${a.map(o=>`<button type="button" class="type-filter-btn${n===o[0]?" active":""}" data-lc-mode="${escHtml(o[0])}">${escHtml(o[1])}</button>`).join("")}
2088      <label class="ck-lc-weeks-lbl">
2089        <span class="form-section-label">Window</span>
2090        <select class="form-input ck-lc-weeks" data-lc-weeks>
2091          ${s.map(o=>`<option value="${o}"${o===t?" selected":""}>${o} weeks</option>`).join("")}
2092        </select>
2093      </label>
2094      <span class="ck-muted ck-lc-count">${escHtml(isNaN(i)?"":`${i} session${i===1?"":"s"} in the window`)}</span>
2095    </div>${_ckLcModeMedians(e,n)}`}function _ckLcModeMedians(e,t){if(t==="all")return"";const n=e.by_capture_mode&&!_ckErrOf(e.by_capture_mode)?e.by_capture_mode:{},a=o=>String(o||"").toLowerCase().replace(/[^a-z]/g,""),s=Object.keys(n).find(o=>a(o)===t);if(!s)return"";const i=n[s]||{};return`<div class="ck-lc-scale ck-muted">${escHtml(`${_ckLcModeLabel(s)} only (${Number(i.n)||0} sessions): recording ${_ckLcDur(i.length)} \xB7 wait to Generate ${_ckLcDur(i.stop_to_click)} \xB7 generating ${_ckLcDur(i.generating)} \xB7 to Discord ${_ckLcDur(i.ready_to_discord)}`)}</div>`}function _ckLcRender(){const e=document.getElementById("admin-lifecycle-wrap");if(!e)return;const t=adminLoadLifecycle.data,n=adminLoadLifecycle.weeks||12,a=adminLoadLifecycle.mode||"all";if(!t){e.innerHTML=_ckEmpty("No lifecycle data loaded.");return}const s=_ckErrOf(t);if(s){e.innerHTML=_ckLcControls({},n,a)+_ckErrBody(s);return}e.innerHTML=_ckLcControls(t,n,a)+_ckLcStrip(t)+_ckLcRates(t)+`<div class="ck-sub"><span class="form-section-label">Every session in the window, newest first</span>${_ckLcSessions(t,a)}</div>`}function _ckLcClickHandler(e){const t=e.target.closest("[data-lc-mode]");t&&(adminLoadLifecycle.mode=t.getAttribute("data-lc-mode")||"all",_ckLcRender())}function _ckLcChangeHandler(e){const t=e.target.closest("[data-lc-weeks]");if(!t)return;const n=parseInt(t.value,10);!n||n===adminLoadLifecycle.weeks||(adminLoadLifecycle.weeks=n,adminLoadLifecycle())}async function adminLoadLifecycle(){const e=document.getElementById("admin-lifecycle-wrap");if(e){adminLoadLifecycle.weeks||(adminLoadLifecycle.weeks=12),adminLoadLifecycle.mode||(adminLoadLifecycle.mode="all"),e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{adminLoadLifecycle.data=await api("GET",`/api/admin/lifecycle?weeks=${encodeURIComponent(adminLoadLifecycle.weeks)}`)}catch(t){adminLoadLifecycle.data={error:t.message||"Lifecycle failed to load"}}_ckLcRender(),e.addEventListener("click",_ckLcClickHandler),e.addEventListener("change",_ckLcChangeHandler)}}function _cockpitInjectSinceLastLooked(e){const t=document.getElementById("admin-cockpit-banner");if(!t)return;const n="tf_admin_cockpit_snapshot",a=Date.now(),s={ts:a,mrr:e.mrr&&e.mrr.current_dollars||0,mrrSource:e.mrr&&e.mrr.source||"users",paying:e.mrr&&e.mrr.paid_total||0,gms:e.active_gms_7d||0};let i=null;try{i=JSON.parse(localStorage.getItem(n)||"null")}catch{i=null}try{localStorage.setItem(n,JSON.stringify(s))}catch{}if(!i||!i.ts)return;const o=(i.mrrSource||"users")!==s.mrrSource,r=Math.max(1,Math.round((a-i.ts)/6e4)),c=r<60?`${r}m`:r<1440?`${Math.round(r/60)}h`:`${Math.round(r/1440)}d`,l=(u,p,m)=>{const f=u-p,g=m?`$${Math.abs(f).toFixed(2)}`:`${Math.abs(f)}`;return f===0?'<span class="cockpit-delta cockpit-delta--flat">\xB10</span>':`<span class="cockpit-delta ${f>0?"cockpit-delta--up":"cockpit-delta--down"}">${f>0?"\u25B2":"\u25BC"} ${g}</span>`},d=document.createElement("div");d.className="cockpit-since",d.innerHTML=`Since your last visit <strong>${c} ago</strong>: MRR ${o?`<span class="cockpit-delta cockpit-delta--flat">now from ${escHtml(s.mrrSource==="stripe"?"Stripe":"our records")}</span>`:l(s.mrr,i.mrr,!0)} \xB7 Paying ${l(s.paying,i.paying,!1)} \xB7 Active GMs ${l(s.gms,i.gms,!1)}`,t.prepend(d)}async function _cockpitInjectAlerts(){const e=document.getElementById("admin-cockpit-banner");if(!e)return;let t;try{t=await api("GET","/api/admin/alerts")}catch{return}const n=t.heavy_users_24h||[],a=t.expensive_campaigns||[],s=t.rapid_fire_emails||[],i=t.release_notes_pending||0;if(!n.length&&!a.length&&!s.length&&!i)return;const o=[];if(i&&o.push(`<span class="cockpit-alert-item"><strong>${i}</strong> release note${i===1?"":"s"} waiting for your review (Updates tab)</span>`),n.length){const c=n.slice(0,3).map(l=>`${escHtml(l.email)} (${l.calls_24h} calls \xB7 ${_fmt$(l.cost_24h)})`).join(", ");o.push(`<span class="cockpit-alert-item"><strong>${n.length}</strong> heavy AI user${n.length===1?"":"s"} (24h): ${c}${n.length>3?" \u2026":""}</span>`)}if(s.length&&o.push(`<span class="cockpit-alert-item"><strong>${s.length}</strong> rapid-fire: ${s.slice(0,3).map(escHtml).join(", ")}${s.length>3?" \u2026":""}</span>`),a.length){const c=a.slice(0,3).map(l=>`${escHtml(l.name)}
2095 (${_fmt$(l.total_cost)})`).join(", ");o.push(`<span class="cockpit-alert-item"><strong>${a.length}</strong> expensive campaign${a.length===1?"":"s"}: ${c}${a.length>3?" \u2026":""}</span>`)}const r=document.createElement("div");r.className="cockpit-alert-banner",r.innerHTML=`<span class="cockpit-alert-badge">\u26A0 Alerts</span> ${o.join('<span class="cockpit-alert-sep">\xB7</span>')}`,e.appendChild(r)}function _pulseDrillClickHandler(e){const t=e.target.closest("[data-drill]");if(!t)return;const n=t.getAttribute("data-drill");n&&_pulseShowDrilldown(n)}async function _pulseShowDrilldown(e){const t=document.getElementById("pulse-drill-modal"),n=document.getElementById("pulse-drill-title"),a=document.getElementById("pulse-drill-body");if(!(!t||!n||!a)){n.textContent="Loading\u2026",a.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>',t.classList.remove("hidden");try{const s=await api("GET",`/api/admin/pulse-drilldown?panel=${encodeURIComponent(e)}`);n.textContent=s.title||"Detail";const i=s.columns||[],o=s.rows||[];if(!o.length){a.innerHTML='<p class="placeholder--padded">No rows.</p>';return}a.innerHTML=`
2096      <div class="pulse-drill-meta">${o.length} row${o.length===1?"":"s"}</div>
2097      <div class="pulse-drill-table-wrap">
2098        <table class="pulse-drill-table">
2099          <thead><tr>${i.map(r=>`<th>${escHtml(r)}</th>`).join("")}</tr></thead>
2100          <tbody>
2101            ${o.map((r,c)=>{const l=(s.drill_rows||[])[c]||"";return`
2102              <tr${l?` class="pulse-stat-drill" data-drill="${escHtml(l)}" title="Click for detail"`:""}>${r.map(u=>`<td>${escHtml(String(u??""))}</td>`).join("")}</tr>`}).join("")}
2103          </tbody>
2104        </table>
2105      </div>`,a.onclick=_pulseDrillClickHandler}catch(s){n.textContent="Error",a.innerHTML=`<p class="placeholder--padded error">${escHtml(s.message||"Failed to load")}</p>`}}}document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("pulse-drill-modal"),t=document.getElementById("pulse-drill-close");t&&t.addEventListener("click",()=>e?.classList.add("hidden")),e&&e.addEventListener("click",n=>{n.target===e&&e.classList.add("hidden")}),document.addEventListener("keydown",n=>{_tfModalEsc(n,e)&&e.classList.add("hidden")})}),window.openAdminPanel=openAdminPanel;function _adminRenderLoadError(e,t,n){if(!e)return;const a=(t.message||"").includes("Admin access")||(t.message||"").includes("403");e.innerHTML=`<div class="admin-load-error">
2106    <p class="placeholder--padded error">\u26A0 ${escHtml(t.message||"Failed to load")}</p>
2107    ${a?'<button id="admin-activate-btn" class="btn-new">Activate Admin Access</button>':""}
2108    <button id="admin-retry-btn" class="btn-ghost btn-xs">\u21BA Retry</button>
2109  </div>`,e.querySelector("#admin-activate-btn")?.addEventListener("click",async()=>{try{await api("POST","/api/admin/activate"),showToast("Admin activated, reloading\u2026",3e3),setTimeout(()=>location.reload(),1500)}catch(s){showToast("Activate failed: "+s.message,5e3)}}),e.querySelector("#admin-retry-btn")?.addEventListener("click",()=>{n&&n()})}function _adminTable(e,t){const n={q:"",sortIdx:t.defaultSort?t.defaultSort.idx:null,dir:t.defaultSort?t.defaultSort.dir:"desc",active:new Set};function a(){let c=t.rows.slice();const l=n.q.trim().toLowerCase();if(l&&t.search&&(c=c.filter(d=>t.search(d,l))),n.active.size&&t.filters&&t.filters.forEach(d=>{n.active.has(d.id)&&(c=c.filter(d.test))}),n.sortIdx!=null&&t.columns[n.sortIdx]&&t.columns[n.sortIdx].sort){const d=t.columns[n.sortIdx].sort,u=n.dir==="asc"?1:-1;c.sort((p,m)=>{let f=d(p),g=d(m);return f==null&&g==null?0:f==null?1:g==null?-1:typeof f=="number"&&typeof g=="number"?(f-g)*u:String(f).localeCompare(String(g))*u})}return c}function s(){return t.columns.map((c,l)=>{const d=!!c.sort,u=d&&n.sortIdx===l?`<span class="admin-th-arrow">${n.dir==="asc"?"\u25B2":"\u25BC"}</span>`:"";return`<th${d?` class="admin-th--sortable" data-sort-idx="${l}"`:""}${c.title?` title="${escHtml(c.title)}"`:""}>${escHtml(c.label)}${u}</th>`}).join("")}function i(){const c=a(),l=new Set((t.sections||[]).filter(v=>v.excludeFromTotal).map(v=>v.key)),d=t.sectionOf&&l.size?c.filter(v=>!l.has(t.sectionOf(v))):c,u=e.querySelector(".admin-table tbody"),p=e.querySelector(".admin-table thead tr"),m=e.querySelector(".admin-tbl-count");p&&(p.innerHTML=s());const f=t.columns.some(v=>v.agg),g=(v,b,y)=>{const _=t.columns.map((w,$)=>{if($===0)return`<td>${escHtml(b)}</td>`;if(!w.agg)return"<td></td>";
2109const E=v.reduce((k,C)=>k+(Number(w.agg(C))||0),0);return`<td>${w.aggFmt?w.aggFmt(E):E}</td>`}).join("");return`<tr class="admin-tbl-subtotal${y?" admin-tbl-subtotal--grand":""}">${_}</tr>`};let h;if(c.length&&t.sections&&t.sectionOf){const v=new Map(t.sections.map(b=>[b.key,[]]));c.forEach(b=>{const y=t.sectionOf(b);v.has(y)&&v.get(y).push(b)}),h=t.sections.map(b=>{const y=v.get(b.key)||[],_=`<tr class="admin-tbl-section"><td colspan="${t.columns.length}">${escHtml(b.label)}<span class="admin-tbl-section-n">${y.length}</span></td></tr>`,w=y.length?y.map(t.renderRow).join(""):`<tr class="admin-tbl-section-empty"><td colspan="${t.columns.length}">None</td></tr>`,$=y.length&&f?g(y,"Subtotal",!1):"";return _+w+$}).join(""),f&&(h+=g(d,"All (counted)",!0))}else h=c.length?c.map(t.renderRow).join(""):`<tr><td colspan="${t.columns.length}"><p class="admin-empty">No matches.</p></td></tr>`,c.length&&f&&(h+=g(d,"Total",!0));if(u&&(u.innerHTML=h),m){const v=c.length-d.length;m.textContent=`${d.length} ${t.countNoun||"rows"}`+(v>0?` \xB7 ${v} not counted (internal / spam / linked dupes)`:"")}e.querySelectorAll(".admin-th--sortable").forEach(v=>v.addEventListener("click",()=>{const b=parseInt(v.dataset.sortIdx,10);n.sortIdx===b?n.dir=n.dir==="asc"?"desc":"asc":(n.sortIdx=b,n.dir="desc"),i()})),t.afterRender&&t.afterRender(e)}const o=(t.filters||[]).map(c=>`<button class="admin-chip" data-chip="${escHtml(c.id)}">${escHtml(c.label)}</button>`).join("");e.innerHTML=`
2110    ${t.notice||""}
2111    <div class="admin-tbl-controls">
2112      ${t.search?`<input type="search" class="admin-search" placeholder="${escHtml(t.searchPlaceholder||"Search\u2026")}">`:""}
2113      ${o?`<div class="admin-tbl-chips">${o}</div>`:""}
2114      <span class="admin-tbl-count"></span>
2115    </div>
2116    <div class="admin-table-wrap">
2117      <table class="admin-table"><thead><tr></tr></thead><tbody></tbody></table>
2118    </div>`;const r=e.querySelector(".admin-search");r&&r.addEventListener("input",()=>{n.q=r.value,i()}),e.querySelectorAll(".admin-chip").forEach(c=>c.addEventListener("click",()=>{const l=c.dataset.chip;n.active.has(l)?(n.active.delete(l),c.classList.remove("active")):(n.active.add(l),c.classList.add("active")),i()})),i()}async function adminLoadUsers(){const e=document.getElementById("admin-users-table-wrap");e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{const[t,n,a]=await Promise.all([api("GET","/api/admin/users"),api("GET","/api/billing/tiers").catch(()=>null),api("GET","/api/admin/account-links").catch(()=>null)]);if(!t.length){e.innerHTML='<p class="admin-empty">No users yet.</p>';return}const s={};(n&&n.tiers||[]).forEach(y=>{s[y.key]=(y.price_monthly_cents||0)/100});const i=y=>{const _=(y.subscription_tier||"free").toLowerCase(),w=s[_]||0;return y.id!==null&&w>0&&(y.subscription_status||"")==="active"&&(y.ai_cost_total||0)>=.6*w},o=y=>Math.max(y.last_engaged_at?new Date(y.last_engaged_at).getTime():0,y.last_visited_at?new Date(y.last_visited_at).getTime():0)||null,r=y=>{const _=o(y);return _&&Date.now()-_<60*864e5},c=[["[email protected]","[email protected]"],["[email protected]","[email protected]"]],l={},d=(a&&a.groups||[]).concat(c),u={};(a&&a.links||[]).forEach(y=>{[y.email_a,y.email_b].forEach(_=>{_&&(u[_]=u[_]||new Set).add(y.signal)})}),d.forEach((y,_)=>{const $=y.map(E=>l[(E||"").toLowerCase()]).find(Boolean)||"__alias"+_;
2118y.forEach(E=>{l[(E||"").toLowerCase()]=$})});const p=y=>{const _=(y.email||"").toLowerCase();return l[_]?l[_]:(_.split("@")[0]||"").replace(/\+.*/,"").replace(/\./g,"")||"__id"+y.id},m=y=>{const _=(y.subscription_status||"")==="active"&&y.subscription_tier&&y.subscription_tier!=="free"?1:0,w=(y.campaigns_as_gm||0)+(y.campaigns_as_player||0)+(Array.isArray(y.claimed_pcs)?y.claimed_pcs.length:0)+(y.verified?1:0);return _*1e18+w*1e15+(r(y)?1:0)*1e14+(o(y)||0)},f=new Map;t.forEach(y=>{if(y.id===null)return;const _=p(y);(f.get(_)||f.set(_,[]).get(_)).push(y)}),f.forEach(y=>{const _=[...y].sort(($,E)=>m(E)-m($))[0],w=y.filter(r).length;y.forEach($=>{$._clusterSize=y.length,$._isPrimary=$===_,$._linkedEmails=y.filter(E=>E!==$).map(E=>E.email),$._multiActive=y.length>1&&w>=2})});const g={free:"Free",hobby:"Hobby",standard:"Standard",founding_gm:"Founding",master:"Master",pro:"Pro"},h={free:"admin-pill--muted",hobby:"admin-pill--ok",standard:"admin-pill--ok",founding_gm:"admin-pill--gold",master:"admin-pill--teal",pro:"admin-pill--teal"},v=y=>{const _=y.id===null,w=y.display_name||y.email,$=_avatarHtml(y.avatar_url||null,w,26),E=y.is_blocked?'<span class="admin-
2118pill admin-pill--blocked">Blocked</span>':"";let k="\u2014";if(!_){const M=(y.subscription_tier||"free").toLowerCase(),D=g[M]||M,O=h[M]||"admin-pill--muted",N=(y.subscription_status||"").toLowerCase(),G=N==="past_due"||N==="canceled"?` <span style="font-size:0.65rem;color:var(--accent-amber)" title="${escHtml(N)}">${N==="canceled"?"\u2298":"\u26A0"}</span>`:"",F=M==="free"&&y.founding_gm_eligible?' <span class="admin-pill admin-pill--gold" style="font-size:0.6rem;padding:1px 5px" title="Founding GM eligible">elig.</span>':"";let W="";if(y.is_comped)W=' <span class="admin-pill admin-pill--muted" style="font-size:0.6rem;padding:1px 5px" title="Comped: no billing interval">comp</span>';else if(M!=="free"&&["active","past_due","trialing"].includes(N)){const z=(y.subscription_interval||"").toLowerCase();W=` <span class="admin-pill admin-pill--muted" style="font-size:0.6rem;padding:1px 5px" title="Billing interval${z?"":": not on file, priced as monthly"}">${z==="annual"?"annual":z==="monthly"?"monthly":"interval unset"}</span>`}
2118k=`<span class="admin-pill ${O}">${D}</span>${W}${G}${F}`}let C="\u2014";if(!_){const M=y.sessions_used_this_period??0,D=y.sessions_cap??0;D>=100?C=`${M} \xB7 \u221E`:D>0?C=`<span style="${M>=D?"color:var(--accent-amber);font-weight:600":""}">${M}/${D}</span>`:C=`<span style="color:var(--text-3)">${M}/0</span>`}let S="\u2014";if(!_){const M=y.images_used_this_period??0,D=y.images_cap??0,O=y.campaigns_as_gm??0,N=D>0?`AI images this calendar month across all ${O} of this GM's campaign${O===1?"":"s"}, vs their per-account tier allowance of ${D}/month. One shared pool for the whole account (not per campaign).`:"No AI-image entitlement on this tier (free / Player).";if(D>0){const G=M>=D;S=`<span title="${escHtml(N)}" style="${G?"color:var(--accent-amber);font-weight:600":""}">${M}/${D}</span>`}else S=`<span title="${escHtml(N)}" style="color:var(--text-3)">${M}/0</span>`}const q=y.session_credits_remaining??0,I=Math.max(0,Math.floor(Number((y.half_credits||{}).session)||0)),T=_?"\u2014":q>0||I?`<span style="font-weight:600;color:var(--accent-teal)">${_tfCreditCount(q,I)}</span>`:'<span style="color:var(--text-3)">0</span>';let P="";
2118_||(y.behavioral_role==="gm"?P='<span class="admin-pill admin-pill--teal" style="font-size:0.6rem;padding:1px 5px">GM</span>':y.behavioral_role==="player"&&(P='<span class="admin-pill admin-pill--muted" style="font-size:0.6rem;padding:1px 5px">Player</span>'));let B="";if(!_&&y._clusterSize>1){const M=y._multiActive?"admin-pill--warn":"admin-pill--muted",D=y._multiActive?"\u26A0 ":"",O=u[(y.email||"").toLowerCase()],N="Linked (same person): "+(y._linkedEmails||[]).join(", ")+(y._isPrimary?", main account (counted)":", duplicate, counted under the main account")+(O&&O.size?" \xB7 auto-linked by "+[...O].join(", "):"");B=`<span class="admin-pill ${M}" style="font-size:0.6rem;padding:1px 5px" title="${escHtml(N)}">${D}\u{1F517} ${y._clusterSize}</span>`}const H=String(y.last_credit_reason||""),A=/withheld|revoked/.test(H)&&!(y.session_credits_remaining>0)?`<button class="btn-ghost btn-xs admin-user-regrant-btn" data-id="${y.id}" data-reason="${escHtml(H)}"
2119                   title="Restore the 2 trial credits (last event: ${escHtml(H)})">\u21BA Re-grant trial</button>`:"",x=_?"":`
2120              <div style="display:flex;gap:4px;justify-content:flex-end">
2121                ${A}
2122                <button class="btn-ghost btn-xs admin-user-block-btn" data-id="${y.id}" data-blocked="${!!y.is_blocked}"
2123                  style="${y.is_blocked?"color:var(--accent-teal)":"color:var(--accent-amber)"}">
2124                  ${y.is_blocked?"\u2713 Unblock":"\u2298 Block"}
2125                </button>
2126                <button class="btn-ghost btn-xs admin-user-delete-btn" data-id="${y.id}" data-email="${escHtml(y.display_name||y.email)}" data-campaigns="${y.campaigns_as_gm||0}"
2127                  style="color:var(--accent-red)">
2128                  \u{1F5D1} Delete
2129                </button>
2130              </div>`;return`<tr${_?' style="opacity:0.6;font-style:italic"':""}${y.is_blocked?' style="opacity:0.55"':""}>
2131              <td${_?"":` title="User ID: ${y.id}"`}>
2132                <div style="display:flex;align-items:center;gap:0.5rem">
2133                  <span class="le-author-avatar" style="width:26px;height:26px;flex-shrink:0">${$}</span>
2134                  <div>
2135                    ${y.display_name?`<div style="font-weight:600;font-size:0.82rem">${escHtml(y.display_name)}</div>`:""}
2136                    <div style="font-size:0.75rem;color:var(--text-3);display:flex;align-items:center;gap:0.35rem;flex-wrap:wrap">
2137                      ${escHtml(y.email)} ${P} ${B}
2138                    </div>
2139                    ${E}
2140                  </div>
2141                </div>
2142              </td>
2143              <td>
2143${_?"\u2014":`<span class="admin-pill ${y.verified?"admin-pill--ok":"admin-pill--warn"}">${y.verified?"Yes":"No"}</span>`}</td>
2144              <td>${k}</td>
2145              <td style="font-variant-numeric:tabular-nums">${C}</td>
2146              <td style="font-variant-numeric:tabular-nums">${S}</td>
2147              <td style="font-variant-numeric:tabular-nums">${T}</td>
2148              <td>${!_&&y.is_admin?'<span class="admin-pill admin-pill--admin">Admin</span>':""}</td>
2149              <td style="font-variant-numeric:tabular-nums">${_?"\u2014":(()=>{const M=y.campaigns_as_gm??0,D=Array.isArray(y.campaigns_as_gm_names)?y.campaigns_as_gm_names.filter(Boolean):[];return M&&D.length?`<span class="admin-hovercount" title="GM in:
2150${escHtml(D.join(`
2151`))}">${M}</span>`:String(M)})()}</td>
2152              <td style="font-variant-numeric:tabular-nums">${_?"\u2014":(()=>{const M=y.campaigns_as_player??0,D=Array.isArray(y.campaigns_as_player_names)?y.campaigns_as_player_names.filter(Boolean):[];return M&&D.length?`<span class="admin-hovercount" title="Player in:
2153${escHtml(D.join(`
2154`))}">${M}</span>`:String(M)})()}</td>
2155              <td>${(()=>{const M=Array.isArray(y.claimed_pcs)?y.claimed_pcs:[];if(_||!M.length)return'<span style="color:var(--text-3)">\u2014</span>';const D=M.length>1?` <span style="color:var(--text-3)">+${M.length-1}</span>`:"",O=M.map(N=>`${N.pc}${N.campaign?` (${N.campaign})`:""}`).join(", ");return`<span title="${escHtml(O)}">\u{1F3AD} ${escHtml(M[0].pc)}</span>${D}`})()}</td>
2156              <td>${_fmt$(y.ai_cost_total)}</td>
2157              <td>${_fmt$(y.ai_cost_week)}</td>
2158              <td>${(()=>{const M=o(y);if(!M)return"\u2014";const D=!y.last_engaged_at||y.last_visited_at&&new Date(y.last_visited_at).getTime()>new Date(y.last_engaged_at).getTime();return new Date(M).toLocaleDateString()+(D?' <span title="Most recent signal is an app visit (reading), not a created artifact">\u{1F4D6}</span>':"")})()}</td>
2159              <td>${y.created_at?new Date(y.created_at).toLocaleDateString():"\u2014"}</td>
2160              <td>${x}</td>
2161            </tr>`},b=y=>y??null;_adminTable(e,{rows:t,columns:[{label:"User",sort:y=>(y.display_name||y.email||"").toLowerCase()},{label:"Verified"},{label:"Tier",sort:y=>s[(y.subscription_tier||"free").toLowerCase()]||0},{label:"Sessions",sort:y=>y.id===null?null:y.sessions_used_this_period??0},{label:"Images",title:"AI images generated this calendar month vs the per-account tier allowance (one pool across all the GM's campaigns). Matches what the image-quota gate enforces.",sort:y=>y.id===null?null:y.images_used_this_period??0},{label:"Credits",sort:y=>y.id===null?null:y.session_credits_remaining??0},{label:"Admin"},{label:"GM in",title:"Campaigns where this user is the primary GM",sort:y=>y.id===null?null:y.campaigns_as_gm??0,agg:y=>y.campaigns_as_gm??0},{label:"Player in",title:"Campaigns where this user is a player (incl. co-GM)",sort:y=>y.id===null?null:y.campaigns_as_player??0,agg:y=>y.campaigns_as_player??0},{label:"Claimed PC",title:"Character(s) this user has claimed (pc_assignments)",sort:y=>y.id===null?null:Array.isArray(y.claimed_pcs)?y.claimed_pcs.length:0,agg:y=>Array.isArray(y.claimed_pcs)?y.claimed_pcs.length:0},{label:"AI Cost (total)",sort:y=>b(y.ai_cost_total??0),agg:y=>y.ai_cost_total??0,aggFmt:y=>_fmt$(y)},{label:"AI Cost (7d)",sort:y=>b(y.ai_cost_week??0),agg:y=>y.ai_cost_week??0,aggFmt:y=>_fmt$(y)},{label:"Last Active",title:"Most recent real engagement (login, session, recap, AI use, live attendance, share/like) or deliberate app open (\u{1F4D6} = visit-only). Still ignores a left-open tab's background polls.",sort:y=>o(y)},{label:"Joined",sort:y=>y.created_at?new Date(y.created_at).getTime():null},{label:""}],renderRow:v,sections:[{key:"paid_gm",label:"Paid GMs"},{key:"active_gm",label:"Active GMs"},{key:"active_player",label:"Active players"},{key:"inactive",label:"Inactive"},{key:"linked_dupe",label:"Linked duplicates (counted under their main account)",excludeFromTotal:!0},{key:"internal",label:"Internal / test / spam (not counted)",excludeFromTotal:!0}],sectionOf:y=>{const _=new Set([]),w=(y.email||"").toLowerCase();if(y.id===null||w.endsWith("@thread-fall.com")||_.has(w)||y.is_spam)return"internal";if(y._clusterSize>1&&!y._isPrimary)return"linked_dupe";const $=r(y);return(y.subscription_status||"")==="active"&&y.subscription_tier&&y.subscription_tier!=="free"?"paid_gm":y.behavioral_role==="gm"&&$?"active_gm":y.behavioral_role==="player"&&$?"active_player":"inactive"},search:(y,_)=>y.id!==null&&((y.email||"").toLowerCase().includes(_)||(y.display_name||"").toLowerCase().includes(_)),searchPlaceholder:"Search email or name\u2026",filters:[{id:"past_due",label:"Past due",test:y=>(y.subscription_status||"")==="past_due"},{id:"atcap",label:"At/over cap",test:y=>{const _=y.sessions_cap??0;return y.id!==null&&_>0&&_<100&&(y.sessions_used_this_period??0)>=_}},{id:"new30",label:"New 30d",test:y=>y.created_at&&Date.now()-new Date(y.created_at).getTime()<30*864e5},{id:"claimed",label:"Claimed a PC",test:y=>Array.isArray(y.claimed_pcs)&&y.claimed_pcs.length>0},{id:"margin",label:"\u26A0 Margin-neg",test:i}],defaultSort:{idx:10,dir:"desc"}
2161,countNoun:"users",afterRender:y=>{y.querySelectorAll(".admin-user-block-btn").forEach(_=>{_.addEventListener("click",async()=>{const w=parseInt(_.dataset.id),$=_.dataset.blocked!=="true";if(await confirmDialog({title:`${$?"Block":"Unblock"} User`,message:`${$?"Block":"Unblock"} this user?`,confirmLabel:$?"Block":"Unblock",danger:!!$}))try{await api("POST",`/api/admin/users/${w}/block`,{blocked:$}),adminLoadUsers()}catch(E){showToast("Failed: "+E.message)}})}),y.querySelectorAll(".admin-user-regrant-btn").forEach(_=>{_.addEventListener("click",async()=>{const w=parseInt(_.dataset.id);if(await confirmDialog({title:"Re-grant trial credits",message:`Restore the 2 trial credits on this account? Last credit event: ${_.dataset.reason||"unknown"}.`,confirmLabel:"Re-grant"}))try{await api("POST",`/api/admin/users/${w}/trial-regrant`,{}),showToast("Trial credits restored.",4e3),adminLoadUsers()}catch($){showToast("Failed: "+$.message)}})}),y.querySelectorAll(".admin-user-delete-btn").forEach(_=>{_.addEventListener("click",async()=>{const w=parseInt(_.dataset.id),$=_.dataset.email,E=parseInt(_.dataset.campaigns||"0",10),k=E>0?` This will also delete ${E} owned campaign${E===1?"":"s"} (sessions, codex, party, transcripts, everything).`:"";if(await confirmDialog({title:"Delete User",message:`Permanently delete "${$}" and all their data?${k} This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await api("DELETE",`/api/admin/users/${w}`),adminLoadUsers()}catch(C){showToast("Failed: "+C.message)}})})}})}catch(t){e.innerHTML=`<p class="admin-empty">${escHtml(t.message)}</p>`}}async function adminLoadCampaigns(){const e=document.getElementById("admin-campaigns-table-wrap");e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{const t=await api("GET","/api/admin/campaigns");if(!t.length){e.innerHTML='<p class="admin-empty">No campaigns yet.</p>';return}_adminTable(e,{rows:t,columns:[{label:"Name",sort:a=>(a.name||"").toLowerCase()},{label:"GM",sort:a=>(a.gm_email||"").toLowerCase()},{label:"Members",sort:a=>a.member_count??0,agg:a=>a.member_count??0},{label:"Sessions",sort:a=>a.session_count??0,agg:a=>a.session_count??0},{label:"Entries",sort:a=>a.entity_count??0,agg:a=>a.entity_count??0},{label:"AI Cost",sort:a=>a.ai_cost_total??0,agg:a=>a.ai_cost_total??0,aggFmt:a=>_fmt$(a)},{label:"Created",sort:a=>a.created_at?new Date(a.created_at).getTime():null},{label:""}],renderRow:a=>{const s=!!a.is_archived,i=s&&a.archived_at?Math.max(0,Math.ceil((new Date(a.archived_at).getTime()+30*864e5-Date.now())/864e5)):null;return`<tr class="${s?"admin-camp-row--archived":""}">
2162            <td><button class="admin-camp-name-link" data-id="${a.id}" data-name="${escHtml(a.name)}" data-archived="${s?"1":"0"}" title="Campaign ID: ${a.id}, ${s?"open to examine (does NOT restore; campaign stays archived)":"open this campaign"}">${escHtml(a.name)}</button>${s?` <span class="tf-badge tf-badge--amber admin-camp-archived-badge" title="Archived${a.archived_at?" "+new Date(a.archived_at).toLocaleDateString():""}, hard-deleted 30 days after archiving">Archived${i!==null?` \xB7 ${i}d left`:""}</span>`:""}
2162${a.is_public?` <span class="admin-pill admin-pill--teal" title="Public showcase${a.public_slug?" \xB7 /"+a.public_slug:""}">Public</span>`:""}${a.completed_at?` <span class="admin-pill admin-pill--gold" title="Marked completed ${new Date(a.completed_at).toLocaleDateString()}">Completed</span>`:""}${a.is_test?` <span class="admin-pill admin-pill--warn" title="Test campaign: excluded from the Usage tab's behavioral series (cost views still count it)">Test</span>`:""}</td>
2163            <td style="color:var(--text-2)">${escHtml(a.gm_email||"\u2014")}</td>
2164            <td>${a.member_count}</td>
2165            <td>${a.session_count}</td>
2166            <td>${a.entity_count}</td>
2167            <td>${_fmt$(a.ai_cost_total)}</td>
2168            <td>${a.created_at?new Date(a.created_at).toLocaleDateString():"\u2014"}</td>
2169            <td style="display:flex;gap:0.4rem;align-items:center">
2170              ${s?`<button class="btn-ghost btn-xs admin-examine-camp-btn" data-id="${a.id}" data-name="${escHtml(a.name)}" title="Open to examine: does NOT restore (campaign stays archived)">Examine \u2192</button>
2171                   <button class="btn-ghost btn-xs admin-restore-camp-btn" data-id="${a.id}" data-name="${escHtml(a.name)}">\u21A9 Restore</button>`:`<button class="btn-ghost btn-xs admin-enter-camp-btn" data-id="${a.id}">Enter \u2192</button>`}
2172              <button class="btn-ghost btn-xs admin-camp-sessions-btn" data-id="${a.id}" title="Show this campaign's sessions">Sessions</button>
2173              <button class="btn-ghost btn-xs admin-camp-members-btn" data-id="${a.id}" data-name="${escHtml(a.name)}">Members</button>
2174              ${s?"":`<button class="btn-ghost btn-xs admin-camp-completed-btn" data-id="${a.id}" data-name="${escHtml(a.name)}" data-completed="${a.completed_at?"1":"0"}" title="${a.completed_at?"Reopen (clear Completed)":"Mark this campaign Completed"}">${a.completed_at?"\u21A9 Reopen":"\u2713 Complete"}</button>`}
2175              ${s?"":`<button class="btn-ghost btn-xs admin-camp-test-btn" data-id="${a.id}" data-name="${escHtml(a.name)}" data-test="${a.is_test?"1":"0"}" title="${a.is_test?"Unflag: count this campaign in the Usage tab again":"Flag as test: exclude from the Usage tab\u2019s behavioral series"}">${a.is_test?"\u21A9 Untest":"\u2697 Test"}</button>`}
2176              ${s?"":`<button class="btn-ghost btn-ghost--danger btn-xs admin-delete-camp-btn" data-id="${a.id}" data-name="${escHtml(a.name)}" title="Delete campaign">\u{1F5D1}</button>`}
2177            </td>
2178          </tr>
2179          <tr class="admin-camp-members-row hidden" data-camp-id="${a.id}">
2180            <td colspan="8" style="padding:0.75rem 1rem;background:var(--bg)">
2181              <div class="admin-camp-members-body" data-camp-id="${a.id}"></div>
2182            </td>
2183          </tr>
2184          <tr class="admin-camp-sessions-row hidden" data-camp-id="${a.id}">
2185            <td colspan="8" style="padding:0.75rem 1rem;background:var(--bg)">
2186              <div class="admin-camp-sessions-body" data-camp-id="${a.id}"></div>
2187            </td>
2188          </tr>`},sections:[{key:"paid",label:"Paid campaigns"},{key:"active",label:"Active campaigns"},{key:"completed",label:"Completed campaigns"},{key:"inactive",label:"Inactive campaigns"}],sectionOf:a=>a.is_archived?"inactive":a.completed_at?"completed":(a.gm_status||"")==="active"&&a.gm_tier&&a.gm_tier!=="free"&&!a.gm_is_comped?"paid":a.last_session_at&&Date.now()-new Date(a.last_session_at).getTime()<60*864e5?"active":"inactive",search:(a,s)=>(a.name||"").toLowerCase().includes(s)||(a.gm_email||"").toLowerCase().includes(s),searchPlaceholder:"Search campaign or GM\u2026",filters:[{id:"public",label:"Public",test:a=>!!a.is_public},{id:"archived",label:"Archived",test:a=>!!a.is_archived},{id:"zerosess",label:"Zero sessions",test:a=>(a.session_count||0)===0},{id:"test",label:"Test",test:a=>!!a.is_test},{id:"hascost",label:"Cost > $0.10",test:a=>(a.ai_cost_total||0)>.1}],defaultSort:{idx:3,dir:"desc"},countNoun:"campaigns",afterRender:a=>{a.querySelectorAll(".admin-enter-camp-btn").forEach(s=>{s.addEventListener("click",async()=>{await bootWithCampaignId(parseInt(s.dataset.id)),applyRoleUI()})}),a.querySelectorAll(".admin-camp-name-link, .admin-examine-camp-btn").forEach(s=>{s.addEventListener("click",async()=>{(s.dataset.archived==="1"||s.classList.contains("admin-examine-camp-btn"))&&showToast(`Opening archived "${s.dataset.name}" to examine; it stays archived (not restored).`),await bootWithCampaignId(parseInt(s.dataset.id)),applyRoleUI()})}),a.querySelectorAll(".admin-restore-camp-btn").forEach(s=>{s.addEventListener("click",async()=>{const i=s.textContent;s.disabled=!0,s.textContent="Restoring\u2026";try{await api("POST",`/api/campaigns/${s.dataset.id}/restore`),showToast(`Restored "${s.dataset.name}".`),adminLoadCampaigns()}catch(o){showToast("Restore failed: "+o.message),s.disabled=!1,s.textContent=i}})}),a.querySelectorAll(".admin-camp-members-btn").forEach(s=>{s.addEventListener("click",async()=>{const i=parseInt(s.dataset.id),o=a.querySelector(`.admin-camp-members-row[data-camp-id="${i}"]`),r=a.querySelector(`.admin-camp-members-body[data-camp-id="${i}"]`);o.classList.contains("hidden")?(o.classList.remove("hidden"),await _adminLoadCampMembers(i,r)):o.classList.add("hidden")})}),a.querySelectorAll(".admin-camp-sessions-btn").forEach(s=>{s.addEventListener("click",async()=>{const i=parseInt(s.dataset.id),o=a.querySelector(`.admin-camp-sessions-row[data-camp-id="${i}"]`),r=a.querySelector(`.admin-camp-sessions-body[data-camp-id="${i}"]`);o.classList.contains("hidden")?(o.classList.remove("hidden"),await _adminLoadCampSessions(i,r)):o.classList.add("hidden")})}),a.querySelectorAll(".admin-delete-camp-btn").forEach(s=>{s.addEventListener("click",()=>{_openCampDeleteModal(parseInt(s.dataset.id),s.dataset.name,"admin")})}),a.querySelectorAll(".admin-camp-completed-btn").forEach(s=>{s.addEventListener("click",async()=>{const i=s.dataset.completed!=="1";if(!(i&&!await confirmDialog({title:"Mark Completed",message:`Mark "${s.dataset.name}" as Completed? It moves to the Completed section. This does NOT archive or delete it.`,confirmLabel:"Mark Completed"})))try{await api("POST",`/api/admin/campaigns/${s.dataset.id}/completed`,{completed:i}),showToast(i?`Marked "${s.dataset.name}" completed.`:`Reopened "${s.dataset.name}".`),adminLoadCampaigns()}catch(o){showToast("Failed: "+o.message)}})}),a.querySelectorAll(".admin-camp-test-btn").forEach(s=>{s.addEventListener("click",async()=>{const i=s.dataset.test!=="1";try{await api("POST",`/api/admin/campaigns/${s.dataset.id}/test-flag`,{is_test:i}),showToast(i?`"${s.dataset.name}" flagged as test, excluded from Usage.`:`"${s.dataset.name}" unflagged, counted in Usage again.`),adminLoadCampaigns()}catch(o){showToast("Failed: "+o.message)}})})}})}catch(t){e.innerHTML=`<p class="admin-empty">${escHtml(t.message)}</p>`}}async function _adminLoadCampMembers(e,t){t.innerHTML='<span class="spinner"></span>';try{const[n,a]=await Promise.all([api("GET",`/api/admin/campaigns/${e}/members`),api("GET","/api/admin/users")]),s=new Set(n.map(d=>
2188d.user_id)),i=a.filter(d=>d.id&&!s.has(d.id)&&d.verified),o=d=>d.role==="gm"?"GM":d.role==="co_gm"?"Co-GM":d.role==="observer"?"Observer":"Player",r=n.map(d=>{const u=d.display_name?`<strong>${escHtml(d.display_name)}</strong> <span style="color:var(--text-3);font-size:0.78rem">${escHtml(d.email)}</span>`:escHtml(d.email);return`<tr>
2189        <td title="User ID: ${d.user_id}">${u}</td>
2190        <td><span class="camp-role-badge camp-role-${d.role==="gm"?"gm":d.role==="co_gm"?"co-gm":d.role==="observer"?"observer":"member"}">${o(d)}</span></td>
2191        <td><button class="btn-ghost btn-ghost--danger btn-xs" data-uid="${d.user_id}" data-cid="${e}">Remove</button></td>
2192      </tr>`}).join(""),c=`admin-add-member-list-${e}`,l=i.map(d=>{const u=d.display_name?`${d.display_name} (${d.email})`:d.email;return`<option value="${escHtml(d.email)}" data-uid="${d.id}">${escHtml(u)}</option>`}).join("");t.innerHTML=`
2193      <table class="admin-table" style="margin-bottom:0.75rem">
2194        <thead><tr><th>User</th><th>Role</th><th></th></tr></thead>
2195        <tbody>${r||'<tr><td colspan="3" style="color:var(--text-3)">No members</td></tr>'}</tbody>
2196      </table>
2197      <datalist id="${c}">${l}</datalist>
2198      <div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
2199        <input class="admin-add-member-input" list="${c}" placeholder="${i.length?"Search by name or email\u2026":"No other users to add"}"
2200          ${i.length?"":"disabled"}
2201          style="flex:1;min-width:220px;padding:0.3rem 0.6rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg);color:var(--text);font-size:var(--font-size-sm)" />
2202        <select class="admin-add-member-role" style="padding:0.3rem 0.6rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg);color:var(--text);font-size:var(--font-size-sm)">
2203          <option value="member">Player</option>
2204          <option value="co_gm">Co-GM</option>
2205          <option value="gm">GM</option>
2206          <option value="observer">Observer</option>
2207        </select>
2208        <button class="btn-new btn-xs admin-add-member-btn" ${i.length?"":"disabled"}>+ Add</button>
2209      </div>`,t.querySelectorAll("[data-uid]").forEach(d=>{d.addEventListener("click",async()=>{if(await confirmDialog({title:"Remove Member",message:"Remove this member from the campaign?",confirmLabel:"Remove"}))try{await api("DELETE",`/api/admin/campaigns/${d.dataset.cid}/members/${d.dataset.uid}`),await _adminLoadCampMembers(e,t)}catch(u){showToast("Failed: "+u.message)}})}),t.querySelector(".admin-add-member-btn").addEventListener("click",async()=>{const d=t.querySelector(".admin-add-member-input"),u=t.querySelector(".admin-add-member-role").value,p=d.value.trim();if(!p){showToast("Select a user to add");return}const m=p.match(/\(([^)]+)\)$/),f=m?m[1]:p;try{await api("POST",`/api/admin/campaigns/${e}/members`,{email:f,role:u}),await _adminLoadCampMembers(e,t)}catch(g){showToast("Failed: "+g.message)}})}catch(n){t.innerHTML=`<span style="color:var(--accent-red)">${escHtml(n.message)}</span>`}}async function _adminLoadCampSessions(e,t){t.innerHTML='<span class="spinner"></span>';try{const n=await api("GET",`/api/admin/campaigns/${e}/sessions`);if(!n.length){t.innerHTML='<p class="admin-empty">No sessions.</p>';return}const a=r=>r?new Date(r).toLocaleDateString():"\u2014",s=n.map(r=>`
2210      <tr>
2211        <td>${escHtml(r.title||"Untitled")}</td>
2212        <td>${a(r.date||r.created_at)}</td>
2213        <td>
2213${r.is_live_transcript?'<span class="admin-pill admin-pill--teal">Live</span>':'<span class="admin-pill admin-pill--muted">Manual</span>'}</td>
2214        <td style="font-variant-numeric:tabular-nums">${r.recap_run_count}${r.recap_run_count>1?' <span style="color:var(--accent-amber)" title="re-recapped: repeated recap cost">\u21BB</span>':""}</td>
2215        <td>${r.last_recap_cost_usd!=null?_fmt$(r.last_recap_cost_usd):"\u2014"}</td>
2216        <td>${a(r.last_recap_at)}</td>
2217      </tr>`).join(""),i=n.reduce((r,c)=>r+(c.last_recap_cost_usd||0),0),o=n.filter(r=>r.is_live_transcript).length;t.innerHTML=`
2218      <table class="admin-table">
2219        <thead><tr><th>Session</th><th>Date</th><th>Type</th><th>Recap runs</th><th>Last recap $</th><th>Last recap</th></tr></thead>
2220        <tbody>${s}</tbody>
2221        <tfoot><tr class="admin-tbl-subtotal admin-tbl-subtotal--grand"><td>${n.length} sessions \xB7 ${o} live</td><td></td><td></td><td></td><td>${_fmt$(i)}</td><td></td></tr></tfoot>
2222      </table>`}catch(n){t.innerHTML=`<p class="admin-empty">${escHtml(n.message)}</p>`}}async function adminLoadAiUsage(){const e=document.getElementById("admin-ai-usage-wrap");e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{const t=await api("GET","/api/admin/ai-usage"),n=(d,u,p)=>u.length?`<table class="admin-table"><thead><tr>${d.map(m=>`<th>${m}</th>`).join("")}</tr></thead>
2223        <tbody>${u.join("")}</tbody></table>`:`<p class="admin-empty">${p}</p>`,a=t.by_day.map(d=>`<tr>
2224      <td>${d.day}</td><td>${d.calls}</td>
2225      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2226      <td>${_fmt$(d.cost_usd)}</td></tr>`),s=t.by_endpoint.map(d=>`<tr>
2227      <td><code style="font-size:0.78rem">${escHtml(d.endpoint)}</code></td><td>${d.calls}</td>
2228      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2229      <td>${_fmt$(d.cost_usd)}</td></tr>`),i=t.by_model.map(d=>`<tr>
2230      <td><code style="font-size:0.78rem">${escHtml(d.model)}</code></td><td>${d.calls}</td>
2231      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2232      <td>${_fmt$(d.cost_usd)}</td></tr>`),o=t.by_user.map(d=>`<tr>
2233      <td>
2234        <div style="font-weight:600;font-size:0.82rem">${escHtml(d.user_label)}</div>
2235        ${d.user_label!==d.email?`<div style="font-size:0.72rem;color:var(--text-3)">${escHtml(d.email)}</div>`:""}
2236      </td>
2237      <td>${d.calls}</td>
2238      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2239      <td>${_fmt$(d.cost_usd)}</td>
2240      <td style="font-size:0.75rem;color:var(--text-3)">${d.last_call_at?new Date(d.last_call_at).toLocaleDateString():"\u2014"}</td></tr>`),r=t.by_campaign.map(d=>`<tr>
2241      <td><strong>${escHtml(d.campaign_name)}</strong></td>
2242      <td>${d.calls}</td>
2243      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2244      <td>${_fmt$(d.cost_usd)}</td>
2245      <td style="font-size:0.75rem;color:var(--text-3)">${d.last_call_at?new Date(d.last_call_at).toLocaleDateString():"\u2014"}</td></tr>`),c=t.recent.map(d=>`<tr>
2246      <td style="font-size:0.75rem;color:var(--text-3);white-space:nowrap">${d.created_at?new Date(d.created_at).toLocaleString():"\u2014"}</td>
2247      <td style="font-size:0.75rem">${escHtml(d.user_label)}</td>
2248      <td style="font-size:0.75rem;color:var(--text-2)">${escHtml(d.campaign_name||"\u2014")}</td>
2249      <td><code style="font-size:0.72rem">${escHtml(d.endpoint)}</code></td>
2250      <td style="font-size:0.75rem;color:var(--text-3)">${escHtml(d.model||"\u2014")}</td>
2251      <td>${_fmtK(d.input_tokens)}</td><td>${_fmtK(d.output_tokens)}</td>
2252      <td>${_fmt$(d.cost_usd)}</td></tr>`),l=["","Calls","Tok In","Tok Out","Cost"];e.innerHTML=`
2253      <div class="admin-ai-grid">
2254        <div class="admin-ai-col">
2255          <div class="admin-section-hdr">By User (all-time, top 50)</div>
2256          ${n(["User","Calls","Tok In","Tok Out","Cost","Last Call"],o,"No data yet.")}
2257        </div>
2258        <div class="admin-ai-col">
2259          <div class="admin-section-hdr">By Campaign (all-time, top 50)</div>
2260          ${n(["Campaign","Calls","Tok In","Tok Out","Cost","Last Call"],r,"No data yet.")}
2261        </div>
2262      </div>
2263
2264      <div class="admin-ai-grid">
2265        <div class="admin-ai-col">
2266          <div class="admin-section-hdr">By Day (last 30 days)</div>
2267          ${n(["Date",...l.slice(1)],a,"No data yet.")}
2268        </div>
2269        <div class="admin-ai-col">
2270          <div class="admin-section-hdr">By Endpoint</div>
2271          ${n(["Endpoint",...l.slice(1)],s,"No data yet.")}
2272          <div class="admin-section-hdr" style="margin-top:1.5rem">By Model</div>
2273          ${n(["Model",...l.slice(1)],i,"No data yet.")}
2274        </div>
2275      </div>
2276
2277      <div class="admin-section-hdr">Recent Calls (last 100)</div>
2278      ${n(["Time","User","Campaign","Endpoint","Model","Tok In","Tok Out","Cost"],c,"No calls recorded yet.")}
2279    `}catch(t){e.innerHTML=`<p class="admin-empty">${escHtml(t.message)}</p>`}}async function adminLoadAffiliates(){const e=document.getElementById("admin-affiliates-wrap");e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{const n=(await api("GET","/api/admin/affiliates")).affiliates||[];let s=`
2280      <div class="admin-section-hdr">\u{1F91D} Affiliates</div>
2281      <p class="form-helper-text" style="margin-bottom:1rem">
2282        Baseline commission: 20% of a referred user's payments for their first 12 months, 10% thereafter.
2283        Commission accrues automatically on every <code>invoice.payment_succeeded</code> for a referred user.
2284        Stripe Connect payouts (the affiliate enters their own banking via Stripe-hosted onboarding) are Phase 2.
2285      </p>
2286
2287      <span class="form-section-label">+ New Affiliate</span>
2288      <div class="admin-actions-bar affiliate-form-row">
2289        <input type="email" id="aff-new-email" placeholder="email (required)" class="affiliate-input" />
2290        <input type="text" id="aff-new-name" placeholder="display name" class="affiliate-input" />
2291        <input type="number" id="aff-new-userid" placeholder="Threadfall user id (optional)" class="affiliate-input affiliate-input--num" />
2292        <button class="btn-new btn-sm" id="aff-new-btn">Create</button>
2293      </div>
2294      <p id="aff-new-err" class="error hidden"></p>
2295
2296      <span class="form-section-label" style="margin-top:1rem;display:inline-block">Set Referral: attribute a user to an affiliate</span>
2297      <div class="admin-actions-bar affiliate-form-row">
2298        <input type="number" id="aff-refer-userid" placeholder="user id to attribute" class="affiliate-input affiliate-input--num" />
2299        <select id="aff-refer-affid" class="affiliate-input">
2300          <option value="">(clear referral)</option>
2301          ${n.map(i=>`<option value="${i.id}">${escHtml(i.display_name||i.email)} (#${i.id})</option>`).join("")}
2302        </select>
2303        <button class="btn-new btn-sm" id="aff-refer-btn">Assign</button>
2304      </div>
2305      <p id="aff-refer-err" class="error hidden"></p>
2306
2307      <span class="form-section-label" style="margin-top:1.25rem;display:inline-block">All Affiliates</span>
2308    `;n.length?(s+=`<table class="admin-table"><thead><tr>
2309        <th>Affiliate</th><th>Linked user</th><th class="num">Referred</th>
2310        <th class="num">Accrued</th><th class="num">Paid</th><th>Connect</th><th></th>
2311      </tr></thead><tbody>`,n.forEach(i=>{const o=i.has_connect_account?escHtml(i.onboarding_status):'<span style="color:var(--text-3)">not started</span>';s+=`<tr>
2312          <td><strong>${escHtml(i.display_name||i.email)}</strong><br>
2313              <span style="color:var(--text-3);font-size:0.8rem">${escHtml(i.email)}</span></td>
2314          <td>${i.user_id?"#"+i.user_id:"\u2014"}</td>
2315          <td class="num">${i.referred_users}</td>
2316          <td class="num">${_fmt$((i.accrued_cents||0)/100)}</td>
2317          <td class="num">${_fmt$((i.paid_cents||0)/100)}</td>
2318          <td>${o}</td>
2319          <td><button class="btn-ghost btn-xs aff-ledger-btn" data-id="${i.id}">Ledger</button></td>
2320        </tr>`}),s+="</tbody></table>"):s+='<p class="placeholder--padded">No affiliates yet. Create one above.</p>',s+='<div id="aff-ledger-view"></div>',e.innerHTML=s,document.getElementById("aff-new-btn")?.addEventListener("click",_adminCreateAffiliate),document.getElementById("aff-refer-btn")?.addEventListener("click",_adminAssignReferral),e.querySelectorAll(".aff-ledger-btn").forEach(i=>
2320i.addEventListener("click",()=>_adminViewAffiliateLedger(i.dataset.id)))}catch(t){e.innerHTML=`<p class="placeholder--padded error">Failed to load affiliates: ${escHtml(t.message)}</p>`}}async function _adminCreateAffiliate(){const e=document.getElementById("aff-new-email").value.trim(),t=document.getElementById("aff-new-name").value.trim(),n=document.getElementById("aff-new-userid").value.trim(),a=document.getElementById("aff-new-err");if(a.classList.add("hidden"),!e){a.textContent="Email is required.",a.classList.remove("hidden");return}const s={email:e};t&&(s.display_name=t),n&&(s.user_id=parseInt(n,10));try{await api("POST","/api/admin/affiliates",s),showToast("Affiliate created.",3e3),await adminLoadAffiliates()}catch(i){a.textContent=i.message,a.classList.remove("hidden")}}async function _adminAssignReferral(){const e=document.getElementById("aff-refer-userid").value.trim(),t=document.getElementById("aff-refer-affid").value,n=document.getElementById("aff-refer-err");if(n.classList.add("hidden"),!e){n.textContent="User id is required.",n.classList.remove("hidden");return}try{await api("POST",`/api/admin/users/${parseInt(e,10)}/refer`,{affiliate_id:t?parseInt(t,10):null}),showToast(t?"Referral assigned.":"Referral cleared.",3e3),await adminLoadAffiliates()}catch(a){n.textContent=a.message,n.classList.remove("hidden")}}async function _adminViewAffiliateLedger(e){const t=document.getElementById("aff-ledger-view");t.innerHTML='<div class="loading-row" style="justify-content:center;padding:1rem"><span class="spinner"></span></div>';try{const n=await api("GET",`/api/admin/affiliates/${e}/commissions`),a=n.summary||{},s=a.accrued_cents||0;let i=`<span class="form-section-label" style="margin-top:1.25rem;display:inline-block">
2321      Ledger: ${escHtml(n.affiliate.display_name||n.affiliate.email)}
2322      <span style="color:var(--text-3);font-weight:400;text-transform:none;letter-spacing:0;margin-left:0.6rem">
2323        ${_fmt$(s/100)} accrued \xB7 ${_fmt$((a.paid_cents||0)/100)} paid \xB7 ${a.total_rows||0} rows
2324      </span></span>`;s>0&&(i+=`<div class="admin-actions-bar affiliate-form-row" style="margin-top:0.5rem">
2325        <span style="color:var(--text-2);font-size:var(--font-size-sm)">Pay out ${_fmt$(s/100)} accrued:</span>
2326        <input type="text" id="aff-payout-ref" placeholder="Wise transfer reference (required)" class="affiliate-input" />
2327        <select id="aff-payout-method" class="affiliate-input affiliate-input--num">
2328          <option value="wise">Wise</option>
2329          <option value="paypal">PayPal</option>
2330          <option value="other">Other</option>
2331        </select>
2332        <button class="btn-new btn-sm" id="aff-payout-btn" data-id="${e}">Record payout</button>
2333      </div>
2334      <p id="aff-payout-err" class="error hidden"></p>`),n.commissions.length?(i+=`<table class="admin-table"><thead><tr>
2335        <th>Date</th><th>Referred user</th><th class="num">Gross</th><th class="num">Rate</th>
2336        <th class="num">Commission</th><th>Status</th><th>Period</th><th>Payout ref</th>
2337      </tr></thead><tbody>`,n.commissions.forEach(r=>{i+=`<tr>
2338          <td>${(r.created_at||"").slice(0,10)}</td>
2339          <td>${escHtml(r.referred_email||"#"+(r.referred_user_id||"?"))}</td>
2340          <td class="num">${_fmt$((r.gross_amount_cents||0)/100)}</td>
2341          <td class="num">${escHtml(r.rate_label||"")}</td>
2342          <td class="num">${_fmt$((r.commission_cents||0)/100)}</td>
2343          <td>${escHtml(r.status)}</td>
2344          <td>${(r.period_month||"").slice(0,7)}</td>
2345          <td>${r.payout_reference?escHtml(r.payout_reference):"\u2014"}</td>
2346        </tr>`}),i+="</tbody></table>"):i+='<p class="placeholder--padded">No commission rows yet. Accrues on the next invoice payment by a referred user.</p>',t.innerHTML=i;const o=document.getElementById("aff-payout-btn");o&&o.addEventListener("click",()=>_adminRecordPayout(e)),t.scrollIntoView({behavior:"smooth",block:"nearest"})}catch(n){t.innerHTML=`<p class="placeholder--padded error">Failed to load ledger: ${escHtml(n.message)}</p>`}}async function _adminRecordPayout(e){const t=document.getElementById("aff-payout-ref").value.trim(),n=document.getElementById("aff-payout-method").value,a=document.getElementById("aff-payout-err");if(a.classList.add("hidden"),!t){a.textContent="Payout reference is required, paste the Wise transfer id.",a.
2346classList.remove("hidden");return}if(await confirmDialog({title:"Record payout",message:"This marks every accrued commission row for this affiliate as paid. It does NOT move money, record this only AFTER you've actually sent the Wise transfer.",confirmLabel:"Mark paid"}))try{const s=await api("POST",`/api/admin/affiliates/${e}/record-payout`,{payout_reference:t,payout_method:n});showToast(s.paid_count?`Recorded: ${s.paid_count} row(s), ${_fmt$((s.paid_cents||0)/100)} marked paid.`:s.message||"Nothing to pay out.",4e3),await adminLoadAffiliates(),await _adminViewAffiliateLedger(e)}catch(s){a.textContent=s.message,a.classList.remove("hidden")}}let _adminCommsCache=[];async function adminLoadComms(){const e=document.getElementById("admin-comms-wrap");e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';try{const t=await api("GET","/api/admin/scheduled-emails"),n=t.emails||[],a=t.caps||[],s=t.audiences||{};_adminCommsCache=n;const i=a.map(c=>`<span class="comms-cap ${c.used>=c.cap?"comms-cap--full":""}">${escHtml(c.label)} <strong>${c.used}/${c.cap}</strong></span>`).join(""),o=Object.entries(s).map(([c,l])=>`<option value="${escHtml(c)}">${escHtml(l)}</option>`).join("");let r=`
2347      <div class="admin-section-hdr">\u{1F4E3} Communications</div>
2348      <p class="form-helper-text" style="margin-bottom:0.5rem">
2349        Scheduled broadcast emails: a daily cron sends anything due. Hard monthly cap:
2350        Free/Hobby/Standard see 2, Master/Pro see 3; "All users" sends count against both.
2351      </p>
2352      <div class="comms-cap-row">
2353        <span class="form-section-label" style="margin:0">${escHtml(t.month||"")} usage</span>
2354        ${i}
2355      </div>
2356
2357      <span class="form-section-label" style="margin-top:1.25rem;display:inline-block" id="comms-form-title">+ Schedule new email</span>
2358      <div class="comms-form">
2359        <input type="hidden" id="comms-edit-id" value="" />
2360        <div class="comms-form-row">
2361          <input type="text" id="comms-subject" placeholder="Subject line" class="form-input" />
2362          <select id="comms-audience" class="form-input">${o}</select>
2363          <input type="date" id="comms-date" class="form-input" />
2364        </div>
2365        <input type="text" id="comms-preheader" placeholder="Preheader / inbox preview text (optional)" class="form-input" />
2366        <div class="comms-form-row">
2367          <input type="text" id="comms-image" placeholder="Hero image URL (optional)" class="form-input" />
2368          <input type="text" id="comms-cta-label" placeholder="CTA button label (optional)" class="form-input" />
2369          <input type="text" id="comms-cta-url" placeholder="CTA button URL (optional)" class="form-input" />
2370        </div>
2371        <textarea id="comms-body" rows="7" placeholder="Body HTML: the inner content (paragraphs, lists). Wrapped automatically with the marketing scaffold + unsubscribe link." class="form-input comms-body-input"></textarea>
2372        <div class="admin-actions-bar" style="margin-top:0.4rem">
2373          <button class="btn-new btn-sm" id="comms-save-btn">Schedule email</button>
2374          <button class="btn-ghost btn-sm hidden" id="comms-cancel-edit-btn">Cancel edit</button>
2375        </div>
2376        <p id="comms-form-err" class="error hidden"></p>
2377      </div>
2378
2379      <span class="form-section-label" style="margin-top:1.5rem;display:inline-block">Queue &amp; history</span>
2380    `;n.length?(r+='<div class="comms-list">',n.forEach(c=>{const l=s[c.audience]||c.audience;let d=`${escHtml(l)} \xB7 ${escHtml(c.scheduled_for||"")}`;c.status==="sent"&&(d+=` \xB7 ${c.recipient_count!=null?c.recipient_count:0} sent`),c.status==="failed"&&c.send_error&&(d+=` \xB7 ${escHtml(c.send_error)}`);const u=c.status==="pending"?`<div class="comms-card-actions">
2381               <button class="btn-ghost btn-xs comms-edit-btn" data-id="${c.id}">\u270E Edit</button>
2382               <button class="btn-ghost btn-xs btn-ghost--danger comms-cancel-btn" data-id="${c.id}">Cancel</button>
2383             </div>`:"";r+=`<div class="comms-card comms-card--${escHtml(c.status)}">
2384          <div class="comms-card-main">
2385            <div class="comms-card-subject">${escHtml(c.subject)}</div>
2386            <div class="comms-card-meta"><span class="comms-status comms-status--${escHtml(c.status)}">${escHtml(c.status)}</span>${d}</div>
2387          </div>
2388          ${u}
2389        </div>`}),r+="</div>"):r+='<p class="placeholder--padded">No scheduled emails yet. Compose one above.</p>',e.innerHTML=r,document.getElementById("comms-save-btn")?.addEventListener("click",_adminSaveScheduledEmail),document.getElementById("comms-cancel-edit-btn")?.addEventListener("click",_adminCommsResetForm),e.querySelectorAll(".comms-edit-btn").forEach(c=>c.addEventListener("click",()=>_adminCommsEditEmail(c.dataset.id))),e.querySelectorAll(".comms-cancel-btn").forEach(c=>c.addEventListener("click",()=>_adminCommsCancelEmail(c.dataset.id)))}catch(t){e.innerHTML=`<p class="placeholder--padded error">Failed to load communications: ${escHtml(t.message)}</p>`}}function _adminCommsResetForm(){["comms-edit-id","comms-subject","comms-preheader","comms-image","comms-cta-label","comms-cta-url","comms-body","comms-date"].forEach(t=>{const n=document.getElementById(t);n&&(n.value="")}
2389);const e=document.getElementById("comms-audience");e&&(e.selectedIndex=0),document.getElementById("comms-form-title").textContent="+ Schedule new email",document.getElementById("comms-save-btn").textContent="Schedule email",document.getElementById("comms-cancel-edit-btn").classList.add("hidden"),document.getElementById("comms-form-err").classList.add("hidden")}function _adminCommsEditEmail(e){const t=_adminCommsCache.find(s=>String(s.id)===String(e));if(!t)return;const n=(s,i)=>{const o=document.getElementById(s);o&&(o.value=i||"")};n("comms-edit-id",t.id),n("comms-subject",t.subject),n("comms-preheader",t.preheader),n("comms-image",t.image_url),n("comms-cta-label",t.cta_label),n("comms-cta-url",t.cta_url),n("comms-body",t.body_html),n("comms-date",t.scheduled_for);const a=document.getElementById("comms-audience");a&&(a.value=t.audience||"all"),document.getElementById("comms-form-title").textContent=`\u270E Editing: ${t.subject}`,document.getElementById("comms-save-btn").textContent="Save changes",document.getElementById("comms-cancel-edit-btn").classList.remove("hidden"),document.getElementById("comms-form-err").classList.add("hidden"),document.getElementById("comms-subject").scrollIntoView({behavior:"smooth",block:"center"})}async function _adminSaveScheduledEmail(){const e=document.getElementById("comms-form-err");e.classList.add("hidden");const t=s=>(document.getElementById(s).value||"").trim(),n=t("comms-edit-id"),a={subject:t("comms-subject"),preheader:t("comms-preheader"),body_html:t("comms-body"),image_url:t("comms-image"),cta_label:t("comms-cta-label"),cta_url:t("comms-cta-url"),audience:document.getElementById("comms-audience").value,scheduled_for:t("comms-date")};if(!a.subject||!a.body_html){e.textContent="Subject and body are both required.",e.classList.remove("hidden");return}if(!a.scheduled_for){e.textContent="Pick a send date.",e.classList.remove("hidden");return}try{n?(await api("PATCH",`/api/admin/scheduled-emails/${n}`,a),showToast("Scheduled email updated.",3e3)):(await api("POST","/api/admin/scheduled-emails",a),showToast("Email scheduled.",3e3)),_adminCommsResetForm(),await adminLoadComms()}catch(s){e.textContent=s.message,e.classList.remove("hidden")}}async function _adminCommsCancelEmail(e){if(confirm("Cancel this scheduled email? It will not be sent."))try{await api("PATCH",`/api/admin/scheduled-emails/${e}`,{status:"cancelled"}),showToast("Scheduled email cancelled.",3e3),await adminLoadComms()}catch(t){showToast("Cancel failed: "+t.message,5e3)}}const _RN_LABELS=["Changed","Bug Fix","New Feature"];let _adminSearchDays=28;async function adminLoadSearchConsole(){const e=document.getElementById("admin-search-console-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t;try{t=await api("GET",`/api/admin/search-console?days=${_adminSearchDays}`)}catch(k){e.innerHTML=`<p class="placeholder--padded error">Failed to load Search Console data: ${escHtml(k.message)}</p>`;return}const n=k=>Number(k||0).toLocaleString(),a=k=>(Number(k||0)*100).toFixed(1)+"%",s=k=>Number(k||0).toFixed(1),i=k=>k?new Date(k).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"never",o=(k,C)=>{if(!C)return"";const S=(k-C)/C*100,q=S>=0?"+":"";return` <span class="form-helper-text" title="vs the prior ${t.window_days} days">(${q}${S.toFixed(0)}% vs prior)</span>`},r=k=>{try{return new URL(k).pathname||"/"}catch{return k||""}},c=(k,C,S)=>C.length?`<div class="admin-table-wrap"><table class="admin-table"><thead><tr>${k.map(q=>`<th>${escHtml(q)}</th>`).join("")}</tr></thead><tbody>${C.map(q=>`<tr>${q.map(I=>`<td>${I}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`:`<div class="admin-empty">${escHtml(S)}</div>`,l=t.totals||{},d=t.configured?`<p class="form-helper-text">Property <strong>${escHtml(t.site)}</strong> \xB7 last pull ${escHtml(i(t.last_sync_at))}${t.data_span&&t.data_span.rows?` \xB7 data ${escHtml(t.data_span.first_day)} \u2192 ${escHtml(t.data_span.last_day)} (${n(t.data_span.rows)}
2389 rows)`:" \xB7 no rows yet"} \xB7 Google finalises a day 2 to 3 days late.</p>`:'<p class="placeholder--padded error">GSC_SERVICE_ACCOUNT_JSON is not set on this deployment, so nothing can be pulled.</p>',u=(t.candidates||[]).map(k=>[`<strong>${escHtml(k.key)}</strong>`,n(k.impressions),n(k.clicks),s(k.position),`<span title="${escHtml(k.page)}">${escHtml(r(k.page))}</span>`]),p=(t.queries||[]).slice(0,100).map(k=>[escHtml(k.key),n(k.clicks),n(k.impressions),a(k.ctr),s(k.position),`<span title="${escHtml(k.page)}">${escHtml(r(k.page))}</span>`]),m=(t.pages||[]).slice(0,60).map(k=>[`<span title="${escHtml(k.key)}">${escHtml(r(k.key))}</span>`,n(k.clicks),n(k.impressions),a(k.ctr),s(k.position),escHtml(k.query||"")]),f=t.index||{},g=f.counts||{},h=f.sitemap||{},v=k=>!!k&&Date.now()-new Date(k).getTime()<7*864e5,b=k=>k?new Date(k).toLocaleDateString(void 0,{month:"short",day:"numeric"}):"never",y=k=>{const C={indexed:["ok","Indexed"],not_indexed:["warn","Not indexed"],noindex:["muted","Noindex on purpose"],unchecked:["muted","Not checked yet"],error:["blocked","Check failed"]},[S,q]=C[k.state]||["muted",k.state];
2389return`<span class="admin-pill admin-pill--${S}">${escHtml(q)}</span>`},_={not_indexed:0,error:1,unchecked:2,indexed:3,noindex:4},w=(f.pages||[]).slice().sort((k,C)=>(_[k.state]??9)-(_[C.state]??9)||(v(C.changed_at)?1:0)-(v(k.changed_at)?1:0)||k.path.localeCompare(C.path)).map(k=>[`<a href="${escHtml(k.url)}" target="_blank" rel="noopener">${escHtml(k.path)}</a>`,y(k),escHtml(k.state==="error"?k.error||"":k.coverage||""),escHtml(b(k.last_crawl)),escHtml(i(k.checked_at)),v(k.changed_at)?`<strong>Changed ${escHtml(b(k.changed_at))}</strong><br><span class="form-helper-text">was ${escHtml(k.prev_coverage||k.prev_verdict||"unknown")}</span>`:""]),$=f.error?`<p class="placeholder--padded error">${escHtml(f.error)}</p>`:`<p class="form-helper-text"><strong>${n(g.indexed)} of ${n(f.indexable)}</strong> indexable pages indexed`+(g.not_indexed?` \xB7 <strong>${n(g.not_indexed)}</strong> not indexed`:"")+(g.error?` \xB7 ${n(g.error)} check failed`:"")+(g.unchecked?` \xB7 ${n(g.unchecked)} not checked yet`:"")+(g.noindex?` \xB7 ${n(g.noindex)} noindex on purpose`:"")+` \xB7 ${n(f.batch)} pages checked an hour, a full pass about every ${n(f.cycle_hours)} hours, last check ${escHtml(i(f.last_tick))}`+(h.submitted?` \xB7 sitemap ${n(h.submitted)} URLs, read by Google ${escHtml(i(h.downloaded_at))}, ${n(h.errors)} errors, ${n(h.warnings)} warnings`:"")+(h.last_submitted?` \xB7 last pushed to Google ${escHtml(i(h.last_submitted))}`:"")+(h.submit_failed_at?` \xB7 <strong>sitemap push REFUSED ${escHtml(i(h.submit_failed_at))}</strong> (service account needs full permission on the property)`:"")+"</p>",E=t.candidate_rule||{};e.innerHTML=`
2390    <div class="admin-section">
2391      ${d}
2392      <div class="admin-tbl-controls">
2393        <span class="admin-tbl-count">Last ${t.window_days} days: <strong>${n(l.clicks)}</strong> clicks${o(l.clicks,t.prior&&t.prior.clicks)} \xB7 <strong>${n(l.impressions)}</strong> impressions${o(l.impressions,t.prior&&t.prior.impressions)} \xB7 CTR ${a(l.ctr)} \xB7 avg position ${s(l.position)} \xB7 ${n(l.queries)} queries \xB7 ${n(l.pages)} pages</span>
2394        <select class="form-input" id="admin-search-days" title="Window">
2395          ${[7,28,90].map(k=>`<option value="${k}" ${k===t.window_days?"selected":""}>${k} days</option>`).join("")}
2396        </select>
2397        <button class="btn-new btn-xs" id="admin-search-pull" title="Pull the last 10 days from Search Console now (normally every 12h)">Pull now</button>
2398      </div>
2399    </div>
2400    <div class="admin-section">
2401      <div class="admin-section-hdr">Indexing: is Google serving the pages we watch</div>
2402      <div class="admin-tbl-controls">
2403        ${$}
2404        <button class="btn-new btn-xs" id="admin-index-check" title="Inspect the next ${n(f.batch||4)} watched pages now instead of waiting for the hourly check">Check next pages</button>
2405        <button class="btn-new btn-xs" id="admin-sitemap-submit" title="Tell Google to re-read sitemap.xml now. Do this after publishing a campaign or shipping a new page \u2014 Google otherwise re-reads it on its own schedule, which ran four days behind on 2026-09-12.">Submit sitemap</button>
2406      </div>
2407      ${c(["Page","Status","Google's reason","Last crawled","Checked","Change"],w,"Nothing checked yet. The hourly check fills this in a few pages at a time.")}
2408    </div>
2409    <div class="admin-section">
2410      <div class="admin-section-hdr">Article candidates: seen for, not clicked (\u2265 ${E.min_impressions} impressions, avg position \u2265 ${E.min_position})</div>
2411      ${c(["Query","Impressions","Clicks","Position","Lands on"],u,"No candidates in this window. Either nothing ranks off page 1 yet, or the pull has not run.")}
2412    </div>
2413    <div class="admin-section">
2414      <div class="admin-section-hdr">Top queries (by clicks, then impressions)</div>
2415      ${c(["Query","Clicks","Impressions","CTR","Position","Lands on"],p,"No query data yet.")}
2416    </div>
2417    <div class="admin-section">
2418      <div class="admin-section-hdr">Pages (top query for each)</div>
2419      ${c(["Page","Clicks","Impressions","CTR","Position","Top query"],m,"No page data yet.")}
2420    </div>`,e.querySelector("#admin-search-days")?.addEventListener("change",k=>{_adminSearchDays=parseInt(k.target.value,10)||28,adminLoadSearchConsole()}),e.querySelector("#admin-search-pull")?.addEventListener("click",async k=>{const C=k.currentTarget;C.disabled=!0,C.textContent="Pulling\u2026";try{const S=await api("POST","/api/admin/search-console/pull");S.ok?showToast(`Pulled ${S.rows} rows for ${S.window[0]} \u2192 ${S.window[1]}`,4e3):showToast(`Pull failed at ${S.stage}: ${S.error}`,9e3)}catch(S){showToast("Pull failed: "+S.message,6e3)}adminLoadSearchConsole()}),e.querySelector("#admin-sitemap-submit")?.addEventListener("click",async k=>{const C=k.currentTarget;C.disabled=!0,C.textContent="Submitting\u2026";try{const S=await api("POST","/api/admin/search-console/sitemap");showToast(`Sitemap submitted \xB7 ${S.submitted_urls??"?"} URLs, ${S.errors??0} errors, ${S.warnings??0} warnings \xB7 Google last read it ${i(S.last_downloaded)}`,8e3)}catch(S){showToast("Sitemap submit failed: "+S.message,8e3)}adminLoadSearchConsole()}),e.querySelector("#admin-index-check")?.addEventListener("click",async k=>{const C=k.currentTarget;C.disabled=!0,C.textContent="Checking\u2026";try{const S=await api("POST","/api/admin/search-console/index-check");
2420if(S.ok){const q=(S.dropped||[]).length?` \xB7 no longer indexed: ${S.dropped.join(", ")}`:"";showToast(`Checked ${(S.checked||[]).join(", ")}${q}`,6e3)}else showToast(`Check skipped: ${S.skipped||S.error||"no reason given"}`,8e3)}catch(S){showToast("Check failed: "+S.message,6e3)}adminLoadSearchConsole()})}window.adminLoadSearchConsole=adminLoadSearchConsole;async function adminLoadReleaseNotes(){const e=document.getElementById("admin-release-notes-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t;try{t=await api("GET","/api/admin/release-notes")}catch(h){e.innerHTML=`<p class="placeholder--padded error">Failed to load release notes: ${escHtml(h.message)}</p>`;return}const n=t.items||{},a=t.counts||{},s=a.approved||0,i=a.pending||0;_tfRailDot(document.getElementById("admin-rail-ops"),"admin-sec-ops-release",i?"watch":"clear",i?`${i} waiting for review`:"Nothing waiting");const o=h=>h?new Date(h).toLocaleDateString(void 0,{month:"short",day:"numeric"}):"",r=h=>{const v=h.status==="posted",b=h.edited_body||h.body||"",y=(h.source_commit||"").slice(0,7),_=v?`<span class="rn-label">${escHtml(h.label)}</span>`:`<select class="form-input rn-label-sel" data-id="${h.id}" title="Which kind of change this is; Changed posts first, then fixes, then features">${_RN_LABELS.map(C=>`<option value="${C}" ${C===h.label?"selected":""}>${C}</option>`).join("")}</select>`;let w="";h.status==="pending"?w=`<button class="btn-new btn-xs rn-act" data-act="approved" data-id="${h.id}" title="Approve this item; it posts with the next release notes">Approve</button>
2421                 <button class="btn-ghost btn-xs btn-ghost--danger rn-act" data-act="dropped" data-id="${h.id}" title="Never post this one">Drop</button>`:h.status==="approved"?w=`<button class="btn-ghost btn-xs rn-act" data-act="pending" data-id="${h.id}">Unapprove</button>
2422                 <button class="btn-ghost btn-xs btn-ghost--danger rn-act" data-act="dropped" data-id="${h.id}">Drop</button>`:h.status==="dropped"?w=`<button class="btn-ghost btn-xs rn-act" data-act="pending" data-id="${h.id}">Restore</button>`:h.status==="posting"&&(w='<span class="comms-status">posting\u2026</span>');const $=v?"":`<button class="btn-ghost btn-xs rn-save hidden" data-id="${h.id}">Save text</button>`,E=v?`<div class="rn-posted-text">${escHtml(b)}</div>`:`<textarea class="form-input rn-text" data-id="${h.id}" rows="3">${escHtml(b)}</textarea>`,k=v&&h.posted_at?` \xB7 posted ${escHtml(o(h.posted_at))}`:"";return`<div class="comms-card rn-card rn-card--${escHtml(h.status)}" data-id="${h.id}">
2423      <div class="comms-card-main">
2424        <div class="comms-card-meta rn-meta">${_}<span>added ${escHtml(o(h.added_at))}${y?" \xB7 "+escHtml(y):""}${k}</span></div>
2425        ${E}
2426      </div>
2427      <div class="comms-card-actions rn-actions">${w}${$}</div>
2428    </div>`},c=(h,v,b)=>`<div class="rn-group">
2429      <span class="form-section-label">${h} (${v.length})</span>
2430      ${v.length?`<div class="comms-list">${v.map(r).join("")}</div>`:`<p class="placeholder--padded">${b}</p>`}
2431    </div>`,l=n.pending||[],d=(n.approved||[]).concat(n.posting||[]),u=n.dropped||[],p=n.posted||[],m=(t.preview||[]).join(`
2432
2433\u2014 next message \u2014
2434
2435`),f=t.posts||[];let g=`
2436    <p class="form-helper-text" style="margin-bottom:0.5rem">
2437      Every shipped change a reader would notice lands here from TODO.md at deploy time. Approve the ones worth
2438      telling people about, edit the wording, drop the rest. The bot posts the approved set to #release-notes on its own
2439      once there is enough (five approved items, or a week since the last post with anything approved), two hours after your last decision.
2440      Post now skips the wait.
2441    </p>
2442    <div class="rn-status-row">
2443      <label class="rn-toggle" title="Pause this and nothing posts until you press Post now">
2444        <input type="checkbox" id="rn-autopost-chk" ${t.autopost_enabled?"checked":""}> Auto-post when there is enough
2445      </label>
2446      <span class="rn-ready ${t.ready?"rn-ready--ready":""}">${t.ready?"Ready: ":""}${escHtml(t.reason||"")}</span>
2447      <button class="btn-new btn-sm" id="rn-post-now" ${s?"":"disabled"} title="Post every approved item to #release-notes right now">Post now (${s})</button>
2448    </div>`;g+=c("Waiting for review",l,"Nothing waiting. New lines arrive with the next deploy."),g+=c("Approved, goes out next",d,"Nothing approved yet."),g+=`<div class="rn-group">
2449      <span class="form-section-label">Preview: exactly what the bot will post</span>
2450      ${m?`<pre class="rn-preview">${escHtml(m)}</pre>`:'<p class="placeholder--padded">Approve an item to see the post.</p>'}
2451    </div>`,u.length&&(g+=`<details class="rn-group"><summary class="form-section-label">Dropped (${u.length})</summary>
2452      <div class="comms-list">${u.map(r).join("")}</div></details>`),g+=`<div class="rn-group"><span class="form-section-label">Posted (${f.length})</span>`,f.length?(g+='<div class="comms-list">'+f.map(h=>`<div class="comms-card">
2453        <div class="comms-card-main">
2454          <div class="comms-card-subject">${escHtml(new Date(h.posted_at).toLocaleString())}</div>
2455          <div class="comms-card-meta">${h.item_count} item${h.item_count===1?"":"s"} \xB7 ${(h.message_ids||[]).length} message${(h.message_ids||[]).length===1?"":"s"} \xB7 ${escHtml(h.trigger_kind||"")}${h.error?" \xB7 "+escHtml(h.error):""}</div>
2456        </div>
2457      </div>`).join("")+"</div>",p.length&&(g+=`<details class="rn-group"><summary class="form-section-label">Posted items (${p.length})</summary>
2458        <div class="comms-list">${p.map(r).join("")}</div></details>`)):g+='<p class="placeholder--padded">The bot has not posted yet.</p>',g+="</div>",e.innerHTML=g,e.onclick=async h=>{const v=h.target.closest(".rn-act"),b=h.target.closest(".rn-save"),y=h.target.closest("#rn-post-now");if(v){const _=v.dataset.id,w={status:v.dataset.act},$=e.querySelector(`.rn-text[data-id="${_}"]`);$&&$.value.trim()&&$.value.trim()!==($.defaultValue||"").trim()&&(w.body=$.value);try{await api("PATCH",`/api/admin/release-notes/${_}`,w),await adminLoadReleaseNotes()}catch(E){showToast("Update failed: "+E.message,5e3)}return}if(b){const _=b.dataset.id,w=e.querySelector(`.rn-text[data-id="${_}"]`);if(!w||!w.value.trim()){showToast("The text cannot be empty.",3e3);return}try{await api("PATCH",`/api/admin/release-notes/${_}`,{body:w.value}),showToast("Text saved.",2e3),await adminLoadReleaseNotes()}catch($){showToast("Save failed: "+$.message,5e3)}return}if(y){if(!confirm(`Post ${s} approved item${s===1?"":"s"} to #release-notes now?`))return;y.disabled=!0;try{const _=await api("POST","/api/admin/release-notes/post-now",{});showToast(`Posted ${_.posted} item${_.posted===1?"":"s"} in ${_.messages} message${_.messages===1?"":"s"}.`,4e3)}catch(_){showToast("Post failed: "+_.message,6e3)}await adminLoadReleaseNotes()}},e.oninput=h=>{const v=h.target.closest(".rn-text");if(!v)return;const b=e.querySelector(`.rn-save[data-id="${v.dataset.id}"]`);b&&b.classList.toggle("hidden",v.value.trim()===(v.defaultValue||"").trim())},e.onchange=async h=>{const v=h.target.closest(".rn-label-sel");if(v){try{await api("PATCH",`/api/admin/release-notes/${v.dataset.id}`,{label:v.value}),showToast("Label updated.",2e3)}catch(y){showToast("Update failed: "+y.message,5e3)}return}const b=h.target.closest("#rn-autopost-chk");if(b)try{await api("PUT","/api/admin/release-notes/settings",{autopost:b.checked}),await adminLoadReleaseNotes()}catch(y){showToast("Update failed: "+y.message,5e3)}}}async function loadAdminServices(){const e=document.getElementById("admin-services-list");if(e){e.innerHTML='<span class="spinner"></span>';try{const{services:t}=await api("GET","/api/admin/services");e.innerHTML=t.map(_renderServiceCard).join("")}catch(t){e.innerHTML=`<p class="error">${t.message}</p>`}}}function _renderServiceCard(e){const t=e.balance_usd!==null,n=e.daily_burn_usd>0?`$${e.daily_burn_usd.toFixed(2)}/day`:"No usage tracked";let a="svc-status-ok",s="";t&&e.days_remaining!==null&&(e.days_remaining<7?a="svc-status-danger":e.days_remaining<30&&(a="svc-status-warn"),s=e.days_remaining>=9999?"No burn detected":`~${e.days_remaining} days remaining`);const i=e.recorded_at?`Recorded ${new Date(e.recorded_at).toLocaleDateString()}`:"Balance not set";let o="";if(e.cloudinary&&!e.cloudinary.error){const c=e.cloudinary,l=(d,u)=>`
2459      <div class="svc-quota-row">
2460        <span class="svc-quota-label">${u}</span>
2461        <div class="svc-quota-bar"><div class="svc-quota-fill ${d>80?"svc-quota-warn":""}" style="width:${Math.min(d,100)}%"></div></div>
2462        <span class="svc-quota-pct">${d}%</span>
2463      </div>`;o=`<div class="svc-quota-group">
2464      ${l(c.storage_pct,`Storage ${c.storage_used_gb}/${c.storage_limit_gb} GB`)}
2465      ${l(c.bandwidth_pct,`Bandwidth ${c.bandwidth_used_gb}/${c.bandwidth_limit_gb} GB`)}
2466      ${l(c.transformations_pct,"Transforms")}
2467    </div>`}else e.cloudinary&&e.cloudinary.error&&(o=`<p class="svc-error-note">Quota fetch failed: ${escHtml(e.cloudinary.error)}</p>`);const r=e.monthly_history.length?`<div class="svc-history">${e.monthly_history.map(c=>`<span class="svc-hist-item"><span class="svc-hist-month">${escHtml(c.month)}</span><span class="svc-hist-cost">$${c.cost.toFixed(2)}</span></span>`).join("")}</div>`:"";return`
2468  <div class="svc-card ${a}">
2469    <div class="svc-card-header">
2470      <div class="svc-card-title">${escHtml(e.label)}</div>
2471      <a href="${escHtml(e.dashboard_url)}" target="_blank" class="btn-ghost btn-xs">Manage \u2192</a>
2472    </div>
2473
2474    ${o}
2475
2476    <div class="svc-stats-row">
2477      <div class="svc-stat">
2478        <div class="svc-stat-val">${t?"$"+e.balance_usd.toFixed(2):"\u2014"}</div>
2479        <div class="svc-stat-lbl">${i}</div>
2480      </div>
2481      <div class="svc-stat">
2482        <div class="svc-stat-val">${n}</div>
2483        <div class="svc-stat-lbl">30-day avg burn</div>
2484      </div>
2485      ${s?`<div class="svc-stat">
2486        <div class="svc-stat-val ${a==="svc-status-danger"?"text-danger":""}">${s}</div>
2487        <div class="svc-stat-lbl">Until $${e.alert_threshold.toFixed(0)} threshold</div>
2488      </div>`:""}
2489    </div>
2490
2491    ${r}
2492
2493    <div class="svc-update-row">
2494      <input type="number" step="0.01" min="0" placeholder="New balance ($)"
2495             class="svc-balance-input" id="svc-bal-${e.service}" />
2496      <input type="number" step="1" min="0" placeholder="Alert at ($)"
2497             class="svc-threshold-input" id="svc-thr-${e.service}"
2498             value="${e.alert_threshold}" />
2499      <button class="btn-secondary btn-xs" onclick="_updateServiceBalance('${e.service}')">Save</button>
2500    </div>
2501  </div>`}async function _updateServiceBalance(e){const t=document.getElementById(`svc-bal-${e}`),n=document.getElementById(`svc-thr-${e}`),a={};if(t.value!==""&&(a.balance_usd=parseFloat(t.value)),n.value!==""&&(a.alert_threshold_usd=parseFloat(n.value)),!!Object.keys(a).length)try{await api("PUT",`/api/admin/services/${e}`,a),showToast("Balance updated."),loadAdminServices()}catch(s){showToast("Error: "+s.message)}}window._updateServiceBalance=_updateServiceBalance;const _FIXED_COSTS=[{service:"Anthropic",plan:"API Credit (prepaid)",monthly:100,note:"Budget; actual usage tracked below"},{service:"AssemblyAI",plan:"u3-rt-pro streaming",monthly:0,note:"$0.45/hr ($0.0075/min), pay-as-you-go"},{service:"Vercel",plan:"Pro",monthly:20,note:"60s functions, 100 GB bandwidth"},{service:"Neon Postgres",plan:"Free Tier",monthly:0,note:"0.5 GB storage, 191 compute hrs/mo"},{service:"Cloudinary",plan:"Free Tier",monthly:0,note:"25 credits/mo (storage + transforms)"},{service:"Ably",plan:"Free Tier",monthly:0,note:"6M messages/mo, 200 connections"},{service:"Domain",plan:"3-Year (prepaid)",monthly:1.62,note:"thread-fall.com ($58.37 \xF7 36 mo)"},{service:"Email",plan:"Annual (prepaid)",monthly:7.57,note:"$90.80 incl. tax \xF7 12 mo"},{service:"Sentry",plan:"Free Tier",monthly:0,note:"Error tracking: 5K errors/mo"}];async function adminLoadCosts(){const e=document.getElementById("admin-costs-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t=null;try{t=(await api("GET","/api/admin/overview")).ai?.cost_month??null}catch{}const n=_FIXED_COSTS.reduce((d,u)=>d+u.monthly,0),a=_FIXED_COSTS.find(d=>d.service==="Anthropic")?.monthly??100,s=t??0,i=Math.min(100,s/a*100).toFixed(1),o=Math.max(0,a-s),r=n-a+s,c=d=>`$${d.toFixed(2)}`,l=_FIXED_COSTS.map(d=>`
2502    <tr>
2503      <td class="costs-td costs-td--service">${escHtml(d.service)}</td>
2504      <td class="costs-td">${escHtml(d.plan)}</td>
2505      <td class="costs-td costs-td--num">${d.monthly===0?'<span class="costs-free">Free</span>':c(d.monthly)}</td>
2506      <td class="costs-td costs-td--note">${escHtml(d.note)}</td>
2507    </tr>`).join("");e.innerHTML=`
2508    <div class="admin-section">
2509      <div class="admin-section-label form-section-label">
2509Fixed Monthly Infrastructure</div>
2510      <table class="costs-table">
2511        <thead>
2512          <tr>
2513            <th class="costs-th">Service</th>
2514            <th class="costs-th">Plan</th>
2515            <th class="costs-th costs-th--num">Monthly</th>
2516            <th class="costs-th">Notes</th>
2517          </tr>
2518        </thead>
2519        <tbody>${l}</tbody>
2520        <tfoot>
2521          <tr class="costs-total-row">
2522            <td class="costs-td" colspan="2"><strong>Fixed Total</strong></td>
2523            <td class="costs-td costs-td--num"><strong>${c(n)}</strong></td>
2524            <td class="costs-td costs-td--note">per month</td>
2525          </tr>
2526        </tfoot>
2527      </table>
2528    </div>
2529
2530    <div class="admin-section">
2531      <div class="admin-section-label form-section-label">Anthropic API: This Month's Consumption</div>
2532      <div class="costs-ai-block">
2533        <div class="costs-ai-row">
2534          <span class="costs-ai-label">Spent</span>
2535          <span class="costs-ai-val costs-ai-val--spent">${t!==null?c(s):"\u2014"}</span>
2536          <span class="costs-ai-label">of ${c(a)} budget</span>
2537          <span class="costs-ai-label">\xB7 ${c(o)} remaining</span>
2538        </div>
2539        <div class="costs-progress-track">
2540          <div class="costs-progress-bar${i>80?" costs-progress-bar--warn":""}" style="width:${i}%"></div>
2541        </div>
2542        <div class="costs-ai-pct">${i}% consumed</div>
2543      </div>
2544    </div>
2545
2546    <div class="admin-section">
2547      <div class="admin-section-label form-section-label">Estimated Real Burn This Month</div>
2548      <div class="costs-burn-row">
2549        <div class="costs-burn-card">
2550          <div class="costs-burn-label">Fixed (non-AI)</div>
2551          <div class="costs-burn-val">${c(n-a)}</div>
2552        </div>
2553        <div class="costs-burn-sep">+</div>
2554        <div class="costs-burn-card">
2555          <div class="costs-burn-label">Anthropic Actual</div>
2556          <div class="costs-burn-val">${t!==null?c(s):"\u2014"}</div>
2557        </div>
2558        <div class="costs-burn-sep">=</div>
2559        <div class="costs-burn-card costs-burn-card--total">
2560          <div class="costs-burn-label">Total Burn</div>
2561          <div class="costs-burn-val">${t!==null?c(r):"\u2014"}</div>
2562        </div>
2563      </div>
2564      <p class="costs-note">Free-tier services (Neon, Cloudinary, Ably) incur $0 unless usage exceeds plan limits.</p>
2565    </div>
2566
2567    <div class="admin-section">
2568      <div class="admin-section-label form-section-label">Service Health &amp; Dashboards</div>
2569      <div id="admin-services-list" class="admin-services-list"><span class="spinner"></span></div>
2570    </div>
2571
2572    <div class="admin-section">
2573      <div class="admin-section-label form-section-label">\u{1F4B0} Cost / Pricing Calculator</div>
2574      <div id="admin-cost-calc-wrap"><div class="loading-row" style="justify-content:center;padding:1.25rem"><span class="spinner"></span></div></div>
2575    </div>
2576
2577    <div class="admin-section">
2578      <div class="admin-section-label form-section-label">\u{1F501} Recap Regeneration Patterns</div>
2579      <div id="admin-recap-regen-wrap"><div class="loading-row" style="justify-content:center;padding:1.25rem"><span class="spinner"></span></div></div>
2580    </div>`,loadAdminServices(),renderCostCalculator(),renderRecapRegenPatterns()}window.adminLoadCosts=adminLoadCosts;async function renderRecapRegenPatterns(){const e=document.getElementById("admin-recap-regen-wrap");if(!e)return;let t;try{t=await api("GET","/api/admin/recap-regen-patterns")}catch(i){e.innerHTML=`<p class="placeholder--padded error">Recap regen data unavailable: ${escHtml(i.message)}</p>`;return}const n=i=>`$${(Number(i)||0).toFixed(4)}`,a=t.summary||{},s=(t.top_sessions||[]).map(i=>{const o=i.last_recap_at?new Date(i.last_recap_at).toLocaleString([],{dateStyle:"short",timeStyle:"short"}):"\u2014";return`
2581      <tr>
2582        <td class="cost-calc-tier-name"><strong>${escHtml(i.title||`Session ${i.id}`)}</strong>
2583          <span class="cost-calc-card-sub" style="margin-left:0.4rem">${escHtml(i.campaign_name||"")}</span></td>
2584        <td class="cost-calc-card-sub">${escHtml(i.gm_display_name||"(unknown)")}</td>
2585        <td class="cost-calc-num"><strong>${i.recap_run_count}</strong></td>
2586        <td class="cost-calc-num">${n(i.last_recap_cost_usd)}</td>
2587        <td class="cost-calc-card-sub">${o}</td>
2588      </tr>`}).join("")||'<tr><td colspan="5" class="cost-calc-bucket-name" style="text-align:center;color:var(--text-3)">No sessions have been regenerated yet. (Telemetry only tracks runs after this feature shipped.)</td></tr>';e.innerHTML=`
2589    <div class="cost-calc-summary">
2590      <div class="cost-calc-card">
2591        <div class="cost-calc-card-lbl form-section-label">Sessions with a recap</div>
2592        <div class="cost-calc-card-val">${a.sessions_recapped||0}</div>
2593        <div class="cost-calc-card-sub">tracked since this feature shipped</div>
2594      </div>
2595      <div class="cost-calc-card">
2596        <div class="cost-calc-card-lbl form-section-label">Regenerated (&gt; 1\xD7)</div>
2597        <div class="cost-calc-card-val">${a.sessions_regenerated||0}</div>
2598        <div class="cost-calc-card-sub">${a.regen_rate_pct||0}% of recapped sessions</div>
2599      </div>
2600      <div class="cost-calc-card">
2601        <div class="cost-calc-card-lbl form-section-label">Avg runs per recap</div>
2602        <div class="cost-calc-card-val">${a.total_runs&&a.sessions_recapped?(a.total_runs/a.sessions_recapped).toFixed(2):"\u2014"}</div>
2603        <div class="cost-calc-card-sub">median ${(a.median_runs||0).toFixed(1)} \xB7 max ${a.max_runs||0}</div>
2604      </div>
2605      <div class="cost-calc-card">
2606        <div class="cost-calc-card-lbl form-section-label">Total last-run cost</div>
2607        <div class="cost-calc-card-val">${n(a.last_run_cost_total_usd)}</div>
2608        <div class="cost-calc-card-sub">sum of MOST RECENT run cost per session</div>
2609      </div>
2610    </div>
2611
2612    <div class="cost-calc-block">
2613      <div class="cost-calc-block-hdr">Top regenerated sessions <span class="cost-calc-block-sub">(recap_run_count &gt; 1, capped at 30)</span></div>
2614      <table class="cost-calc-tier-table">
2615        <thead>
2616          <tr>
2617            <th>Session</th>
2618            <th>GM</th>
2619            <th class="cost-calc-num">Runs</th>
2620            <th class="cost-calc-num">Last run cost</th>
2621            <th>Last run at</th>
2622          </tr>
2623        </thead>
2624        <tbody>${s}</tbody>
2625      </table>
2626    </div>
2627  `}window.renderRecapRegenPatterns=renderRecapRegenPatterns;async function renderCostCalculator(){const e=document.getElementById("admin-cost-calc-wrap");if(!e)return;let t;try{t=await api("GET","/api/admin/cost-analysis")}catch(h){e.innerHTML=`<p class="placeholder--padded error">Cost analysis failed: ${escHtml(h.message)}</p>`;return}const n=h=>`$${(Number(h)||0).toFixed(2)}`,a=h=>`${Math.round((Number(h)||0)*100)}%`,s=Object.entries(t.fixed_monthly.breakdown).map(([h,v])=>`
2628    <div class="cost-calc-fixed-row">
2629      <label class="cost-calc-fixed-label" for="cc-fixed-${h}">${escHtml(h.charAt(0).toUpperCase()+h.slice(1))}</label>
2630      <span class="cost-calc-fixed-prefix">$</span>
2631      <input type="number" step="0.01" min="0" class="cost-calc-fixed-input" id="cc-fixed-${h}"
2632             value="${(v||0).toFixed(2)}" data-key="fixed_${h}_cents" />
2633      <span class="cost-calc-fixed-suffix">/mo</span>
2634    </div>`).join(""),i=h=>Number(h)>=100?"\u221E":String(h),o=t.pricing.tiers.map(h=>{const v=h.hard_capped?` <span class="cost-calc-capped" title="Hard-capped at $${h.recommended_price_usd}">cap</span>`:h.monotonic_capped?' <span class="cost-calc-capped cost-calc-capped--mono" title="Lowered so this tier is cheaper than the next-lower tier + extra-session add-ons">\u2193</span>':"",b=h.sessions>=100&&h.sessions_modeled!==h.sessions?`<span class="cost-calc-card-sub" style="margin-left:0.35rem">(modeled ${h.sessions_modeled})</span>`:"",y=h.has_ai_prep_suite?'<span class="cost-calc-ap-yes" title="AI Prep Suite: Loose Threads + Fates + Weave + GM Notebook live context">Yes</span>':'<span class="cost-calc-ap-no">\u2014</span>';return`
2635      <tr>
2636        <td class="cost-calc-tier-name"><strong>${escHtml(h.name)}</strong></td>
2637        <td class="cost-calc-num">${i(h.campaigns)}</td>
2638        <td class="cost-calc-num">${i(h.sessions)}${b}</td>
2639        <td class="cost-calc-num">${y}</td>
2640        <td class="cost-calc-num">${n(h.cost_full_use_usd)}</td>
2641        <td class="cost-calc-num">${n(h.cost_half_use_usd)}</td>
2642        <td class="cost-calc-num">${a(h.margin_full_use)}</td>
2643        <td class="cost-calc-num">${a(h.margin_half_use)}</td>
2644        <td class="cost-calc-num cost-calc-price"><strong>${n(h.recommended_price_usd)}</strong>${v}</td>
2645      </tr>`}).join(""),r=t.pricing.ai_prep_suite_inputs||{},c=`
2646    <div class="cost-calc-fixed-grid">
2647      <div class="cost-calc-fixed-row">
2648        <label class="cost-calc-fixed-label" for="cc-ap-threads">Loose Threads / session</label>
2649        <span class="cost-calc-fixed-prefix">$</span>
2650        <input type="number" step="0.01" min="0" class="cost-calc-ap-input" id="cc-ap-threads"
2651               value="${(r.loose_threads_cost_per_session_usd||0).toFixed(2)}"
2652               data-key="ai_prep_loose_threads_cost_cents" data-kind="cents" />
2653        <span class="cost-calc-fixed-suffix">amortized</span>
2654      </div>
2655      <div class="cost-calc-fixed-row">
2656        <label class="cost-calc-fixed-label" for="cc-ap-fate">Fate generation</label>
2657        <span class="cost-calc-fixed-prefix">$</span>
2658        <input type="number" step="0.01" min="0" class="cost-calc-ap-input" id="cc-ap-fate"
2659               value="${(r.fate_cost_usd||0).toFixed(2)}"
2660               data-key="ai_prep_fate_cost_cents" data-kind="cents" />
2661        <span class="cost-calc-fixed-suffix">per Fate</span>
2662      </div>
2663      <div class="cost-calc-fixed-row">
2664        <label class="cost-calc-fixed-label" for="cc-ap-weave">Weave session</label>
2665        <span class="cost-calc-fixed-prefix">$</span>
2666        <input type="number" step="0.01" min="0" class="cost-calc-ap-input" id="cc-ap-weave"
2667               value="${(r.weave_session_cost_usd||0).toFixed(2)}"
2668               data-key="ai_prep_weave_cost_cents" data-kind="cents" />
2669        <span class="cost-calc-fixed-suffix">per Weave</span>
2670      </div>
2671      <div class="cost-calc-fixed-row">
2672        <label class="cost-calc-fixed-label" for="cc-ap-fates-mo">Master Fates / mo</label>
2673        <input type="number" step="1" min="0" class="cost-calc-ap-input" id="cc-ap-fates-mo"
2674               value="${r.master_fates_per_month||0}"
2675               data-key="master_fates_per_month" data-kind="int" />
2676        <span class="cost-calc-fixed-suffix">typical</span>
2677      </div>
2678      <div class="cost-calc-fixed-row">
2679        <label class="cost-calc-fixed-label" for="cc-ap-weave-mo">Master Weaves / mo</label>
2680        <input type="number" step="1" min="0" class="cost-calc-ap-input" id="cc-ap-weave-mo"
2681               value="${r.master_weave_sessions_per_month||0}"
2682               data-key="master_weave_sessions_per_month" data-kind="int" />
2683        <span class="cost-calc-fixed-suffix">typical</span>
2684      </div>
2685      <div class="cost-calc-fixed-row">
2686        <label class="cost-calc-fixed-label" for="cc-ap-promult">Pro multiplier</label>
2687        <input type="number" step="0.1" min="1" class="cost-calc-ap-input" id="cc-ap-promult"
2688               value="${(r.pro_multiplier||1).toFixed(1)}"
2689               data-key="pro_ai_prep_multiplier_bps" data-kind="bps" />
2690        <span class="cost-calc-fixed-suffix">\xD7 Master AP usage</span>
2691      </div>
2692      <div class="cost-calc-fixed-row">
2693        <label class="cost-calc-fixed-label" for="cc-ap-prosess">Pro typical sessions</label>
2694        <input type="number" step="1" min="1" class="cost-calc-ap-input" id="cc-ap-prosess"
2695               value="${r.pro_typical_sessions_per_month||25}"
2696               data-key="pro_typical_sessions_per_month" data-kind="int" />
2697        <span class="cost-calc-fixed-suffix">per Pro user / mo</span>
2698      </div>
2699    </div>`,l=t.pricing.honor_tier_exposure||{},d=l.founding_gm||{},u=l.founding_partner||{},p=`
2700    <table class="cost-calc-tier-table cost-calc-honor-table">
2701      <thead>
2702        <tr>
2703          <th>Honor tier</th>
2704          <th class="cost-calc-num">Users</th>
2705          <th class="cost-calc-num">Sess modeled</th>
2706          <th class="cost-calc-num">Cost / user</th>
2707          <th class="cost-calc-num">Revenue / user</th>
2708          <th class="cost-calc-num">Monthly exposure</th>
2709        </tr>
2710      </thead>
2711      <tbody>
2712        <tr>
2713          <td class="cost-calc-tier-name"><strong>Founding GM</strong> <span class="cost-calc-card-sub">($7.99 honor rate)</span></td>
2714          <td class="cost-calc-num">${d.user_cap||30}</td>
2715          <td class="cost-calc-num">${d.sessions_modeled||"\u2014"}</td>
2716          <td class="cost-calc-num">${n(d.cost_per_user_usd)}</td>
2717          <td class="cost-calc-num">${n(d.revenue_per_user_usd)}</td>
2718          <td class="cost-calc-num"><strong>${n(d.max_monthly_exposure_usd)}</strong></td>
2719        </tr>
2720        <tr>
2721          <td class="cost-calc-tier-name"><strong>Founding Partner</strong> <span class="cost-calc-card-sub">${escHtml(u.label||"")}</span></td>
2722          <td class="cost-calc-num">${u.users||1}</td>
2723          <td class="cost-calc-num">${u.sessions_modeled||"\u2014"}</td>
2724          <td class="cost-calc-num">${n(u.cost_per_user_usd)}</td>
2725          <td class="cost-calc-num">${n(u.revenue_per_user_usd)}</td>
2726          <td class="cost-calc-num"><strong>${n(u.monthly_cost_usd)}</strong></td>
2727        </tr>
2728        <tr class="cost-calc-honor-total">
2729          <td colspan="5" style="text-align:right"><strong>Total honor-tier monthly cost exposure</strong></td>
2730          <td class="cost-calc-num"><strong>${n(l.total_monthly_exposure_usd)}</strong></td>
2731        </tr>
2732      </tbody>
2733    </table>`,m=Math.abs(t.sanity_check.delta_pct)<10?"cost-calc-sanity--ok":Math.abs(t.sanity_check.delta_pct)<25?"cost-calc-sanity--warn":"cost-calc-sanity--bad",f=Object.entries(t.variable_window).filter(([h,v])=>Number(v)>0).sort((h,v)=>Number(v[1])-Number(h[1])).map(([h,v])=>`<tr><td class="cost-calc-bucket-name">${escHtml(h.replace(/_/g," "))}</td><td class="cost-calc-num">${n(v)}</td></tr>`).join("")||'<tr><td colspan="2" class="cost-calc-bucket-name" style="text-align:center;color:var(--text-3)">No usage in window.</td></tr>';e.innerHTML=`
2734    ${t.per_campaign_detail&&t.cost_model_monthly?(()=>{const h=t.cost_model_monthly,v=t.aai_rate||{effective_per_hour:0,base_per_hour:0,speaker_labels_per_hour:0},b=t.per_campaign_detail.map(y=>`
2735        <tr class="${y.margin_direct_negative?"cost-canon-row--neg":""}">
2736          <td>${escHtml(y.name||"\u2014")}</td>
2737          <td style="color:var(--text-3);font-size:0.82rem">${escHtml(y.gm_email||"\u2014")}</td>
2738          <td style="font-variant-numeric:tabular-nums">${y.sessions_n}${y.live_n?` \xB7 <span style="color:var(--accent-teal)">${y.live_n} live</span>`:""}</td>
2739          <td class="cost-calc-num">${n(y.direct_transcription)}</td>
2740          <td class="cost-calc-num">${n(y.direct_ai)}</td>
2741          <td class="cost-calc-num">${n(y.indirect)}</td>
2742          <td class="cost-calc-num" style="font-weight:700">${n(y.total)}</td>
2743          <td class="cost-calc-num">${y.paying?n(y.revenue):'<span style="color:var(--text-3)">free</span>'}</td>
2744          <td class="cost-calc-num">${y.margin_direct_pct==null?"\u2014":`${y.margin_direct_negative?"\u26A0 ":""}${y.margin_direct_pct}%`}</td>
2745        </tr>`).join("")||'<tr><td colspan="9" style="text-align:center;color:var(--text-3)">No campaign cost in window.</td></tr>';return`
2746        <div class="admin-merged-hdr">The cost model: total \u2192 direct vs indirect (monthly)</div>
2747        <div class="cost-calc-summary">
2748          <div class="cost-calc-card"><div class="cost-calc-card-lbl form-section-label">Total monthly cost</div><div class="cost-calc-card-val">${n(h.grand_total)}</div><div class="cost-calc-card-sub">direct ${n(h.direct_total)} + indirect ${n(h.indirect_total)}</div></div>
2749          <div class="cost-calc-card"><div class="cost-calc-card-lbl form-section-label">Direct (usage)</div><div class="cost-calc-card-val">${n(h.direct_total)}</div><div class="cost-calc-card-sub">transcription @ ${n(v.effective_per_hour)}/hr + AI</div></div>
2750          <div class="cost-calc-card"><div class="cost-calc-card-lbl form-section-label">Indirect (fixed infra)</div><div class="cost-calc-card-val">${n(h.indirect_total)}</div><div class="cost-calc-card-sub">${n(h.indirect_per_active_campaign)} / active campaign \xB7 ${h.active_campaigns} active</div></div>
2751        </div>
2752        <div class="admin-table-wrap">
2753          <table class="admin-table cost-canon-table">
2754            <thead><tr><th>
2754Campaign</th><th>GM</th><th>Sessions</th><th>Transcription</th><th>AI</th><th>Indirect</th><th>Total</th><th>Revenue</th><th>Margin*</th></tr></thead>
2755            <tbody>${b}</tbody>
2756          </table>
2757        </div>
2758        <p class="form-helper-text">Transcription billed at ${n(v.effective_per_hour)}/hr (${n(v.base_per_hour)} base + ${n(v.speaker_labels_per_hour)} speaker labels), from measured audio-hours, not word count. *Margin = tier price vs direct cost; \u26A0 = direct cost alone exceeds the tier price.${t.orphaned_ai_cost_usd>0?` Plus ${n(t.orphaned_ai_cost_usd)} unattributed AI (deleted/archived campaigns).`:""}</p>
2759      `})():""}
2760    <div class="admin-merged-hdr">Distribution &amp; tier calculator</div>
2761    <div class="cost-calc-summary">
2762      <div class="cost-calc-card">
2763        <div class="cost-calc-card-lbl form-section-label">Active campaigns</div>
2764        <div class="cost-calc-card-val">${t.campaigns.active}</div>
2765        <div class="cost-calc-card-sub">${t.campaigns.dormant} dormant \xB7 ${t.campaigns.archived} archived</div>
2766      </div>
2767      <div class="cost-calc-card">
2768        <div class="cost-calc-card-lbl form-section-label">Avg cost / campaign / mo</div>
2769        <div class="cost-calc-card-val">${n(t.per_campaign_monthly.mean)}</div>
2770        <div class="cost-calc-card-sub">p50 ${n(t.per_campaign_monthly.p50)} \xB7 p75 ${n(t.per_campaign_monthly.p75)} \xB7 p95 ${n(t.per_campaign_monthly.p95)}</div>
2771      </div>
2772      <div class="cost-calc-card cost-calc-card--split">
2773        <div class="cost-calc-card-lbl form-section-label">Per-session variable</div>
2774        <div class="cost-calc-card-val">${n((t.per_session_variable_split||{}).live_usd)}</div>
2775        <div class="cost-calc-card-sub">live session \xB7 ${t.per_campaign_monthly.total_sessions_in_window} sessions / ${t.window_days}d \xB7 used for tier math</div>
2776        ${t.per_session_variable_split?`
2777          <div class="cost-calc-split-rows">
2778            <div class="cost-calc-split-row"><span class="cost-calc-split-lbl">live</span><span class="cost-calc-split-val">${n(t.per_session_variable_split.live_usd)}</span><span class="cost-calc-split-meta">${t.per_session_variable_split.live_sessions_n}\xD7 \xB7 ${Math.round((t.per_session_variable_split.live_session_pct||0)*100)}%</span></div>
2779            <div class="cost-calc-split-row"><span class="cost-calc-split-lbl">manual</span><span class="cost-calc-split-val">${n(t.per_session_variable_split.manual_usd)}</span><span class="cost-calc-split-meta">${t.per_session_variable_split.manual_sessions_n}\xD7</span></div>
2780          </div>`:""}
2781      </div>
2782      <div class="cost-calc-card cost-calc-card--editable">
2783        <div class="cost-calc-card-lbl form-section-label">Target margin</div>
2784        <div class="cost-calc-card-val cost-calc-card-val--input">
2785          <input type="range" min="0" max="95" step="1" class="cost-calc-margin-range"
2786                 value="${Math.round(t.pricing.target_margin*100)}" />
2787          <span class="cost-calc-bignum">
2788            <input type="number" step="1" min="0" max="95" class="cost-calc-margin-input cost-calc-bignum-input"
2789                   value="${Math.round(t.pricing.target_margin*100)}" data-key="target_margin_bps" />%
2790          </span>
2791        </div>
2792        <div class="cost-calc-card-sub">drag or type \xB7 prices update live</div>
2793      </div>
2794      <div class="cost-calc-card cost-calc-card--editable">
2795        <div class="cost-calc-card-lbl form-section-label">Pricing scale</div>
2796        <div class="cost-calc-card-val cost-calc-card-val--input">
2797          <span class="cost-calc-bignum">
2798            <input type="number" step="1" min="1" max="100000" class="cost-calc-target-input cost-calc-bignum-input"
2799                   value="${t.pricing.target_campaigns_for_scale}" data-key="pricing_target_campaigns" />
2800          </span>
2801        </div>
2802        <div class="cost-calc-card-sub">campaigns \xB7 fixed share ${n(t.pricing.fixed_share_at_target_scale)}/mo</div>
2803      </div>
2804    </div>
2805
2806    <div class="cost-calc-grid">
2807      <div class="cost-calc-block">
2808        <div class="cost-calc-block-hdr">Fixed monthly costs <span class="cost-calc-block-sub">(${n(t.fixed_monthly.total)} total)</span></div>
2809        <div class="cost-calc-fixed-grid">${s}</div>
2810      </div>
2811      <div class="cost-calc-block">
2812        <div class="cost-calc-block-hdr">Variable AI / AAI (${t.window_days}d)</div>
2813        <table class="cost-calc-bucket-table">
2814          <tbody>${f}</tbody>
2815        </table>
2816      </div>
2817    </div>
2818
2819    <div class="cost-calc-block">
2820      <div class="cost-calc-block-hdr">AI Prep Suite cost inputs <span class="cost-calc-block-sub">(applied to Master / Pro / Founding-GM only)</span></div>
2821      ${c}
2822    </div>
2823
2824    <div class="cost-calc-block">
2825      <div class="cost-calc-block-hdr">Recommended tier pricing</div>
2826      <table class="cost-calc-tier-table">
2827        <thead>
2828          <tr>
2829            <th>Tier</th>
2830            <th class="cost-calc-num">Camps</th>
2831            <th class="cost-calc-num">Sess/mo</th>
2832            <th class="cost-calc-num">AI Prep</th>
2833            <th class="cost-calc-num">Cost full</th>
2834            <th class="cost-calc-num">Cost 50%</th>
2835            <th class="cost-calc-num">Margin full</th>
2836            <th class="cost-calc-num">Margin 50%</th>
2837            <th class="cost-calc-num">Implied price</th>
2838          </tr>
2839        </thead>
2840        <tbody>${o}</tbody>
2841      </table>
2842      <div class="cost-calc-addon-row">
2843        <span><strong>Extra session add-on:</strong></span>
2844        <span class="cost-calc-fixed-prefix">$</span>
2845        <input type="number" step="0.01" min="0" class="cost-calc-addon-input"
2846               value="${t.pricing.extra_session_usd.toFixed(2)}" data-key="extra_session_price_cents" />
2847        <span class="cost-calc-card-sub">cost ${n(t.pricing.extra_session_cost_usd)} \xB7 margin ${a(t.pricing.extra_session_margin)}</span>
2848      </div>
2849    </div>
2850
2851    <div class="cost-calc-block">
2852      <div class="cost-calc-block-hdr">Honor tier exposure <span class="cost-calc-block-sub">(not on public pricing page)</span></div>
2853      ${p}
2854    </div>
2855
2856    <div class="cost-calc-sanity ${m}">
2857      <strong>Sanity check:</strong>
2858      ${t.campaigns.active} active \xD7 ${n(t.per_campaign_monthly.mean)} = <strong>${n(t.sanity_check.implied_monthly_usd)}</strong> implied
2859      vs <strong>${n(t.sanity_check.observed_monthly_usd)}</strong> observed (last 30d)
2860      &middot; \u0394 ${t.sanity_check.delta_pct>=0?"+":""}${t.sanity_check.delta_pct}%
2861    </div>`;const g=async(h,v)=>{try{await api("PATCH","/api/admin/settings",{settings:{[h]:v}}),renderCostCalculator()}catch(b){showToast("Save failed: "+b.message,4e3)}};e.querySelectorAll(".cost-calc-fixed-input").forEach(h=>{h.addEventListener("change",()=>{const v=Math.round(parseFloat(h.value||"0")*100);g(h.dataset.key,v)})}),e.querySelectorAll(".cost-calc-margin-input").forEach(h=>{h.addEventListener("change",()=>{const v=Math.round(parseFloat(h.value||"0")*100);g(h.dataset.key,v)})}),e.querySelectorAll(".cost-calc-margin-range").forEach(h=>{const v=e.querySelector(".cost-calc-margin-input");h.addEventListener("input",()=>{v&&(v.value=h.value)}),h.addEventListener("change",()=>{const b=Math.round(parseFloat(h.value||"0")*100);g("target_margin_bps",b)})}),e.querySelectorAll(".cost-calc-addon-input").forEach(h=>{h.addEventListener("change",()=>{const v=Math.round(parseFloat(h.value||"0")*100);g(h.dataset.key,v)})}),e.querySelectorAll(".cost-calc-ap-input").forEach(h=>{h.addEventListener("change",()=>{const v=parseFloat(h.value||"0");let b;switch(h.dataset.kind){case"cents":b=Math.max(0,Math.round(v*100));break;case"bps":b=Math.max(100,Math.round(v*100));break;case"int":default:b=Math.max(0,Math.round(v));break}g(h.dataset.key,b)})}),e.querySelectorAll(".cost-calc-target-input").forEach(h=>{h.addEventListener("change",()=>{const v=Math.max(1,Math.round(parseFloat(h.value||"0")));g(h.dataset.key,v)})})}window.renderCostCalculator=renderCostCalculator;async function adminLoadTrends(){const e=document.getElementById("admin-trends-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t;try{t=await api("GET","/api/admin/cost-trends")}catch(N){e.innerHTML=`<p class="placeholder--padded error">Trends load failed: ${escHtml(N.message)}</p>`;return}const n=N=>`$${(Number(N)||0).toFixed(2)}`,a=N=>`${Math.round((Number(N)||0)*100)}%`,s=t.totals.mtd,i=t.totals.qtd,o=t.totals.ytd,r=o.eoy_projection_usd,c=`
2862    <div class="tf-card tf-card--sm trends-period-card">
2863      <div class="trends-period-lbl">Month-to-Date</div>
2864      <div class="trends-period-val">${n(s.total_usd)}</div>
2865      <div class="trends-period-sub">${n(s.ai_usd)} AI \xB7 ${n(s.aai_usd)} AAI</div>
2866    </div>
2867    <div class="tf-card tf-card--sm trends-period-card">
2868      <div class="trends-period-lbl">Quarter-to-Date</div>
2869      <div class="trends-period-val">${n(i.total_usd)}</div>
2870      <div class="trends-period-sub">${n(i.ai_usd)} AI \xB7 ${n(i.aai_usd)} AAI</div>
2871    </div>
2872    <div class="tf-card tf-card--sm trends-period-card">
2873      <div class="trends-period-lbl">Year-to-Date</div>
2874      <div class="trends-period-val">${n(o.total_usd)}</div>
2875      <div class="trends-period-sub">Day ${o.days_into_year} / ${o.year_days}</div>
2876    </div>
2877    <div class="tf-card tf-card--sm trends-period-card trends-period-card--proj">
2878      <div class="trends-period-lbl">EoY Projection</div>
2879      <div class="trends-period-val">${n(r)}</div>
2880      <div class="trends-period-sub">linear from YTD</div>
2881    </div>`,l=720,d=220,u=48,p=16,m=16,f=36,g=l-u-p,h=d-m-f;function v(N,G){const W=[],z=[];for(let V=0;V<=4;V++){const K=N*(1-V/4),se=m+h*V/4;W.push(`<line x1="${u}" y1="${se}" x2="${l-p}" y2="${se}" class="trends-grid-line"/>`),z.push(`<text x="${u-6}" y="${se+4}" class="trends-grid-label" text-anchor="end">${G(K)}</text>`)}return W.join("")+z.join("")}function b(N,G){const F=N.length;if(!F)return"";const W=Math.max(1,Math.ceil(F/6)),z=[];for(let V=0;V<F;V+=W){const K=u+g*(V/Math.max(1,F-1));z.push(`<text x="${K}" y="${d-f+18}" class="trends-grid-label" text-anchor="middle">${escHtml(G(N[V]))}</text>`)}return z.join("")}const y=N=>{if(!N)return"";const G=new Date(N);return`${G.getMonth()+1}/${G.getDate()}`},_=t.weekly_live_cost||[],w=t.target_live_cost_usd||1.5;let $;if(!_.length)$='<p class="placeholder--padded">No live sessions in the last 12 weeks.</p>';else{const N=Math.max(w*1.2,..._.map(K=>K.avg_cost_usd))*1.1,G=g/Math.max(1,_.length-1),F=_.map((K,se)=>{const Q=u+G*se,X=m+h*(1-K.avg_cost_usd/N);return{x:Q,y:X,w:K}}),W=F.map((K,se)=>(se?"L":"M")+K.x.toFixed(1)+" "+K.y.toFixed(1)).join(" "),z=F.map(K=>{const se=K.w.avg_cost_usd>w;return`<circle cx="${K.x}" cy="${K.y}" r="${se?4.5:3.2}" class="${se?"trends-dot trends-dot--over":"trends-dot"}"><title>Week of ${K.w.week_start}: ${n(K.w.avg_cost_usd)} / session (${K.w.live_sessions_n} live)</title></circle>`}).join(""),V=m+h*(1-w/N);$=`
2882      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2883        ${v(N,K=>`$${K.toFixed(2)}`)}
2884        <line x1="${u}" y1="${V}" x2="${l-p}" y2="${V}" class="trends-target-line"/>
2885        <text x="${l-p-4}" y="${V-4}" class="trends-target-label" text-anchor="end">target ${n(w)}</text>
2886        <path d="${W}" class="trends-line trends-line--teal"/>
2887        ${z}
2888        ${b(_.map(K=>K.week_start),y)}
2889      </svg>`}let E;if(!_.length)E='<p class="placeholder--padded">Not enough live sessions for retry data yet.</p>';else{const N=Math.max(.1,..._.map(V=>V.retry_pct))*1.15,G=g/Math.max(1,_.length-1),F=_.map((V,K)=>{const se=u+G*K,Q=m+h*(1-V.retry_pct/N);return{x:se,y:Q,w:V}}),W=F.map((V,K)=>(K?"L":"M")+V.x.toFixed(1)+" "+V.y.toFixed(1)).join(" "),z=F.map(V=>`<circle cx="${V.x}" cy="${V.y}" r="3.2" class="trends-dot"><title>Week of ${V.w.week_start}: ${a(V.w.retry_pct)} retried (${V.w.live_sessions_n} live)</title></circle>`).join("");E=`
2890      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2891        ${v(N,V=>`${Math.round(V*100)}%`)}
2892        <path d="${W}" class="trends-line trends-line--amber"/>
2893        ${z}
2894        ${b(_.map(V=>V.week_start),y)}
2895      </svg>`}const k=t.daily_by_bucket||[],C=["live-scan","recap/narrative","extraction","oracle","other"],S={"live-scan":"var(--accent-blue)","recap/narrative":"var(--accent-teal)",extraction:"var(--accent-gold)",oracle:"#7E3FA0",other:"var(--text-3)"};let q;if(!k.length)q='<p class="placeholder--padded">No spend in the last 90 days.</p>';else{const N=k.map(V=>C.reduce((K,se)=>K+(V.buckets[se]||0),0)),G=Math.max(...N)*1.1||.1,F=g/k.length;let W="";
2895k.forEach((V,K)=>{const se=u+F*K;let Q=0;C.forEach(X=>{const te=V.buckets[X]||0;if(!te)return;const le=h*(te/G),ue=m+h-h*((Q+te)/G);W+=`<rect x="${se.toFixed(1)}" y="${ue.toFixed(1)}" width="${(F-.5).toFixed(1)}" height="${le.toFixed(1)}" fill="${S[X]}" opacity="0.85"><title>${V.date}, ${X}: ${n(te)}</title></rect>`,Q+=te})});const z=C.map(V=>`<span class="trends-legend-item"><span class="trends-legend-swatch" style="background:${S[V]}"></span>${escHtml(V)}</span>`).join("");q=`
2896      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2897        ${v(G,V=>`$${V.toFixed(0)}`)}
2898        ${W}
2899        ${b(k.map(V=>V.date),y)}
2900      </svg>
2901      <div class="trends-legend">${z}</div>`}const I=t.weekly_cost_composition||[],T=t.targets_per_session||{fixed_usd:0,aai_usd:0,ai_usd:0};let P;if(!I.length)P='<p class="placeholder--padded">No sessions in the last 12 weeks.</p>';else{const N=Math.max(.1,...I.map(K=>K.total_usd)),G=Math.max(.1,...I.map(K=>(K.fixed_target_usd||0)+(K.aai_target_usd||0)+(K.ai_target_usd||0))),F=Math.max(N,G)*1.15,W=g/I.length;let z="",V="";I.forEach((K,se)=>{const Q=u+W*se,X=W-.5;let te=0;const le=[{key:"fixed_usd",tgtKey:"fixed_target_usd",label:"fixed",color:"var(--text-3)",opacity:.7},{key:"aai_usd",tgtKey:"aai_target_usd",label:"live tx",color:"var(--accent-blue)",opacity:.85},{key:"ai_usd",tgtKey:"ai_target_usd",label:"AI",color:"var(--accent-teal)",opacity:.85}];let ue=0;le.forEach(me=>{const de=K[me.key]||0,ye=K[me.tgtKey]||0;if(de){const Le=h*(de/F),Se=m+h-h*((te+de)/F),he=de>ye&&ye>0,be=he?` \xB7 OVER target ${n(ye)}`:ye>0?` \xB7 target ${n(ye)}`:"";z+=`<rect x="${Q.toFixed(1)}" y="${Se.toFixed(1)}" width="${X.toFixed(1)}" height="${Le.toFixed(1)}" fill="${me.color}" opacity="${me.opacity}" class="${he?"trends-bar-seg trends-bar-seg--over":"trends-bar-seg"}"><title>Week of ${K.week_start}, ${me.label}: ${n(de)}${be} (${K.sessions_n} sess)</title></rect>`,te+=de}if(ye>0){ue+=ye;const Le=m+h-h*(ue/F);V+=`<line x1="${(Q-1).toFixed(1)}" y1="${Le.toFixed(1)}" x2="${(Q+X+1).toFixed(1)}" y2="${Le.toFixed(1)}" stroke="${me.color}" stroke-width="1.5" stroke-dasharray="3 2" opacity="0.95"/>`}})}),P=`
2902      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2903        ${v(F,K=>`$${K.toFixed(0)}`)}
2904        ${z}
2905        ${V}
2906        ${b(I.map(K=>K.week_start),y)}
2907      </svg>
2908      <div class="trends-legend">
2909        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--text-3);opacity:0.7"></span>fixed (current ${n(t.fixed_per_session_usd||0)}/sess \xB7 tgt ${n(T.fixed_usd)})</span>
2910        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-blue)"></span>live transcript (tgt ${n(T.aai_usd)}/live)</span>
2911        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-teal)"></span>AI (tgt ${n(T.ai_usd)}/sess)</span>
2912        <span class="trends-legend-item trends-legend-item--meta"><span class="trends-legend-tick"></span>per-segment target</span>
2913      </div>`}const B=t.all_sessions_in_period||[],H=t.actual_fixed_per_session_30d_usd||0,A=t.target_fixed_per_session_30d_usd||0,x=t.fixed_monthly_usd||0,M=t.target_fixed_monthly_usd||0,D=t.sessions_in_30d||0;let O;if(!B.length)O='<p class="placeholder--padded">No sessions in the last 30 days.</p>';else{const N=Math.max(.1,...B.map(F=>F.total_usd),T.live_total_usd||0);O=`
2914      <div class="trends-allsess-list">${B.map(F=>{const W=we=>`${(100*(we/N)).toFixed(2)}%`,z=we=>`${(100*(we/N)).toFixed(2)}%`,V=W(F.fixed_usd||0),K=W(F.aai_usd||0),se=W(F.ai_usd||0),Q=z(F.target_fixed_boundary_usd||0),X=z(F.target_aai_boundary_usd||0),te=z(F.target_total_usd||0),le=(F.total_usd||0)>(F.target_total_usd||0),ue=(F.fixed_usd||0)>(F.target_fixed_boundary_usd||0),me=F.is_live&&(F.aai_usd||0)>(F.target_aai_boundary_usd||0)-(F.target_fixed_boundary_usd||0),de=(F.ai_usd||0)>(F.target_total_usd||0)-(F.target_aai_boundary_usd||0),ye=le&&F.target_total_usd?`<span class="trends-over-badge">${(F.total_usd/F.target_total_usd).toFixed(1)}\xD7 tgt</span>`:"",Le=(F.recap_run_count||0)>1?`<span class="trends-retry-badge">${F.recap_run_count} runs</span>`:"",Se=F.is_live?'<span class="trends-live-badge">live</span>':'<span class="trends-manual-badge">manual</span>',he=F.title?escHtml(F.title):"(untitled)",be=escHtml(F.campaign||""),ne=F.is_live?`<sp
2914an class="trends-allsess-tick trends-allsess-tick--aai" style="left:${X}" title="Live transcript target boundary ${n(F.target_aai_boundary_usd)} (cumulative)"></span>`:"";return`<div class="trends-allsess-row">
2915        <div class="trends-allsess-label">
2916          <span class="trends-allsess-camp" title="${be}">${be}</span>
2917          <span class="trends-allsess-title" title="${he}">${he}</span>
2918          ${Se}${Le}
2919        </div>
2920        <div class="trends-allsess-bar-wrap">
2921          <div class="trends-allsess-bar">
2922            <span class="trends-allsess-seg trends-allsess-seg--fixed${ue?" trends-allsess-seg--over":""}" style="width:${V}" title="Fixed ${n(F.fixed_usd)} (target ${n(F.target_fixed_boundary_usd)})"></span>
2923            <span class="trends-allsess-seg trends-allsess-seg--aai${me?" trends-allsess-seg--over":""}"   style="width:${K}"   title="Live transcript ${n(F.aai_usd)}"></span>
2924            <span class="trends-allsess-seg trends-allsess-seg--ai${de?" trends-allsess-seg--over":""}"    style="width:${se}"    title="AI ${n(F.ai_usd)}"></span>
2925            <span class="trends-allsess-tick trends-allsess-tick--fixed" style="left:${Q}" title="Fixed target boundary ${n(F.target_fixed_boundary_usd)}"></span>
2926            ${ne}
2927            <span class="trends-allsess-tick trends-allsess-tick--ai" style="left:${te}" title="Total target ${n(F.target_total_usd)}"></span>
2928          </div>
2929          <div class="trends-allsess-total">${n(F.total_usd)} ${ye}</div>
2930        </div>
2931      </div>`}).join("")}</div>
2932      <div class="trends-legend">
2933        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--text-3);opacity:0.7"></span>fixed (actual ${n(H)}/sess \xB7 tgt ${n(A)})</span>
2934        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-blue)"></span>live transcript (tgt ${n(T.aai_usd)}/live)</span>
2935        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-teal)"></span>AI (tgt ${n(T.ai_usd)}/sess)</span>
2936        <span class="trends-legend-item trends-legend-item--meta"><span class="trends-legend-target-mark"></span>per-segment target boundaries \xB7 live total ${n(T.live_total_usd)}/sess</span>
2937      </div>`}e.innerHTML=`
2938    <div class="trends-period-grid">${c}</div>
2939
2940    <div class="trends-grid">
2941      <section class="tf-card trends-chart-card">
2942        <div class="trends-chart-hdr">
2943          <span class="form-section-label">Cost per Live Session</span>
2944          <span class="trends-chart-sub">weekly \xB7 last 12wk \xB7 target ${n(w)}</span>
2945        </div>
2946        ${$}
2947      </section>
2948
2949      <section class="tf-card trends-chart-card">
2950        <div class="trends-chart-hdr">
2951          <span class="form-section-label">Recap Retry Rate</span>
2952          <span class="trends-chart-sub">% of live sessions with &gt;1 recap run \xB7 leading indicator</span>
2953        </div>
2954        ${E}
2955      </section>
2956
2957      <section class="tf-card trends-chart-card trends-chart-card--wide">
2958        <div class="trends-chart-hdr">
2959          <span class="form-section-label">Daily AI Spend by Bucket</span>
2960          <span class="trends-chart-sub">last 90 days</span>
2961        </div>
2962        ${q}
2963      </section>
2964
2965      <section class="tf-card trends-chart-card">
2966        <div class="trends-chart-hdr">
2967          <span class="form-section-label">Weekly Cost Composition</span>
2968          <span class="trends-chart-sub">fixed + live transcript + AI \xB7 last 12wk \xB7 fixed amortized over active sessions</span>
2969        </div>
2970        ${P}
2971      </section>
2972
2973      <section class="tf-card trends-chart-card">
2974        <div class="trends-chart-hdr">
2975          <span class="form-section-label">All Sessions (last 30 days)</span>
2976          <span class="trends-chart-sub">sorted by cost \xB7 stacked Fixed | Live | AI \xB7 fixed ${n(H)}/sess (actual ${n(x)}/mo \xF7 ${D} sessions) \u2192 target ${n(A)}/sess (at ${n(M)}/mo)</span>
2977        </div>
2978        ${O}
2979      </section>
2980    </div>`}window.adminLoadTrends=adminLoadTrends;async function adminLoadUsage(){const e=document.getElementById("admin-usage-wrap");if(!e)return;e.innerHTML='<div class="loading-row" style="justify-content:center;padding:2rem"><span class="spinner"></span></div>';let t;try{t=await api("GET","/api/admin/usage-trends")}catch(S){e.innerHTML=`<p class="placeholder--padded error">Usage load failed: ${escHtml(S.message)}</p>`;return}const n=t.weekly||[],a=t.totals_28d||{},s=t.review_instrumented_since,i=S=>{const q=S?.cur||0,I=S?.prev||0;if(!I)return q?'<span class="usage-delta usage-delta--new">new</span>':'<span class="usage-delta">\u2014</span>';const T=Math.round(100*(q-I)/I);return T>0?`<span class="usage-delta usage-delta--up">\u25B2 ${T}%</span>`:T<0?`<span class="usage-delta usage-delta--down">\u25BC ${Math.abs(T)}%</span>
2980`:'<span class="usage-delta">flat</span>'},o=(S,q,I)=>`
2981    <div class="tf-card tf-card--sm trends-period-card">
2982      <div class="trends-period-lbl">${S}</div>
2983      <div class="trends-period-val">${q?.cur||0} ${i(q)}</div>
2984      <div class="trends-period-sub">${I}</div>
2985    </div>`,r=!a.player_review?.cur&&s?`instrumenting from ${escHtml(s)}`:"player user-days \xB7 28d vs prior",c=[o("Player Review",a.player_review,r),o("GM Prep",a.gm_prep,"GM prep user-days \xB7 28d vs prior"),o("Scheduled Live",a.live_scheduled,"live sessions \xB7 28d vs prior"),o("Impromptu Live",a.live_impromptu,"live sessions \xB7 28d vs prior")].join(""),l=720,d=220,u=48,p=16,m=16,f=36,g=l-u-p,h=d-m-f,v=(S,q)=>{const I=[];for(let T=0;T<=4;T++){const P=S*(1-T/4),B=m+h*T/4;I.push(`<line x1="${u}" y1="${B}" x2="${l-p}" y2="${B}" class="trends-grid-line"/>`),I.push(`<text x="${u-6}" y="${B+4}" class="trends-grid-label" text-anchor="end">${q(P)}</text>`)}return I.join("")},b=S=>{const q=new Date(S);return`${q.getMonth()+1}/${q.getDate()}`},y=S=>{const q=S.length;if(!q)return"";const I=Math.max(1,Math.ceil(q/6)),T=[];for(let P=0;P<q;P+=I){const B=u+g*(P/Math.max(1,q-1));T.push(`<text x="${B}" y="${d-f+18}" class="trends-grid-label" text-anchor="middle">${escHtml(b(S[P]))}</text>`)}return T.join("")},_=(S,q,I,T)=>{const P=g/Math.max(1,S.length-1),B=S.map((x,M)=>({x:u+P*M,y:m+h*(1-x/q),v:x,i:M})),H=B.map((x,M)=>(M?"L":"M")+x.x.toFixed(1)+" "+x.y.toFixed(1)).join(" "),A=B.map(x=>`<circle cx="${x.x}" cy="${x.y}" r="3.2" class="trends-dot"><title>${T(x.i,x.v)}</title></circle>`).join("");return`<path d="${H}" class="trends-line ${I}"/>${A}`};let w;if(!n.some(S=>S.live_scheduled_n+S.live_impromptu_n))w='<p class="placeholder--padded">No live sessions in the last 12 weeks.</p>';else{const S=Math.max(1,...n.map(T=>T.live_scheduled_n+T.live_impromptu_n))*1.15,q=g/n.length;let I="";n.forEach((T,P)=>{const B=u+q*P+2,H=q-4,A=[{v:T.live_scheduled_n,color:"var(--accent-teal)",label:"scheduled"},{v:T.live_impromptu_n,color:"var(--accent-amber)",label:"impromptu"}];let x=0;A.forEach(M=>{if(!M.v)return;const D=h*(M.v/S),O=m+h-h*((x+M.v)/S);I+=`<rect x="${B.toFixed(1)}" y="${O.toFixed(1)}" width="${H.toFixed(1)}" height="${D.toFixed(1)}" fill="${M.color}" opacity="0.85" class="trends-bar-seg"><title>Week of ${T.week_start}, ${M.label}: ${M.v} live session${M.v===1?"":"s"}</title></rect>`,x+=M.v})}),w=`
2986      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2987        ${v(S,T=>String(Math.round(T)))}
2988        ${I}
2989        ${y(n.map(T=>T.week_start))}
2990      </svg>
2991      <div class="trends-legend">
2992        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-teal)"></span>scheduled (on the campaign's session slot / pinged)</span>
2993        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-amber)"></span>impromptu</span>
2994      </div>`}let $;if(!n.some(S=>S.gm_prep_user_days))$='<p class="placeholder--padded">No GM prep activity in the last 12 weeks.</p>';else{const S=Math.max(1,...n.map(q=>q.gm_prep_user_days))*1.15;$=`
2995      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
2996        ${v(S,q=>String(Math.round(q)))}
2997        ${_(n.map(q=>q.gm_prep_user_days),S,"trends-line--teal",(q,I)=>`Week of ${n[q].week_start}: ${I} GM user-day${I===1?"":"s"} \xB7 ${n[q].gm_prep_actions} prep actions`)}
2998        ${y(n.map(q=>q.week_start))}
2999      </svg>`}let E;if(!n.some(S=>S.player_review_user_days||S.gm_presence_user_days))E=`<p class="placeholder--padded">Presence rollup has no data yet${s?`, instrumenting from ${escHtml(s)}`:""}. First points land after the next hourly cron.</p>`;else{const S=Math.max(1,...n.map(q=>Math.max(q.player_review_user_days,q.gm_presence_user_days)))*1.15;E=`
3000      <svg class="trends-chart" viewBox="0 0 ${l} ${d}" preserveAspectRatio="none">
3001        ${v(S,q=>String(Math.round(q)))}
3002        ${_(n.map(q=>q.gm_presence_user_days),S,"trends-line--amber",(q,I)=>`Week of ${n[q].week_start}: ${I} GM user-day${I===1?"":"s"} in app`)}
3003        ${_(n.map(q=>q.player_review_user_days),S,"trends-line--teal",(q,I)=>`Week of ${n[q].week_start}: ${I} player user-day${I===1?"":"s"} in app`)}
3004        ${y(n.map(q=>q.week_start))}
3005      </svg>
3006      <div class="trends-legend">
3007        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-teal)"></span>players (review)</span>
3008        <span class="trends-legend-item"><span class="trends-legend-swatch" style="background:var(--accent-amber)"></span>GMs</span>
3009        ${s?`<span class="trends-legend-item trends-legend-item--meta">presence-instrumented from ${escHtml(s)}</span>`:""}
3010      </div>`}const k=t.tab_breakdown_28d||[];let C="";if(k.length){const S=I=>typeof _PRES_TAB_LABELS=="object"&&_PRES_TAB_LABELS[I]||I;C=`
3011      <section class="tf-card trends-chart-card">
3012        <div class="trends-chart-hdr">
3013          <span class="form-section-label">Where Time Goes</span>
3014          <span class="trends-chart-sub">by tab and role \xB7 last 28 days \xB7 presence hours are approximate</span>
3015        </div>
3016        <div class="usage-tab-list">${k.map(I=>`
3017      <div class="usage-tab-row">
3018        <span class="usage-tab-name">${escHtml(S(I.tab))}</span>
3019        <span class="usage-tab-role ${I.is_gm?"usage-tab-role--gm":""}">${I.is_gm?"GM":"player"}</span>
3020        <span class="usage-tab-meta">${I.user_days} user-day${I.user_days===1?"":"s"} \xB7 ~${I.hours}h</span>
3021      </div>`).join("")}</div>
3022      </section>`}e.innerHTML=`
3023    <div class="trends-period-grid">${c}</div>
3024    <div class="trends-grid">
3025      <section class="tf-card trends-chart-card">
3026        <div class="trends-chart-hdr">
3027          <span class="form-section-label">Live Sessions: Scheduled vs Impromptu</span>
3028          <span class="trends-chart-sub">weekly \xB7 last 12wk \xB7 ping/recurrence heuristic pre-stamp \xB7 test campaigns excluded</span>
3029        </div>
3030        ${w}
3031      </section>
3032      <section class="tf-card trends-chart-card">
3033        <div class="trends-chart-hdr">
3034          <span class="form-section-label">GM Prep</span>
3035          <span class="trends-chart-sub">GM user-days on prep AI (Fates, Threads brainstorm, Oracle, art, imports) \xB7 weekly \xB7 entity merges excluded (cleanup, not prep)</span>
3036        </div>
3037        ${$}
3038      </section>
3039      <section class="tf-card trends-chart-card">
3040        <div class="trends-chart-hdr">
3041          <span class="form-section-label">In-App Presence by Role</span>
3042          <span class="trends-chart-sub">distinct user-days \xB7 players in-app = review \xB7 weekly</span>
3043        </div>
3044        ${E}
3045      </section>
3046      ${C}
3047    </div>`}window.adminLoadUsage=adminLoadUsage;let _adminPulseInterval=null,_adminPulseState=2;function _apFmtTs(e){if(!e)return"\u2014";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}function _renderAdminPresence(e){const t=document.getElementById("admin-pulse-body");if(!t)return;const n=Object.values(e);if(!n.length||n.every(a=>!a.users.length)){t.innerHTML='<div class="ap-empty">No one online</div>';return}t.innerHTML=n.filter(a=>a.users.length>0).map(a=>`
3048      <div class="ap-campaign-section">
3049        <div class="ap-campaign-name">${escHtml(a.campaign_name)}</div>
3050        ${a.users.map(s=>`
3051          <div class="ap-user-row">
3052            <span class="ap-user-dot ${s.is_recording?"ap-user-dot--rec":""}"></span>
3053            <span class="ap-user-name">${escHtml(s.name||"Unknown")}</span>
3054            <span class="ap-user-tab">${escHtml(_PRES_TAB_LABELS[s.tab]||s.tab||"")}</span>
3055            ${s.is_recording?'<span class="ap-rec-tag">REC</span>':""}
3056          </div>
3057        `).join("")}
3058      </div>
3059    `).join("")}async function _refreshAdminPulse(){const e=document.getElementById("admin-pulse-ts");e&&(e.textContent=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}));try{const t=await api("GET","/api/admin/presence-detail");_presRecordingUsers=new Set,Object.values(t||{}).forEach(n=>{(n.users||[]).forEach(a=>{a.is_recording&&_presRecordingUsers.add(a.name)})}),_renderAdminPresence(t||{})}catch{}try{await api("GET","/api/admin/pulse")}catch{}}
3059function initAdminPulse(){if(!currentUser?.is_admin)return;const e=document.getElementById("admin-pulse-panel");if(!e)return;e.classList.remove("hidden"),e.dataset.state=_adminPulseState;const t=document.getElementById("admin-pulse-collapse-btn");t&&(t.textContent=_adminPulseState===0?"\u2014":""),_refreshAdminPulse(),_adminPulseInterval&&clearInterval(_adminPulseInterval),_adminPulseInterval=setInterval(_refreshAdminPulse,15e3)}function toggleAdminPulse(){_adminPulseState=(_adminPulseState+1)%3;const e=document.getElementById("admin-pulse-panel"),t=document.getElementById("admin-pulse-collapse-btn");e&&(e.dataset.state=_adminPulseState),t&&(t.textContent=_adminPulseState===0?"\u2014":"")}window.toggleAdminPulse=toggleAdminPulse,document.querySelectorAll(".admin-tab").forEach(e=>{e.addEventListener("click",()=>_adminSwitchTab(e.dataset.tab))}),document.getElementById("admin-back-btn").addEventListener("click",()=>{_adminClearHash(),showScreen("campaign-selector")}),document.getElementById("rec-finish-btn").addEventListener("click",()=>{!liveMediaRecorder||liveMediaRecorder.state==="inactive"||(_userInitiatedStop=!0,livePublishing=!0,liveMediaRecorder.stop())});function _liveShareLink(e){const t=currentCampaign&&currentCampaign.id;return t?`https://thread-fall.com/c/${t}`+(e?`?i=${encodeURIComponent(e)}`:""):"https://thread-fall.com"}function _liveShareText(e){return`\u{1F3B2} ${currentCampaign&&currentCampaign.name||"The campaign"} is live. Session starting now. Follow along: ${_liveShareLink(e)}`}function _closeLiveSharePop(){document.querySelectorAll(".rec-share-pop").forEach(e=>e.remove()),document.removeEventListener("click",_liveShareOutside,!0)}function _liveShareOutside(e){e.target.closest(".rec-share-pop")||e.target.closest("#rec-share-btn")||_closeLiveSharePop()}document.getElementById("rec-share-btn")?.addEventListener("click",async e=>{e.stopPropagation();const t=e.currentTarget;if(document.querySelector(".rec-share-pop")){_closeLiveSharePop();return}let n="";try{const l=currentCampaign?await _tfFetchInviteState(currentCampaign.id):null;l&&l.has_active&&l.allow_link_join!==!1&&(n=l.code)}catch{}const a=_liveShareText(n),s=_liveShareLink(n);let i=!1;try{i=!!navigator.share}catch{i=!1}const o=document.createElement("div");o.className="rec-share-pop tf-share-menu",o.innerHTML=`<p class="rec-share-pop-title">Session is live: share the link</p><textarea class="rec-share-pop-text" rows="3" readonly>${escHtml(a)}</textarea>`+(i?'<button type="button" class="tf-share-opt tf-share-opt--primary" data-sh="native">Share\u2026</button>':"")+`<button type="button" class="tf-share-opt${i?"":" tf-share-opt--primary"}" data-sh="copy">Copy message</button><button type="button" class="tf-share-opt" data-sh="copy-link">Copy link only</button>`+(n?`<p class="rec-share-pop-note">Anyone with this link can join your campaign as a player. You'll see every join on your roster, and you can reset the link in Manage.</p>`:"")+`<div class="tf-share-sep" aria-hidden="true"></div><a class="rec-share-pop-link" href="${escHtml(s)}" target="_blank" rel="noopener">${escHtml(s)}</a>`,document.body.appendChild(o);const r=t.getBoundingClientRect();o.style.top=`${Math.max(8,Math.min(r.bottom+6,window.innerHeight-o.offsetHeight-8))}px`,o.style.left=`${Math.max(8,Math.min(r.left,window.innerWidth-o.offsetWidth-8))}px`;const c=l=>o.querySelector(`[data-sh="${l}"]`);c("native")&&(c("native").onclick=async()=>{try{await navigator.share({text:a})}catch{}_closeLiveSharePop()}),c("copy").onclick=()=>{_tfCopyText(a,"Message copied, paste it into your group chat or Discord."),_closeLiveSharePop()},c("copy-link").onclick=()=>{_tfCopyText(s,"Link copied."),_closeLiveSharePop()},setTimeout(()=>document.addEventListener("click",_liveShareOutside,!0),0)}),document.addEventListener("click",e=>{const t=e.target.closest("[data-close-modal]");t&&t.closest(".modal-overlay")?.classList.add("hidden")}),document.getElementById("admin-back-btn")?.addEventListener("click",()=>{showScreen("campaign-selector"),loadCampaignSelector()}
3059),document.querySelectorAll(".admin-tab[data-tab]").forEach(e=>{e.addEventListener("click",()=>_adminSwitchTab(e.dataset.tab))}),document.getElementById("admin-panel-btn")?.addEventListener("click",()=>openAdminPanel()),document.getElementById("broadcast-watch-btn")?.addEventListener("click",_toggleWatcherPanel),document.getElementById("live-watcher-collapse-btn")?.addEventListener("click",_toggleWatcherPanel),document.getElementById("admin-pulse-mini-btn")?.addEventListener("click",toggleAdminPulse),document.getElementById("admin-pulse-collapse-btn")?.addEventListener("click",toggleAdminPulse),document.querySelectorAll(".view-as-btn[data-view-as]").forEach(e=>{e.addEventListener("click",()=>activateViewAs(e.dataset.viewAs))}),document.getElementById("treasury-modal-close")?.addEventListener("click",_closeTreasuryModal),document.getElementById("treasury-cancel-btn")?.addEventListener("click",_closeTreasuryModal),document.getElementById("treasury-save-btn")?.addEventListener("click",_saveTreasury),document.getElementById("treasury-add-item-btn")?.addEventListener("click",_addTreasuryItem),document.getElementById("treasury-create-item-btn")?.addEventListener("click",_createAndAddTreasuryItem),document.getElementById("ls-calc-btn")?.addEventListener("click",_calcLootSplit),document.getElementById("ls-apply-btn")?.addEventListener("click",_applyLootSplit);let _tfBillingTier=null,_tfBillingStatus=null,_tfBillingTiersCache=null,_tfPricingInterval="monthly",_tfThreadsLoaded=!1;const _TF_THREAD_FULL_TIERS=new Set(["standard","founding_gm","master","pro"]),_TF_SOURCE_LABELS={open_quest:"Open Quest",absent_npc:"Absent NPC",unused_item:"Unused Item",faction_dormancy:"Dormant Faction",player_intent:"Player Intent",foreshadowing:"Foreshadowing"};async function _tfLoadBillingTier(){try{const e=await api("GET","/api/billing/status");_tfBillingStatus=e||null,_tfBillingTier=e&&e.tier?e.tier:"free"}catch{_tfBillingStatus=null,_tfBillingTier="free"}currentUser&&currentUser.is_admin&&(_tfBillingTier="pro"),_renderHeaderTierBadge(),typeof _renderAnthologiesSection=="function"&&_renderAnthologiesSection(),_applyThreadsTabVisibility(),_applyTfAiGating(),_tfRefreshNotifications(),typeof _tfRsStart=="function"&&_tfRsStart()}let _tfNotifications=[],_tfNotifPollTimer=null;const _TF_NOTIF_POLL_MS=6e4,_tfShownReferralToastIds=new Set;let _tfPromoStatus=null,_tfPromoObserver=null;const _tfPromoImpressionFired=new WeakSet,_tfPromoLocalDismiss=new Set;async function _tfRefreshPromoStatus(){if(!getStoredToken())return null;try{_tfPromoStatus=await api("GET","/api/referral/promo-status")}catch{_tfPromoStatus=null}return _tfPromoStatus}function _ensurePromoObserver(){return _tfPromoObserver||(_tfPromoObserver=new IntersectionObserver(e=>{for(const t of e){if(!t.isIntersecting||_tfPromoImpressionFired.has(t.target))continue;_tfPromoImpressionFired.add(t.target);const n=t.target.dataset.surface,a=t.target.dataset.sessionId||null;api("POST","/api/referral/event",{surface:n,event_type:"impression",session_id:a?parseInt(a):null}).catch(()=>{})}},{threshold:0,rootMargin:"0px 0px -60px 0px"}),_tfPromoObserver)}async function _mountReferralPromoSlots(){const e=document.querySelectorAll(".referral-promo-slot--post-recap, .referral-promo-slot--post-live");if(!e.length)return;_tfPromoStatus||await _tfRefreshPromoStatus();const t={post_recap:{headline:"Know a GM who'd want this recap?",sub:"Share Threadfall: 2 session credits for you, 1 for them."},post_live:{headline:"Loved running that session?",sub:"Share Threadfall with a GM you know: 2 session credits for you, 1 for them."}};for(const n of e){n.innerHTML="";const a=n.dataset.surface||"post_recap",s=_tfPromoStatus?.[a];if(!(!!(s&&s.eligible)&&!_tfPromoLocalDismiss.has(a))){_tfPromoImpressionFired.delete(n);continue}const o=n.dataset.sessionId||"",r=t[a]||t.post_recap;n.innerHTML=`
3060      <div class="referral-promo-card referral-promo-card--inline">
3061        <button class="referral-promo-dismiss" title="Dismiss for 30 days" aria-label="Dismiss">\xD7</button>
3062        <div class="referral-promo-body">
3063          <strong>${escHtml(r.headline)}</strong>
3064          <span class="referral-promo-sub">${escHtml(r.sub)}</span>
3065        </div>
3066        <button class="btn-new btn-xs referral-promo-cta" type="button">Copy share link</button>
3067      </div>
3068    `,_ensurePromoObserver().observe(n);const c=n.querySelector(".referral-promo-card"),l=n.querySelector(".referral-promo-cta"),d=n.querySelector(".referral-promo-dismiss");l?.addEventListener("click",async()=>{const u=l.textContent;l.disabled=!0,l.textContent="Copying\u2026";try{if(!_tfBillingPacksCache||!_tfBillingPacksCache.__refLink){const f=await api("GET","/api/profile/referral-code");_tfBillingPacksCache||(_tfBillingPacksCache={}),_tfBillingPacksCache.__refLink=f.link||""}const p=_tfBillingPacksCache.__refLink;let m=!1;if(navigator.clipboard?.writeText)try{await navigator.clipboard.writeText(p),m=!0}catch{}l.textContent=m?"Copied!":"Press Ctrl+C",api("POST","/api/referral/event",{surface:a,event_type:"click",session_id:o?parseInt(o):null}).catch(()=>{})}catch{l.textContent="Try Profile"}setTimeout(()=>{l.disabled=!1,l.textContent=u},1500)}),d?.addEventListener("click",async()=>{_tfPromoLocalDismiss.add("post_recap"),_tfPromoLocalDismiss.add("post_live"),c?.classList.add("referral-promo-card--dismissed"),typeof showToast=="function"&&showToast("Dismissed, find your share link anytime in Profile."),api("POST","/api/referral/event",{surface:a,event_type:"dismiss",session_id:o?parseInt(o):null}).catch(()=>{}),setTimeout(async()=>{await _tfRefreshPromoStatus(),_mountReferralPromoSlots()},200)})}}function _tfFindUnreadNotificationForSession(e){return!e||!Array.isArray(_tfNotifications)?null:_tfNotifications.find(t=>t&&t.kind==="recap_ready"&&t.session_id===e&&!t.read_at)||null}async function _tfRefreshNotifications(){if(getStoredToken()){try{const e=await api("GET","/api/notifications");_tfNotifications=e&&Array.isArray(e.notifications)?e.notifications:[];for(const t of _tfNotifications)!t||t.kind!=="referral_credited"||t.read_at||_tfShownReferralToastIds.has(t.id)||(_tfShownReferralToastIds.add(t.id),typeof showToast=="function"&&showToast(t.title||"A referred friend just subscribed."));for(const t of _tfNotifications)!t||t.kind!=="card_created"||t.read_at||_tfShownReferralToastIds.has(t.id)||(_tfShownReferralToastIds.add(t.id),typeof showToast=="function"&&showToast(t.title||"One of your quotes was made into a card."));for(const t of _tfNotifications)if(!(!t||t.kind!=="quest_step_revealed"||t.read_at)&&!_tfShownReferralToastIds.has(t.id)){_tfShownReferralToastIds.add(t.id),typeof showToast=="function"&&showToast(t.title||"Your GM revealed something on your quest.");try{typeof _questsSurfaceVisible=="function"&&_questsSurfaceVisible()&&currentCampaign&&(_q2LoadedCid=null,renderQuests())}catch{}}try{const t=typeof _chr2Panel=="function"?_chr2Panel():null;typeof renderChronicleTimeline=="function"&&t&&!t.classList.contains("hidden")&&t.querySelector("#chron-toc-acts")&&!t.querySelector("input, textarea")&&renderChronicleTimeline()}catch{}}catch(e){console.warn("[notifications] refresh failed:",e?.message)}_tfNotifPollTimer&&clearTimeout(_tfNotifPollTimer),_tfNotifPollTimer=setTimeout(_tfRefreshNotifications,_TF_NOTIF_POLL_MS)}}const _TIER_LABELS={free:"Free",hobby:"Hobby",standard:"Standard",founding_gm:"Founding",master:"Master",pro:"Pro"};function _renderHeaderTierBadge(){const e=_tfBillingTier||"free",t=_TIER_LABELS[e]||"Free",n=document.getElementById("header-tier-badge");n&&n.parentNode&&n.parentNode.removeChild(n),document.querySelectorAll(".nav-profile-btn").forEach(a=>{a.dataset.tier=e,a.title=e==="free"?"Free plan \xB7 click for profile":`${t} plan \xB7 click for profile`})}function _showPricingScreen(){showScreen("pricing-screen"),_renderPricingPage()}let _tfBillingPacksCache=null;function _showTopupScreen(){showScreen("topup-screen"),location.hash!=="#topup"&&history.replaceState({},"","#topup"),_renderTopupPage()}function _hideTopupScreen(){currentCampaign&&currentCampaign.id?showScreen("main-app"):showScreen("campaign-selector"),location.hash==="#topup"&&history.replaceState({},"","/")}async function _renderTopupPage(){const e=document.getElementById("topup-pack-grid");e&&(e.innerHTML='<p class="placeholder">Loading\u2026</p>'),await _tfLoadBillingTier();const t=_tfBillingStatus||{},n=t.usage||{},a=t.tier_caps||{},s=document.getElementById("topup-balance-fullsess"),i=document.getElementById("topup-balance-recap");if(s){const o=n.sessions_this_period||0,r=a.sessions_per_month,c=Number(n.session_credits_remaining??0),l=_tfHalfCredits(n,"session");if(r>=100||r==null)s.textContent=c>0||l?`Unlimited + ${_tfCreditCount(c,l)} banked`:"Unlimited";else{const u=Math.max(0,(r||0)-o)+c;s.textContent=`${_tfCreditCount(u,l)}
3068 (incl. ${_tfCreditCount(c,l)} banked)`}}if(i){const o=Number(n.recap_credits_remaining??0);i.textContent=_tfCreditCount(o,_tfHalfCredits(n,"recap"))}if(!_tfBillingPacksCache)try{_tfBillingPacksCache=await api("GET","/api/billing/packs")}catch(o){e&&(e.innerHTML=`<p class="placeholder error">Could not load top-up packs: ${escHtml(o.message||"Unknown")}</p>`);return}_renderTopupPacks(_tfBillingPacksCache.packs||[])}function _renderTopupPacks(e){const t=document.getElementById("topup-pack-grid");if(t){t.innerHTML="";for(const n of e){const a=n.key==="recap_ten",s=document.createElement("div");s.className="topup-pack-card"+(a?" topup-pack-card--featured":"");const i=`$${(n.price_cents/100).toFixed(2)}`,o=n.credits_granted>1?`$${(n.price_cents/n.credits_granted/100).toFixed(2)} per credit`:"",r=n.credits_granted===1?`+1 ${n.credit_kind} credit`:`+${n.credits_granted} ${n.credit_kind} credits`,c=n.configured?"Buy":"Coming soon";s.innerHTML=`
3069      <p class="topup-pack-label">${escHtml(n.label)}</p>
3070      <p class="topup-pack-sublabel">${escHtml(n.sublabel||"")}</p>
3071      <div class="topup-pack-price">
3072        <span class="topup-pack-price-amount">${escHtml(i)}</span>
3073        ${o?`<span class="topup-pack-price-per">${escHtml(o)}</span>`:""}
3074      </div>
3075      <div class="topup-pack-credits">${escHtml(r)}</div>
3076      <div class="topup-pack-cta">
3077        <button type="button" class="btn-new" data-topup-pack="${escHtml(n.key)}" ${n.configured?"":"disabled"}>${escHtml(c)}</button>
3078      </div>
3079    `,t.appendChild(s)}t.querySelectorAll("button[data-topup-pack]").forEach(n=>{n.addEventListener("click",async()=>{const a=n.getAttribute("data-topup-pack");n.disabled=!0;const s=n.textContent;n.textContent="Redirecting\u2026";try{const i=await api("POST",`/api/billing/buy-session?pack=${encodeURIComponent(a)}`,{});if(i&&i.url){window.location.href=i.url;return}showToast("Could not start checkout. Please try again.")}catch(i){showToast("Checkout error: "+(i.message||"Unknown"))}finally{n.disabled=!1,n.textContent=s}})})}}document.getElementById("topup-back-btn")?.addEventListener("click",_hideTopupScreen);function _hidePricingScreen(){if(!getStoredToken()){showScreen("auth-screen"),typeof showAuthView=="function"&&showAuthView("register");try{history.replaceState({},"","/")}catch{}return}currentCampaign&&currentCampaign.id?showScreen("main-app"):showScreen("campaign-selector"),(location.pathname!=="/"||location.hash==="#pricing")&&history.replaceState({},"","/")}async function _renderPricingPage(){const e=document.getElementById("pricing-tier-grid");e&&(e.innerHTML='<p class="placeholder">Loading plans\u2026</p>');let t=_tfBillingTiersCache;if(!t)try{_tfBillingTiersCache=await api("GET","/api/billing/tiers"),t=_tfBillingTiersCache}catch(n){e&&(e.innerHTML=`<p class="placeholder error">Could not load plans: ${escHtml(n.message||"Unknown")}</p>`);return}if(!_tfBillingStatus&&getStoredToken())try{await _tfLoadBillingTier()}catch{}_renderPricingTierCards(t),_renderFoundingBanner(t)}function _renderFoundingBanner(e){const t=document.getElementById("pricing-founding-banner"),n=document.getElementById("pricing-founding-deadline-text");if(!t||!n||!e||!e.founding_gm_deadline)return;const a=new Date(e.founding_gm_deadline+"T23:59:59Z"),s=new Date;if(s>a){t.classList.add("hidden");return}const i=Math.ceil((a.getTime()-s.getTime())/(1e3*60*60*24)),o=a.toLocaleDateString(void 0,{month:"long",day:"numeric"});n.textContent=`Enrollment closes ${o}, ${i} day${i===1?"":"s"} left.`,t.classList.remove("hidden")}const _PRICING_TAGLINES={hobby:"One active campaign: everything the table needs to run",standard:"The Prep Suite: never start prep from a blank page",founding_gm:"Everything in Standard, at the founding rate",master:"Unlimited campaigns + Anthologies: bind them into a saga",pro:"No caps: every table, every session"};function _renderFeatureBullet(e){return`<li class="pricing-feature-row"><span class="pricing-feature-check" aria-hidden="true">\u2713</span><span class="pricing-feature-text"><span class="pricing-feature-title"><strong>${escHtml(e.title)}</strong></span>`+(e.sub?`<span class="pricing-feature-sub">${escHtml(e.sub)}</span>`:"")+"</span></li>"}function _renderTierBlocks(e){return Array.isArray(e)?e.map(t=>t.kind==="bullets"?(t.items||[]).map(_renderFeatureBullet).join(""):t.kind==="bullet"?_renderFeatureBullet(t.item||{}):t.kind==="suite"?`<li class="pricing-feature-suite"><span class="p2-section-eyebrow pricing-suite-eyebrow">${escHtml(t.title||"")}</span>`+(t.intro?`<div class="pricing-feature-sub">${escHtml(t.intro)}</div>`:"")+(t.items&&t.items.length?'<ul class="pricing-feature-sublist">'+t.items.map(n=>`<li><strong>${escHtml(n.title)}</strong>: ${escHtml(n.sub)}</li>`).join("")+"</ul>":"")+"</li>":t.kind==="inherit"?`<li class="pricing-feature-inherit"><span class="pricing-feature-check" aria-hidden="true">\u2713</span><em>${escHtml(t.label||"")}</em></li>`:"").join(""):""}const _SESSION_CADENCE={hobby:"a monthly or bi-weekly game",standard:"a weekly campaign through a full arc",founding_gm:"a weekly campaign through a full arc",master:"weekly play with room to spare",pro:"multiple campaigns, never pause"};function _renderPricingTierCards(e){const t=document.getElementById("pricing-tier-grid");if(!t||!e)return;const n=_tfPricingInterval||"monthly",a=_tfBillingStatus&&_tfBillingStatus.tier||"free",s=!_tfBillingStatus||_tfBillingStatus.has_access!==!1,i=a!=="free"&&!s?"free":a,o=_tfBillingStatus&&_tfBillingStatus.interval,r=!!(_tfBillingStatus&&_tfBillingStatus.founding_gm_eligible),c=_tfBillingStatus&&_tfBillingStatus.founding_gm_window_expires_at;let l;if(c)l=new Date>new Date(c);else{const g=e.founding_gm_deadline;l=g&&new Date>new Date(g+"T23:59:59Z")}const d={free:0};
3079(e.tiers||[]).forEach(g=>{d[g.key]=g.price_monthly_cents});const u=d[i]??0,p=(e.tiers||[]).filter(g=>!(g.key==="founding_gm"&&i!=="founding_gm"&&(!r||l))),m=i==="free",f=`<div class="tf-card tf-card--lg pricing-tier-card pricing-tier-card--player${m?" is-selected":""}"><h3 class="pricing-tier-name">Player</h3><p class="pricing-tier-tagline">Always free, for everyone at your table</p><div class="pricing-tier-price"><span class="pricing-tier-price-amount">$0</span><span class="pricing-tier-price-per">forever</span></div><ul class="pricing-tier-features"><li class="pricing-feature-volume"><span class="pricing-vol-sessions">2 trial recaps</span><span class="pricing-vol-cadence">run the whole thing on your own table before you pay</span></li>`+_renderFeatureBullet({title:"Join any campaign"})+_renderFeatureBullet({title:"Read every chronicle & browse the codex"})+_renderFeatureBullet({title:"Post during Live and after"})+"</ul>"+(m?'<button type="button" class="btn-new pricing-cta-btn" disabled>Current plan</button>':'<button type="button" class="btn-new pricing-cta-btn" data-pricing-player-start="1">Start free</button>')+"</div>";t.innerHTML=f+p.map(g=>{const v=((n==="annual"?g.price_annual_cents:g.price_monthly_cents)/100).toFixed(2).replace(/\.00$/,""),b=n==="annual"?"/year":"/mo",y=i===g.key&&(!o||o===n),_=g.key==="founding_gm",w=g.tagline||_PRICING_TAGLINES[g.key]||"",$=g.sessions_per_month>=100,E=g.sessions_label||($?"Unlimited recaps":`${g.sessions_per_month} recaps/mo`),k=g.cadence||_SESSION_CADENCE[g.key]||"",C=g.campaigns_label||(g.campaigns>=100?"Unlimited campaigns":g.campaigns===1?"1 campaign":`${g.campaigns} campaigns`),S=g.header_images_per_month||0,q=g.images_label!=null?g.images_label:S>=100?"Unlimited AI art images":S?`${S} AI art image${S===1?"":"s"}/mo`:"",T=`<li class="pricing-feature-volume"><span class="pricing-vol-sessions">${escHtml(E)}</span>`+(k?`<span class="pricing-vol-cadence">${escHtml(k)}</span>`:"")+`<span class="pricing-vol-campaigns">${escHtml(C)}</span>`+(q?`<span class="pricing-vol-images">${escHtml(q)}</span>`:"")+"</li>"+_renderTierBlocks(g.blocks),P=_?'<p class="gm-surface pricing-tier-founding-promise"><strong>$7.99/mo</strong> for as long as you stay subscribed.</p>':"";let B,H="";const A=e.gift,x=g.key==="standard"&&A&&A.price_cents,M=x?`<a class="pricing-cta-split__gift" href="/gift?src=app_pricing_card" target="_blank" rel="noopener" title="Gift Standard: ${A.duration_months} months for $${Math.round(A.price_cents/100)}" aria-label="Gift Standard to your GM: ${A.duration_months} months for $${Math.round(A.price_cents/100)}"><svg class="pricing-cta-split__gift-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><g stroke="var(--on-accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="8" width="18" height="4" rx="1"/><path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"/></g><g stroke="var(--ember, var(--color-danger))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v13"/><path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5"/></g></svg><span class="pricing-cta-split__gift-label">Gift Standard</span><span class="pricing-cta-split__gift-label-short">Gift</span></a>`:"",D=(W,z)=>`<div class="pricing-cta-split ${W}">${z}${M}</div>`;if(y)B=x?D("btn-ghost",'<button type="button" class="pricing-cta-split__main" disabled><span class="pricing-cta-split__main-label">Current plan</span></button>'):'<button type="button" class="btn-ghost pricing-cta-btn" disabled>Current plan</button>';else if(_&&l)B='<button type="button" class="btn-ghost pricing-cta-btn" disabled title="Founding enrollment has closed">Enrollment closed</button>';else if(u>0&&g.price_monthly_cents<u)B=x?D("btn-ghost",'<button type="button" class="pricing-cta-split__main" data-pricing-portal="1" title="Manage your plan in the Stripe portal"><span class="pricing-cta-split__main-label">Manage in Portal</span></button>'):'<button type="button" class="btn-ghost pricing-cta-btn" data-pricing-portal="1" title="Manage your plan in the Stripe portal">Manage in Portal</button>';else{const W=u>0?"Upgrade":"Subscribe";B=x?D("btn-new",`<button type="button" class="pricing-cta-split__main" data-pricing-subscribe="${g.key}" aria-label="${W} Standard"><span class="pricing-cta-split__main-label">${W}</span></button>`):`<button type="button" class="btn-new pricing-cta-btn" data-pricing-subscribe="${g.key}">${W}</button>`,(g.key==="hobby"||g.key==="standard")&&(H='<p class="pricing-cta-flavor">\u{1FA99} Shut up and take my GP!</p>')}const O=g.key==="standard",N=O?" pricing-tier-card--featured":"",G=O?'<div class="cx2-flag pricing-tier-flag">Most popular</div>':"",F=w?`<p class="pricing-tier-tagline">${escHtml(w)}</p>`:"";return`<div class="tf-card tf-card--lg pricing-tier-card pricing-tier-card--${g.key}${N}${y?" is-selected":""}">`+G+`<h3 class="pricing-tier-name">${escHtml(g.name)}</h3>`+F+`<div class="pricing-tier-price"><span class="pricing-tier-price-amount">$${escHtml(v)}</span><span class="pricing-tier-price-per">${escHtml(b)}</span></div>`+P+`<ul class="pricing-tier-features">${T}</ul>`+B+H+"</div>"}).join("")}async function _tfOpenBillingPortal(){try{const e=await api("POST","/api/billing/create-portal-session",{});if(e&&e.url)return window.location.href=e.url,!0;showToast("Could not open Stripe portal. Please try again.")}catch(e){showToast("Stripe portal error: "+(e.message||"Unknown"))}return!1}async function _tfCheckoutError(e){if(e&&e.status===409){await confirmDialog({title:"You already have a subscription",message:(e.message||"You already have an active subscription.")+`
3080
3081The billing portal switches your existing plan (with proration) instead of starting a second one.`,confirmLabel:"Open Manage Subscription",cancelLabel:"Not now",danger:!1})&&await _tfOpenBillingPortal();return}showToast("Checkout error: "+(e&&e.message||"Unknown"))}async function _subscribeToTier(e){if(!getStoredToken()){_hidePricingScreen(),document.getElementById("reg-email")?.focus();return}const t=_tfPricingInterval||"monthly",n=document.querySelector(`[data-
3081pricing-subscribe="${e}"]`),a=n?n.textContent:"";n&&(n.disabled=!0,n.textContent="Redirecting\u2026");try{const s=await api("POST","/api/billing/create-checkout-session",{tier:e,interval:t});if(s&&s.url){window.location.href=s.url;return}showToast("Could not start checkout, please try again.")}catch(s){await _tfCheckoutError(s)}finally{n&&(n.disabled=!1,n.textContent=a)}}async function _openBillingPortal(e){const t=e?e.textContent:"";e&&(e.disabled=!0,e.textContent="Opening\u2026");try{const n=await api("POST","/api/billing/create-portal-session",{});if(n&&n.url){window.location.href=n.url;return}showToast("Could not open the billing portal, please try again.")}catch(n){showToast("Portal error: "+(n.message||"Unknown"))}finally{e&&(e.disabled=!1,e.textContent=t)}}document.getElementById("pricing-back-btn")?.addEventListener("click",_hidePricingScreen),document.querySelectorAll("[data-pricing-interval]").forEach(e=>{e.addEventListener("click",()=>{_tfPricingInterval=e.dataset.pricingInterval||"monthly",document.querySelectorAll("[data-pricing-interval]").forEach(t=>{t.classList.toggle("is-active",t===e),t.setAttribute("aria-selected",t===e?"true":"false")}),_tfBillingTiersCache&&_renderPricingTierCards(_tfBillingTiersCache)})}),document.getElementById("pricing-tier-grid")?.addEventListener("click",e=>{const t=e.target.closest("[data-pricing-subscribe]");if(t){_subscribeToTier(t.dataset.pricingSubscribe);return}if(e.target.closest("[data-pricing-player-start]")){_hidePricingScreen(),document.getElementById("reg-email")?.focus();return}const a=e.target.closest("[data-pricing-portal]");if(a){_openBillingPortal(a);return}}),document.getElementById("pricing-referral-link")?.addEventListener("click",async e=>{e.preventDefault();const t=e.currentTarget,n=t.textContent;t.textContent="Copying\u2026";try{const a=await api("GET","/api/profile/referral-code");let s=!1;if(a&&a.link&&navigator.clipboard?.writeText)try{await navigator.clipboard.writeText(a.link),s=!0}catch{}t.textContent=s?"Link copied!":a&&a.link?a.link:"See Profile",typeof showToast=="function"&&s&&showToast("Referral link copied, share it with a GM.")}catch{t.textContent="Sign in to get your link"}setTimeout(()=>{t.textContent=n},2200)});async function _handleBillingLandingFromPath(){const e=location.pathname||"";if(!(e==="/billing"||e.startsWith("/billing/")||e==="/pricing"))return!1;const t=document.getElementById("pricing-result-banner");if(e.startsWith("/billing/success")){if(new URLSearchParams(location.search).get("type")==="session_credit"){let s=null;try{s=JSON.parse(localStorage.getItem("tf_post_checkout_return")||"null")}catch{s=null}try{localStorage.removeItem("tf_post_checkout_return")}catch{}try{history.replaceState({},"","/")}catch{}if(setTimeout(_tfLoadBillingTier,1500),setTimeout(()=>showToast("+1 session credit added: you can record now.",6e3),200),s&&s.campaignId){const i=s.campaignId;return setTimeout(()=>{try{bootWithCampaignId(i)}catch{}},250),!0}return!1}return t&&(t.classList.remove("hidden","pricing-result-banner--cancel"),t.classList.add("pricing-result-banner--success"),t.innerHTML="<strong>\u{1F389} Welcome aboard!</strong> Your subscription is being activated. It can take a few seconds for the Stripe webhook to settle, refresh in a moment if your plan isn't reflected yet."),setTimeout(_tfLoadBillingTier,2e3),_showPricingScreen(),!0}return e.startsWith("/billing/cancel")?(t&&(t.classList.remove("hidden","pricing-result-banner--success"),t.classList.add("pricing-result-banner--cancel"),t.innerHTML="<strong>No charge.</strong> Your plan is unchanged. Come back any time, your Founding rate is held until enrollment closes."),_showPricingScreen(),!0):e==="/billing"||e==="/pricing"?(t&&t.classList.add("hidden"),await _tfLoadBillingTier(),_showPricingScreen(),!0):!1}function _handleHashRoute(){return location.hash==="#pricing"?(_showPricingScreen(),!0):location.hash==="#topup"?(_showTopupScreen(),!0):!1}window.addEventListener("hashchange",_handleHashRoute),document.addEventListener("click",e=>{if(e.target.closest(".tfa-plans-link")){e.preventDefault();try{history.pushState({},"","#pricing")}catch{}_showPricingScreen()}});const _TF_VALID_TABS=["sessions","party","compendium","quests","map","manage","threads","mychar"];let _tfRouterSilenced=!1;function _tfRouterParse(e){if(e=e||location.pathname||"/",e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e==="/"||e==="")return{type:"selector"};if(e==="/pricing"||e.startsWith("/billing"))return null;const t=e.match(/^\/join\/([A-Za-z0-9_-]{4,64})$/);if(t)return{type:"join",code:t[1]};const n=e.match(/^\/c\/(\d+)(?:\/([a-z]+)(?:\/(\d+))?)?$/);if(!n)return null;const a=parseInt(n[1],10);let s=n[2]||"sessions";const i=n[3]?parseInt(n[3],10):null;
3081return i!==null&&s!=="sessions"?{type:"campaign",cid:a,tab:"sessions",sid:null}:(_TF_VALID_TABS.includes(s)||(s="sessions"),s==="quests"?{type:"campaign",cid:a,tab:"party",sid:i,section:"quests"}:{type:"campaign",cid:a,tab:s,sid:i})}function _tfRouterBuildPath(e){return!e||e.type==="selector"||e.type!=="campaign"||!e.cid?"/":e.tab==="sessions"&&e.sid?`/c/${e.cid}/sessions/${e.sid}`:e.tab&&e.tab!=="sessions"?`/c/${e.cid}/${e.tab}`:`/c/${e.cid}`}function _tfRouterPush(e){if(_tfRouterSilenced)return;const t=_tfRouterBuildPath(e);if(t!==location.pathname)try{history.pushState(e,"",t)}catch{}}function _tfRouterReplace(e){if(_tfRouterSilenced)return;const t=_tfRouterBuildPath(e);try{history.replaceState(e,"",t)}catch{}}async function _tfRouterApply(e){if(e){_tfRouterSilenced=!0;try{if(e.type==="selector"){currentCampaign=null,showScreen("campaign-selector"),await loadCampaignSelector();return}if(e.type==="join"){try{const t=await api("POST","/api/campaigns/join",{invite_code:e.code,via:"link"});showToast(t.already_member?`Already a member of "${t.campaign.name}".`:`Joined "${t.campaign.name}"!`,4e3);const n=t&&t.campaign&&t.campaign.id;if(n){try{history.replaceState({type:"campaign",cid:n,tab:"sessions",sid:null},"",`/c/${n}`)}catch{}await bootWithCampaignId(n)}else{try{history.replaceState({type:"selector"},"","/")}catch{}showScreen("campaign-selector"),await loadCampaignSelector()}}catch(t){const n=t&&t.message?t.message:"Join failed.";showToast(n,5e3);try{history.replaceState({type:"selector"},"","/")}catch{}showScreen("campaign-selector"),await loadCampaignSelector()}return}if(e.type==="campaign"){const t=++_tfNavSeq;if(_tfPendingLanding=e.tab||"sessions",(!currentCampaign||currentCampaign.id!==e.cid)&&(await bootWithCampaignId(e.cid),!currentCampaign)){showToast("Couldn't open that campaign; it may be private. Ask the GM for an invite link.",6e3);try{history.replaceState({type:"selector"},"","/")}catch{}return}if(_tfNavStale(t))return;const n=document.querySelector(`.tab-btn[data-tab="${e.tab}"], .nav-utility-btn[data-tab="${e.tab}"]`);if(e.tab==="party"&&e.section==="quests"?await _partyOpenQuests(e.cid):n&&n.click(),e.tab==="sessions"&&e.sid){const a=_tfNavSeq;let s=0;const i=()=>{_tfNavStale(a)||(s++,Array.isArray(allSessions)&&allSessions.some(o=>o.id===e.sid)?typeof renderChronicleRecap=="function"&&renderChronicleRecap(e.sid):s<30&&setTimeout(i,100))};setTimeout(i,120)}}}finally{_tfRouterSilenced=!1,_tfPendingLanding=null}}}window.addEventListener("popstate",e=>{const t=e.state&&e.state.type?e.state:_tfRouterParse(location.pathname);t&&_tfRouterApply(t)});async function _handleCampaignDeepLink(){_tfConsumeDoorStash();const e=_tfRouterParse(location.pathname);return!e||e.type!=="campaign"&&e.type!=="join"?!1:(e.type==="campaign"&&window._tfDoorCode&&await _tfDoorJoinIfNeeded(e.cid),await _tfRouterApply(e),!!currentCampaign)}async function _tfDoorJoinIfNeeded(e){const t=window._tfDoorCode;if(window._tfDoorCode=null,!!t){try{const n=await api("GET","/api/campaigns");if(Array.isArray(n)&&n.some(a=>a&&a.id===e))return}catch{}try{const n=await api("GET",`/api/campaigns/invite-preview?i=${encodeURIComponent(t)}`);if(!n||!n.joinable)return;if(!window._tfDoorConsent){const s=n.member_count||0;if(!await confirmDialog({title:`Join "${n.name}"?`,message:`${n.gm_name}'s campaign \xB7 ${s} member${s===1?"":"s"}. You'll join as a player, the GM sees every join on their roster.`,confirmLabel:"Join campaign",cancelLabel:"Not now",danger:!1}))return}!(await api("POST","/api/campaigns/join",{invite_code:t,via:"link"})).already_member&&n.name&&showToast(`Joined "${n.name}"!`,4e3)}catch{}finally{window._tfDoorConsent=!1}}}async function _tfMaybeRedeemGift(){let e=null;try{e=localStorage.getItem("tf_gift_code")}catch{}if(e){try{localStorage.removeItem("tf_gift_code")}catch{}try{const t=await api("POST","/api/gifts/redeem",{code:e}),n=t.duration_months||3;t.result==="credited"?showToast(`Gift redeemed. You already subscribe, so we added ${t.credits_added} session credits instead. Thank ${t.gifter_name||"your gifter"} for us.`,1e4):t.result==="extended"?showToast(`Gift redeemed, your Threadfall window just grew by ${n} months.`,9e3):showToast(`Gift redeemed. ${n} months of Threadfall Standard are live on your account. Hit record at your next session.`,1e4),_tfLoadBillingTier()}catch(t){showToast(t.message||"Gift redemption failed, the code may be invalid or already used.",9e3)}}}async function _tfRenderInviteTeaser(){const e=window._tfDoorCode,t
3081=document.getElementById("tfa-invite-teaser");if(t){if(!e){if(!window._tfDeadDoor)return;t.innerHTML=`<div class="tfa-it-meta">This invite link isn't active, ask your GM for a current one.</div>`,t.classList.remove("hidden");return}try{const n=await api("GET",`/api/campaigns/invite-preview?i=${encodeURIComponent(e)}`);if(n&&n.joinable){const a=n.member_count||0;t.innerHTML=`<div class="tfa-it-eyebrow">You've been invited to join</div><div class="tfa-it-name">${escHtml(n.name||"a campaign")}</div><div class="tfa-it-meta">GM'd by ${escHtml(n.gm_name||"the GM")} \xB7 ${a} member${a===1?"":"s"}</div>`,t.classList.remove("hidden"),window._tfDoorConsent=!0}else t.innerHTML=`<div class="tfa-it-meta">This invite link isn't active, ask your GM for a current one.</div>`,t.classList.remove("hidden"),window._tfDoorCode=null}catch{}}}function _tfStashPostLoginPath(){const e=location.pathname;if(e&&(e.startsWith("/c/")||e.startsWith("/join/")))try{localStorage.setItem("tf_post_login_path",e)}catch{}}function _tfWarnDeadDoor(){window._tfDeadDoor&&(window._tfDeadDoor=!1,showToast("This invite link isn't active, ask your GM for a current one.",9e3))}function _tfConsumeDoorStash(){let e=null;try{e=localStorage.getItem("tf_door_stash"),e!==null&&localStorage.removeItem("tf_door_stash")}catch{}if(e)try{const t=JSON.parse(e);if(!t||typeof t.code!="string"||!/^[A-Za-z0-9_-]{4,64}$/.test(t.code)||!t.ts||Date.now()-t.ts>15*60*1e3)return;window._tfDoorCode||(window._tfDoorCode=t.code,t.consent&&(window._tfDoorConsent=!0))}catch{}}function _tfConsumePostLoginPath(){let e=null;try{e=localStorage.getItem("tf_post_login_path"),localStorage.removeItem("tf_post_login_path")}catch{}if(e&&(e.startsWith("/c/")||e.startsWith("/join/"))){try{history.replaceState(null,"",e)}catch{}return!0}return!1}const _FEATURE_REQUIRED_TIER={recap:"hobby",live:"hobby",codex:"hobby",bardic:"hobby",party_assess:"hobby",vision_import:"hobby",loose_threads:"master",weave:"master",fates:"master",ai_live_context:"master"},_PAID_TIERS=new Set(["hobby","standard","founding_gm","master","pro"]),_MASTER_PLUS_TIERS=new Set(["master","pro"]);function _tfFeatureAllowed(e){const t=_tfBillingTier||"free",n=_FEATURE_REQUIRED_TIER[e];return n?n==="hobby"?_PAID_TIERS.has(t):n==="master"?_MASTER_PLUS_TIERS.has(t):!0:!0}function _tfRequireFeature(e){if(_tfFeatureAllowed(e))return!0;const n=_FEATURE_REQUIRED_TIER[e]==="master"?"master_locked":"recap_no_bank";return _showUpgradeModal({scenario:n,locked_feature:e}),!1}const _MASTER_FEATURE_BODIES={loose_threads:"Loose Threads watches your campaign for unresolved beats (absent NPCs, dormant factions, dropped foreshadowing) and surfaces them before your next session so nothing slips through. The rest of AI Prep Suite (Weave + Fates) turns those hooks into a scene you can run with minimal prep.",weave:"Weave takes 2-4 of your loose threads and suggests how they intersect into one playable scene, so you don\u2019t have to manually connect dots at 11pm the night before game.",fates:"Fates writes the full scene from your selected hooks: GM notes, NPC motivations, and foreshadow seeds planted for future sessions.",_default:"AI Prep Suite watches your campaign between sessions and turns unresolved beats into playable scenes, so prep takes minutes, not hours."};function _showUpgradeModal(e){const t=document.getElementById("upgrade-modal");if(!t)return;_tfCoolPaperizeModal(t);const n=_tfBillingStatus||{},a=_tfBillingTier||"free",s=!!n.founding_gm_eligible,i=n.founding_gm_locked_at,o=n.founding_gm_window_expires_at,r=n.founding_gm_deadline;let c=!1,l=0;const d=o?new Date(o):r?new Date(r+"T23:59:59Z"):null;if(s&&!i&&d){const w=new Date;w<=d&&(c=!0,l=Math.ceil((d.getTime()-w.getTime())/864e5))}const u=document.getElementById("upgrade-modal-title"),p=document.getElementById("upgrade-modal-body"),m=document.getElementById("upgrade-modal-primary"),f=document.getElementById("upgrade-modal-pro-footnote"),g=document.getElementById("upgrade-modal-secondary");let h="hobby",v="\u2694 Get Hobby \xB7 $10/mo",b=!1,y="Not yet";if(e.scenario==="master_locked"){u.textContent="Your hooks are piling up between sessions.";const w=_MASTER_FEATURE_BODIES[e.locked_feature]||_MASTER_FEATURE_BODIES._default,$=_PAID_TIERS.has(a)&&!_MASTER_PLUS_TIERS.has(a)?`<p class="upgrade-modal-ack">As a ${_TIER_LABELS[a]||"paid"} member, you\u2019re already in the game, Master adds the prep tools.</p>`:"";p.innerHTML=$+`<p>${escHtml(w)}</p>`,h="master",v="\u2694 Upgrade to Master \xB7 $40/mo",b=!0}else if(e.scenario==="cap_exhausted"){const w=n.current_period_end?new Date(n.current_period_end):null,$=w?w.toLocaleDateString(void 0,{month:"long",day:"numeric"}):"next cycle";a==="hobby"?(u.textContent="You\u2019ve outrun your scroll for this month.",p.innerHTML="<p>You\u2019ve used both recaps for the month. Standard gives you 6/mo across 3 campaigns.</p>",h="standard",v="\u2694 Upgrade to Standard \xB7 $25/mo"):a==="standard"||a==="founding_gm"?(u.textContent="Six sessions and your table\u2019s still going. That\u2019s the problem.",p.innerHTML="<p>Your group runs too often for Standard. Master gives you 10 recaps/mo, unlimited campaigns, and AI prep tools to keep up.<
3081/p>",h="master",v="\u2694 Upgrade to Master \xB7 $40/mo"):(u.textContent="Ten sessions wasn\u2019t enough. Respect.",p.innerHTML="<p>Pro removes the session cap entirely, run as many recaps as your table can generate.</p>",h="pro",v="\u2694 Upgrade to Pro \xB7 $60/mo"),y=`Wait until ${$}`}else u.textContent="Your scroll is empty, Chronicler.",p.innerHTML='<p>You\u2019ve used both your starter recaps. Hobby keeps your campaign history alive:</p><ul class="upgrade-modal-bullets"><li>AI recaps after every session</li><li>Auto-built campaign wiki</li><li>Bardic Companion: chat with your campaign history</li></ul>',h="hobby",v="\u2694 Get Hobby \xB7 $10/mo";e.scenario==="recap_no_bank"&&c&&(h="founding_gm",v="\u{1FA99} Claim Founding GM \xB7 $7.99/mo",p.innerHTML+=`<p class="upgrade-modal-founding-note">Beta rate: $7.99/mo for as long as you stay subscribed. ${l} day${l===1?"":"s"} remaining.</p>`),m.textContent=v,m.dataset.upgradeTier=h,g.textContent=y,f&&f.classList.toggle("hidden",!b);const _=document.getElementById("upgrade-modal-buy-session");if(_){const w=e.scenario==="cap_exhausted"||e.scenario==="recap_no_bank";_.classList.toggle("hidden",!w)}t.classList.remove("hidden")}function _hideUpgradeModal(){document.getElementById("upgrade-modal")?.classList.add("hidden")}document.getElementById("upgrade-modal-primary")?.addEventListener("click",async()=>{const e=document.getElementById("upgrade-modal-primary"),t=e?.dataset.upgradeTier;if(!t)return;e.disabled=!0;const n=e.textContent;e.textContent="Redirecting\u2026";try{const a=await api("POST","/api/billing/create-checkout-session",{tier:t,interval:"monthly"});if(a&&a.url){window.location.href=a.url;return}showToast("Could not start checkout. Please try again.")}catch(a){await _tfCheckoutError(a)}finally{e.disabled=!1,e.textContent=n}}),document.getElementById("upgrade-modal-buy-session")?.addEventListener("click",async()=>{const e=document.getElementById("upgrade-modal-buy-session");if(!e)return;e.disabled=!0;const t=e.textContent;e.textContent="Redirecting\u2026";try{const n={kind:"session_credit",campaignId:currentCampaign?.id||null,origin:document.getElementById("live-session-screen")?.classList.contains("hidden")===!1?"live":"chronicle",stashedAt:Date.now()};localStorage.setItem("tf_post_checkout_return",JSON.stringify(n))}catch{}try{const n=await api("POST","/api/billing/buy-session",{});if(n&&n.url){window.location.href=n.url;return}showToast("Could not start checkout. Please try again.")}catch(n){showToast("Checkout error: "+(n.message||"Unknown"))}finally{e.disabled=!1,e.textContent=t}}),document.getElementById("upgrade-modal-secondary")?.addEventListener("click",_hideUpgradeModal),document.querySelectorAll("[data-upgrade-modal-close]").forEach(e=>{e.addEventListener("click",_hideUpgradeModal)}),document.getElementById("upgrade-modal")?.addEventListener("click",e=>{e.target?.id==="upgrade-modal"&&_hideUpgradeModal()}),document.getElementById("upgrade-modal-pro-footnote")?.addEventListener("click",e=>{e.preventDefault(),_hideUpgradeModal(),_showPricingScreen()});function _applyThreadsTabVisibility(){const e=document.getElementById("tab-btn-threads");if(e){if(!_isGm()){e.classList.add("hidden"),typeof _btsTeardown=="function"&&_btsTeardown(),e.classList.contains("active")&&document.getElementById("tab-btn-sessions")?.click();return}e.classList.remove("hidden"),_applyTabIntroBanners(),typeof _btsTabDotRefresh=="function"&&_btsTabDotRefresh()}}const _INTRO_BANNERS={chronicle:{key:"tf_chronicle_intro_dismissed_v3",eyebrow:"START HERE",title:"Session 0 builds your party and Codex before you play.",body:"Hit <em>+ New Session</em> below and drop in your characters\u2019 backstories, your world, and any lore your players already know."},mychar:{key:"tf_mychar_intro_dismissed_v1",eyebrow:"MY CHARACTER",title:"This is your character\u2019s page.",body:"The fields with a \u270E are yours to shape; the rest grow from your sessions."},quests:{key:"tf_quests_intro_dismissed_v3",eyebrow:"QUESTS",title:"The ledger fills itself. Your job is to keep it honest.",body:"Run a session recap and Threadfall extracts your quests automatically, updating them as the story moves. Come back to confirm them, close out what\u2019s resolved, and tag personal arcs as <em>
3081Background Quests</em> so they don\u2019t crowd the main thread."},atlas:{key:"tf_atlas_intro_dismissed_v3",eyebrow:"ATLAS",title:"Upload a map. Watch it come alive.",body:()=>"Drop in any map image and Threadfall pins the locations it surfaces from your recaps, then traces <em>The Journey</em> as your party moves through them. Tap any two pins for <em>Directions</em> ("+(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart()?"the distance and the rough going":"days on the road, on foot or by ship")+") and add more maps to nest your world, region, and sites."}};function _introBannerHtml(e){const t=_INTRO_BANNERS[e];if(!t)return"";let n=!1;try{n=localStorage.getItem(t.key)==="1"}catch{}return`<section class="tab-intro-banner cool-paper-surface${n?" hidden":""}" data-tf-intro-key="${escHtml(t.key)}" aria-label="${escHtml(t.eyebrow)} tip"><button class="tab-intro-dismiss tf-intro-dismiss" title="Dismiss" aria-label="Dismiss intro">\u2715</button><div class="tab-intro-content"><span class="tab-intro-eyebrow">${escHtml(t.eyebrow)}</span><h3 class="tab-intro-title">${escHtml(t.title)}</h3><p class="tab-intro-body">${typeof t.body=="function"?t.body():t.body}</p></div></section>`}function _applyTabIntroBanners(){document.querySelectorAll(".tab-intro-banner[data-tf-intro-key]").forEach(e=>{const t=e.getAttribute("data-tf-intro-key");let n=!1;try{n=localStorage.getItem(t)==="1"}catch{}e.classList.toggle("hidden",n||e.dataset.tfSuppressed==="1")})}document.addEventListener("click",e=>{const t=e.target.closest?.(".tf-intro-dismiss");if(!t)return;const n=t.closest(".tab-intro-banner");if(!n)return;const a=n.getAttribute("data-tf-intro-key");if(a)try{localStorage.setItem(a,"1")}catch{}n.classList.add("hidden")}),document.addEventListener("DOMContentLoaded",_applyTabIntroBanners);const _SETUP_ROW_TARGET={invite:"manage-players-section",discord:"manage-discord-section",next:"manage-next-session-section",identity:"manage-campaign-section"},_SETUP_CARD_SEEN=new Set,_SETUP_CHEVRON='<svg class="tf-setup-chev" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5 10.5 8 6 12.5"/></svg>';function _setupCardKey(e){return"tf_setup_card_dismissed_"+e}function _setupRowKey(e,t){return"tf_setup_row_"+e+"_"+t}function _setupFlag(e){try{return localStorage.getItem(e)==="1"}catch{return!1}}function _setupCardState(){const e=currentCampaign;if(!e||!_isGm()||e.is_archived||(allSessions||[]).some(a=>a&&a.last_recap_at))return null;if(_entitiesLoadedCid!==e.id)return"pending";const t=(allEntities||[]).filter(a=>a&&a.type==="pc").length;if(t>0&&!_SETUP_CARD_SEEN.has(e.id))return null;_SETUP_CARD_SEEN.add(e.id);const n=Number(e.member_count)||0;return{cid:e.id,name:e.name||"This campaign",dismissed:_setupFlag(_setupCardKey(e.id)),pcCount:t,rows:[{key:"invite",label:"Invite your players",done:n>0,doneText:n===1?"1 player invited.":n+" players invited."},{key:"discord",label:"Link Discord",done:!!e.discord_linked,doneText:"Discord linked."},{key:"next",label:"Set your next session",done:!!e.next_session_at,doneText:"Next session is set."},{key:"identity",label:"Add a party name or cover art",done:!!(e.cover_image_url||e.party_name),doneText:e.party_name?"Party name set.":"Cover art added."}]}}function _setupCardHtml(e){if(!e||e==="pending"||e.dismissed)return"";const t=e.pcCount>0,n=t?'<p class="tf-setup-req-done">'+(e.pcCount===1?"1 character added.":escHtml(String(e.pcCount))+" characters added.")+"</p>":'<p class="tf-setup-req-body">Your PCs feed Party, Live, and every recap. Add them from the Party tab. <a class="tf-setup-learn" href="/help/getting-started" target="_blank" rel="noopener">Getting started guide</a></p>',a=t?"":'<button type="button" class="btn-new btn-sm tf-setup-add-pc">+ Add a character</button>',s=e.rows.filter(i=>i.done||!_setupFlag(_setupRowKey(e.cid,i.key))).map(i=>i.done?'<li class="tf-setup-row is-done"><span class="tf-setup-row-label">'+escHtml(i.label)+'</span><span class="tf-setup-row-fact">'+escHtml(i.doneText)+"</span></li>":'<li class="tf-setup-row"><button type="button" class="tf-setup-row-go" data-setup-go="'+escHtml(i.key)+'">'+escHtml(i.label)+_SETUP_CHEVRON+'</button><button type="button" class="tf-setup-row-skip" data-setup-skip="'+escHtml(i.key)+'" aria-label="Hide '+escHtml(i.label)+'">Not now</button></li>').join("");return'<section class="tab-intro-banner cool-paper-surface tf-setup-card" data-tf-intro-key="'+escHtml(_setupCardKey(e.cid))+'" aria-label="Campaign setup"><button class="tab-intro-dismiss tf-intro-dismiss" title="Dismiss" aria-label="Dismiss campaign setup">\u2715</button><div class="tab-intro-content"><span class="tab-intro-eyebrow">START HERE</span>
3081<h3 class="tab-intro-title">'+escHtml(e.name)+' is ready for its party.</h3><div class="tf-setup-req"><div class="tf-setup-req-text"><span class="tf-setup-req-label">Add your player characters</span>'+n+"</div>"+a+"</div>"+(s?'<p class="tf-setup-divider">The rest is optional. Nothing here will chase you.</p><ul class="tf-setup-rows">'+s+"</ul>":"")+"</div></section>"}function _setupCardSyncBanner(e,t){const n=_INTRO_BANNERS.chronicle.key,a=e.querySelector('.tab-intro-banner[data-tf-intro-key="'+n+'"]');a&&(t?a.dataset.tfSuppressed="1":delete a.dataset.tfSuppressed,a.classList.toggle("hidden",!!t||_setupFlag(n)))}function _setupCardRefresh(){const e=document.getElementById("tab-chronicle2"),t=e&&e.querySelector("#tf-setup-slot");if(!t)return;const n=_setupCardState();if(n==="pending"){_setupCardSyncBanner(e,!0);return}if(!n){t.innerHTML&&(t.innerHTML=""),_setupCardSyncBanner(e,!1);return}t.innerHTML=_setupCardHtml(n),_setupCardSyncBanner(e,!0)}async function _setupCardSyncFromServer(){const e=document.getElementById("tab-chronicle2");if(!e||!e.querySelector(".tf-setup-card"))return;const t=_setupCardState();if(!t||t==="pending"||t.dismissed||t.rows.every(a=>a.done))return;const n=currentCampaign&&currentCampaign.id;if(n)try{const a=await api("GET",`/api/campaigns/${n}`);if(_campaignStale(n)||!currentCampaign)return;for(const s of["member_count","discord_linked","next_session_at","cover_image_url","party_name"])currentCampaign[s]=a[s];_setupCardRefresh()}catch{}}function _setupCardGoto(e){const t=_SETUP_ROW_TARGET[e];t&&(document.getElementById("nav-manage-btn")?.click(),setTimeout(()=>document.getElementById(t)?.scrollIntoView({behavior:"smooth",block:"start"}),250))}document.addEventListener("click",e=>{if(e.target.closest?.(".tf-setup-card .tf-intro-dismiss")){_setupCardRefresh();return}if(e.target.closest?.(".tf-setup-add-pc")){openEntityModal(null,null,"pc",{batchMode:!0});return}const t=e.target.closest?.("[data-setup-go]");if(t){_setupCardGoto(t.getAttribute("data-setup-go"));return}const n=e.target.closest?.("[data-setup-skip]");if(n){const a=currentCampaign&&currentCampaign.id;if(a)try{localStorage.setItem(_setupRowKey(a,n.getAttribute("data-setup-skip")),"1")}catch{}_setupCardRefresh()}});function _tfFatesAiAllowed(){return _TF_THREAD_FULL_TIERS.has(_tfBillingTier||"free")}const _TF_AI_GATED_TITLE="AI feature: available on Standard and above",_TF_AI_GATED_SELECTORS=["#threads-detect-btn","#fates-suggestions-btn","#fates-scene-regenerate"];function _applyTfAiGating(e=document){const t=_tfFatesAiAllowed();for(const a of _TF_AI_GATED_SELECTORS){const s=e.querySelector(a);s&&(s.dataset.tfOrigTitle||(s.dataset.tfOrigTitle=s.title||""),t?(s.disabled=!1,s.title=s.dataset.tfOrigTitle):(s.disabled=!0,s.title=_TF_AI_GATED_TITLE))}e.querySelectorAll("[data-fate-generate-foreshadow]").forEach(a=>{a.dataset.tfOrigTitle||(a.dataset.tfOrigTitle=a.title||""),t?(a.disabled=!1,a.title=a.dataset.tfOrigTitle):(a.disabled=!0,a.title=_TF_AI_GATED_TITLE)});const n=e.querySelector("#fates-edit-generate-seeds");n&&(n.dataset.tfOrigTitle||(n.dataset.tfOrigTitle=n.title||""),t?(n.disabled=!1,n.title=n.dataset.tfOrigTitle):(n.disabled=!0,n.title=_TF_AI_GATED_TITLE))}const _tfHooksAutoDetected=new Set,_HOOK_SORT_LABELS={heat:"Heat",newest:"Newest",oldest:"Oldest",alpha:"A\u2013Z"},_HOOK_SORT_CYCLE=["heat","newest","oldest","alpha"],_HOOK_MODE_LABELS={active:"Active",dismissed:"Dismissed",all:"All"},_HOOK_MODE_CYCLE=["active","dismissed","all"];let _hooksSearch="",_hooksSort="heat",_hooksMode="active",_hooksTypeFilter="",_hooksAllThreads=[],_hooksResolvedThreads=[],_fatesWeaves=[],_fatesThreadsById={},_fatesNeighbors={},_fatesBlocklist=[],_fatesJunctionThreads=[],_fatesEntities=[],_fatesEntityById={},_fatesPossibleIdx=0,_fatesSynthesisCache={},_fatesSynthesisInflight={},_fatesHiddenEntitiesInWeave=new Set,_fatesAddedEntities=new Set,_fatesPickerSearch="",_fatesEntityRailExpanded=!1,_fatesEditMode=!1,_fatesEditSnapshot=null,_inlineSectionField=null,_fatesSelected=new Set,_hooksLastResponseMeta={total:0,preview:!1},_tfThreadsLastTotal=0,_btsSpec=null,_btsLoadedCid=null,_btsRailState={pinned:null,active:null},_btsPartySpec=null,_btsPartyCid=null,_btsRules=null,_btsRulesCid=null,_btsFactions=null,_btsFactionsCid=null,_btsRnParty=null,_btsRnPartyCid=null,_btsMapSecrets=null,_btsMapSecretsCid=null,_btsDotCid=null,_btsNoteDraftEid=null;const _btsPcOpen=new Set,_BTS_OFFER_MAX=5,_BTS_SECS=[["bts-sec-plan","Plan"],["bts-sec-waiting","Waiting"],["bts-sec-bench","Workbench"],["bts-sec-fates","Fates"]],_BTS_AREAS=[["party","Party","Notes, backstories and private quests for each character",["gm","pc"]],["honors","Honors Roll","Deeds the table will remember, and the moments nominated for them",["table"]],["quests","Hidden Quests","Unrevealed quests, and quests private to one character",["reveal","pc"]],["hidden","Codex Secrets","Hidden entries you can reveal, and n
3081otes that stay yours",["reveal","gm"]],["atlas","Map Secrets","Places, routes and encounter marks the table hasn't found yet",["reveal"]],["factions","Factions","Where each faction stands, and what it wants",["table","gm"]],["npcs","NPC Standing","Where each NPC stands with the party",["table"]],["rules","Table Rules","House rules, and lines and veils",["table"]]],_BTS_AUDIENCE={gm:'<span class="gm-surface-seal">GM Only</span>',pc:'<span class="gm-surface--staged bts-scope"><span class="gm-surface-seal">Private to PC</span></span>',reveal:'<span class="gm-surface-seal bts-seal--reveal">Stage for Reveal</span>',table:'<span class="gm-surface-seal bts-seal--table">Whole Table</span>'},_BTS_TYPE_LABEL={npc:"NPC",location:"Place",faction:"Faction",item:"Item",deity:"Deity",lore:"Lore",pc:"PC",companion:"Companion"},_BTS_ADD_TYPES=[["npc","NPC"],["location","Place"],["faction","Faction"],["item","Item"],["lore","Lore"],["deity","Deity"]];function _btsStage(){return document.querySelector("#tab-threads > .fates-stage")}function _btsEditorOpen(e){if(!e)return!1;if(e.querySelector(".bts-plan-ta, .gm-notes-ta, .p2-honors-editor, .p2-hn-form"))return!0;const t=document.activeElement;return!!(t&&e.contains(t)&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)&&(t.value||"").trim())}function _btsOpenKey(e){return`tf_bts_open_${e}`}function _btsOpenRead(e){try{const t=JSON.parse(localStorage.getItem(_btsOpenKey(e))||"[]");return Array.isArray(t)?t.filter(n=>typeof n=="string"):[]}catch{return[]}}function _btsOpenWrite(e,t){try{localStorage.setItem(_btsOpenKey(e),JSON.stringify(t.filter(n=>/^[a-z]{1,12}$/.test(n))))}catch{}}function _btsAct(e,t){const n=currentCampaign&&currentCampaign.id;return!n||!_isGm()?Promise.resolve():api("POST",`/api/campaigns/${n}/behind-the-screen/event`,t?{kind:e,key:t}:{kind:e}).catch(()=>{})}function _btsTeardown(){_btsTabDot(!1);const e=_btsStage();if(!e)return;e.querySelector("#bts-rail")?.classList.add("hidden"),e.querySelector("#bts-root")?.remove();const t=document.getElementById("fates-masthead");t&&(t.innerHTML="",t.dataset.ready=""),_btsSpec=null,_btsLoadedCid=null,_btsFactions=null,_btsFactionsCid=null,_btsRnParty=null,_btsRnPartyCid=null,_btsDotCid=null,_btsMapSecrets=null,_btsMapSecretsCid=null}function _btsEntity(e){const t=(allEntities||[]).find(n=>n.id===e);return t&&!_hiddenFromViewer(t)?t:null}function _btsEntityImg(e){try{const t=typeof _entityImageUrl=="function"?_entityImageUrl(e):e&&e.image_url;return t?typeof thumbUrl=="function"&&thumbUrl(t)||t:""}catch{return e&&e.image_url||""}}function _btsExcerpt(e,t){const n=String(e||"").replace(/\s+/g," ").trim();return n.length>t?n.slice(0,t-1).trimEnd()+"\u2026":n}function _btsRailCfg(){const e=_btsStage(),t=e&&e.querySelector("#bts-rail");return{rail:t,panel:e,scroller:e,state:_btsRailState,sections:()=>e&&t&&!t.classList.contains("hidden")?_BTS_SECS.map(([n])=>e.querySelector("#"+n)).filter(Boolean):[],bandTop:()=>t.getBoundingClientRect().bottom,scrollOffset:()=>t.getBoundingClientRect().height}}function _btsTabDot(e){const t=document.getElementById("tab-btn-threads");t&&t.classList.toggle("has-waiting",!!e)}async function _btsTabDotRefresh(){const e=currentCampaign&&currentCampaign.id;if(!(!e||!_isGm()||_btsDotCid===e)){_btsDotCid=e;try{const t=await api("GET",`/api/campaigns/${e}/behind-the-screen`);if(_campaignStale(e)||!_isGm())return;_btsTabDot(_btsWaits(t)>0)}catch{_btsDotCid=null}}}function _btsRow(e){const t=e.tone==="reveal"?" bts-seal--reveal":"",n=(e.tag?`<span class="gm-surface-seal bts-row-tag${t}">${escHtml(e.tag)}</span>`:"")+(e.tag2?`<span class="gm-surface-seal bts-row-tag">${escHtml(e.tag2)}</span>`:""),a=e.sub?`<p class="bts-row-sub">${escHtml(e.sub)}</p>`:"",s=(e.actions||[]).map(r=>`<button type="button" class="${r.cls||"btn-new btn-xs"}" data-bts-act="${r.act}"${r.title?` title="${escHtml(r.title)}"`:""}>${escHtml(r.label)}</button>`).join(""),i=e.offer?`<button type="button" class="tab-intro-dismiss bts-row-dismiss" data-bts-act="dismiss" aria-label="Dismiss this suggestion" title="${escHtml(e.dismissT
3081itle||"Hide this.")}">\u2715</button>`:"";return`<div class="${`tf-card tf-card--xs bts-row${e.offer?" bts-row--offer":""}${e.tone==="violet"?" gm-surface--staged bts-row--violet":""}${e.tone==="reveal"?" bts-row--reveal":""}`}"${e.key?` data-bts-key="${escHtml(e.key)}"`:""}${e.data||""}><div class="bts-row-head">${e.img?`<img class="bts-pc-face bts-fac-face" src="${escHtml(e.img)}" alt="" loading="lazy">`:""}${n}<span class="bts-row-name">${escHtml(e.name)}</span>`+(s&&!e.actsBelow?`<span class="bts-row-acts">${s}</span>`:"")+(e.extra||"")+i+"</div>"+a+(s&&e.actsBelow?`<div class="bts-row-acts bts-row-acts--below">${s}</div>`:"")+(e.below||"")+"</div>"}function _btsSecHead(e,t){return`<div class="bts-sec-hdr"><span class="form-section-label">${escHtml(e)}</span></div>`+(t?`<p class="form-helper-text bts-sec-help">${escHtml(t)}</p>`:"")}function _btsSharedWith(e){return`Shared only with ${e&&e.name||"this character"}`}function _btsNoteBox(e){const t=(e.gm_notes||"").trim();return`<div class="gm-surface-body gm-bs-prose bts-note-display" data-bts-host="1" data-eid="${e.id}" tabindex="0" role="textbox" aria-multiline="true" aria-label="Your note on ${escHtml(e.name)}: click to edit" title="Click to edit, saves when you click away"><span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>`+(t?escHtml(t).replace(/\n/g,"<br>"):'<span class="gm-notes-prompt">Click to write a note only you can see&hellip;</span>')+"</div>"}function _btsWhen(e){const t=e&&e.next_session_at,n=t?new Date(t):null;if(!n||isNaN(n.getTime()))return{heading:"Next Session",isTonight:!1,line:""};const a=new Date;return n.toDateString()===a.toDateString()?{heading:"Tonight",isTonight:!0,line:""}:n.getTime()<a.getTime()?{heading:"Next Session",isTonight:!1,line:""}:{heading:`Next Session \xB7 ${n.toLocaleDateString(void 0,{weekday:"long",month:"long",day:"numeric"})}`,isTonight:!1,line:""}}function _btsPlanHtml(e,t){const n=_btsWhen(e),a=!!(e.next_session_at&&!isNaN(new Date(e.next_session_at).getTime())&&new Date(e.next_session_at).getTime()>=new Date().setHours(0,0,0,0)),s=e.plan&&e.plan.text||"",i=n.isTonight?"Tonight, I want to\u2026":"Next session, I want to\u2026";return'<section class="bts-sec" id="bts-sec-plan">'+_btsSecHead(n.heading,"")+(a?"":'<p class="bts-when bts-when--none">No date set. <button type="button" class="bts-link" data-bts-act="schedule">Set a date</button></p>')+`<div class="gm-surface bts-plan" role="group" aria-label="Your plan \xB7 GM only"><div class="gm-surface-head"><span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.screen}</span><span class="gm-surface-label">Your plan</span><span class="gm-surface-seal">GM&nbsp;only</span></div><div class="gm-surface-body cx2-prose gm-bs-prose bts-plan-display" data-bts-starter="${escHtml(i)}" tabindex="0" role="textbox" aria-multiline="true" aria-label="Your plan: click to edit" title="Click to edit, saves when you click away"><span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>`+(s?escHtml(s).replace(/\n/g,"<br>"):`<span class="gm-notes-prompt">${escHtml(i)}</span>`)+"</div></div>"+_btsOffersHtml(e,t)+"</section>"}function _btsOffers(e,t){const n=new Set(e.dismissed||[]),a=[];return(e.stage||[]).forEach(s=>{const i=_btsEntity(s.entity_id);if(!i||(i.gm_notes||"").trim())return;const o=`note:${i.id}`;n.has(o)||a.push({key:o,entityId:i.id,name:`${i.name} was on stage last session.`,sub:"Worth a note while it's fresh?",dismissT
3081itle:`Hide this. It comes back if ${i.name} is on stage again.`})}),_q2LoadedCid===t&&(allQuests||[]).filter(s=>s.status==="active"&&!s.is_background&&!s.pending_reveal).forEach(s=>{if(_q2Leads(s).some(o=>(o.status||"open")==="open"))return;const i=`quest:${s.id}`;n.has(i)||a.push({key:i,questId:s.id,name:`${s.title} has no next step planned.`,sub:"Add one?",dismissTitle:"Hide this."})}),a.slice(0,_BTS_OFFER_MAX)}function _btsMoveLabel(e){if(e.kind==="agenda")return e.delta<0?"Agenda moves a step back":"Agenda moves a step forward";const t=a=>a==null?"Unknown":_P2_RN_LABELS[String(a)]||"Unknown",n=Math.max(-3,Math.min(3,(e.standing==null?0:e.standing)+(e.delta||0)));return`${t(e.standing)} \u2192 ${t(n)}`}function _btsProposalRow(e){const t=_btsEntity(e.faction_entity_id);return _btsRow({img:t?_btsEntityImg(t):"",name:e.name,sub:e.note||"",extra:`<span class="bts-prop-move">${escHtml(_btsMoveLabel(e))}</span>`,data:` data-bts-pid="${e.id}"`,actions:[{act:"prop-confirm",label:"Apply",title:e.kind==="agenda"?"Apply this tick to the agenda":"Apply this change in standing"},{act:"prop-dismiss",label:"Dismiss",cls:"btn-ghost btn-xs",title:"Not this. It does not come back."}]})}async function _btsLegendAct(e,t,n,a,s){const i=_btsSpec&&_btsSpec.legend;if(i){t.disabled=!0;try{let o;if(e==="legend-undo")o=await api("DELETE",`/api/campaigns/${s}/behind-the-screen/legend?session_id=${i.session_id}`);else{const c={session_id:i.session_id,moment_id:null};if(e==="legend-crown"&&n){c.moment_id=+n.dataset.btsMid;const l=n.querySelector(".bts-litm-badge");l&&(c.badge=l.value)}o=await api("POST",`/api/campaigns/${s}/behind-the-screen/legend`,c)}if(_campaignStale(s)||!_btsSpec)return;_btsSpec.legend=o;const r=document.getElementById("bts-root");r&&!_btsEditorOpen(r)&&_btsPaint(r,s),_btsTabDot(_btsWaits(_btsSpec)>0)}catch(o){t.disabled=!1,showToast((e==="legend-undo"?"Undo failed: ":"Could not save: ")+(o&&o.message||"try again"))}}}function _tfCanShareLegend(e){return!!e&&!_isReadOnlyRole()}async function _tfLegendShareMenu(e,t,n){document.querySelectorAll(".tf-share-menu").forEach(u=>u.remove());const a=currentCampaign&&currentCampaign.id;if(!e||!e.id||!a)return;if(e.status!=="done"||!e.png_url){try{const u=await api("GET",`/api/campaigns/${a}/quote-cards/${e.id}`);u&&(e.status=u.status,e.png_url=u.png_url,e.share_token=u.share_token||e.share_token)}catch{}if(e.status!=="done"||!e.png_url){showToast("The card is still being drawn. Try again in a moment.");return}}const s=e.spec||{},i=`thread-fall.com${e.share_token?"/?qc="+e.share_token:""}`,o=`Legend in the Making: ${s.character_name||""}, ${s.session_title||""}. ${s.event_text||""} \xB7 made live at ${i}`,r=document.createElement("div");r.className="tf-share-menu",r.innerHTML='<button type="button" class="tf-share-opt tf-share-opt--primary" data-sh="file">Share</button><button type="button" class="tf-share-opt" data-sh="copy-img">Copy image</button><button type="button" class="tf-share-opt" data-sh="download">Download</button>'+(n?`<div class="tf-share-sep" aria-hidden="true"></div><button type="button" class="tf-share-opt" data-sh="table">Post to your table's Discord</button>`:""),document.body.appendChild(r);const c=t.getBoundingClientRect();r.style.top=`${Math.max(8,Math.min(c.bottom+6,window.innerHeight-r.offsetHeight-8))}px`,r.style.left=`${Math.max(8,Math.min(c.left,window.innerWidth-r.offsetWidth-8))}px`;const l=()=>r.remove(),d=u=>r.querySelector(`[data-sh="${u}"]`);d("file").onclick=async()=>{l();let u=!1;try{u=!!(navigator.canShare&&navigator.canShare({files:[new File([new Blob],"c.png",{type:"image/png"})]}))}catch{}if(u&&_tfNativeSharePreferred())try{const p=await fetch(e.png_url).then(m=>m.blob());await navigator.share({files:[new File([p],"threadfall-legend.png",{type:"image/png"})],text:o})}catch{}else _tfCopyText(`${o}
3082${e.png_url}`,"Post copied. Paste into Discord. Text and card image included.")},d("copy-img").onclick=async()=>{l();try{const u=await fetch(e.png_url).then(p=>p.blob());await navigator.clipboard.write([new ClipboardItem({[u.type||"image/png"]:u})]),showToast("Image copied, paste it into any chat or Discord.")}catch{_tfCopyText(e.png_url,"Image link copied, paste it anywhere.")}},d("download").onclick=()=>{l(),_qcDownload(e.png_url)},d("table")&&(d("table").onclick=async()=>
3082{l();try{await api("POST",`/api/campaigns/${a}/legend-cards/${e.id}/post-to-table`,{}),showToast("Posted to your table's Discord.")}catch(u){showToast("Could not post: "+(u&&u.message||"try again"))}}),setTimeout(()=>document.addEventListener("click",function u(p){r.contains(p.target)||(l(),document.removeEventListener("click",u))}),0)}function _btsLegendHtml(e){const t=e&&e.legend;if(!t)return"";const n=l=>`<div class="bts-rows">${l.join("")}</div>`,a=l=>{const d=_btsEntity(l);return d?d.name:"A character"},s=l=>{const d=_btsEntity(l);return d?_btsEntityImg(d):""},i="The one moment from last session worth a card.";if(t.crowned){const l=t.crowned,d=l.card,u=d&&d.spec?`<div class="bts-litm-card">${_qcRenderInline(d.spec,.42)}</div>`:"",p=_btsRow({img:s(l.entity_id),tag:l.badge_name,name:a(l.entity_id),sub:_btsExcerpt(l.beat,200),actsBelow:!0,below:u,actions:[{act:"legend-share",label:"Share",title:"Share the card: Discord, copy, download"},{act:"legend-undo",label:"Undo",cls:"btn-ghost btn-xs",title:"Take the crown back"}]});return _btsBlock("table","Legend in the Making","",{rule:"Crowned for last session. It shows on the chapter.",below:n([p])})}if(t.decided)return _btsBlock("table","Legend in the Making","",{rule:"No legend for last session.",below:'<p class="bts-wait-foot"><button type="button" class="bts-link" data-bts-act="legend-undo">Undo</button></p>'});const o=t.candidates||[];if(!o.length)return"";const r=l=>(t.badges||[]).map(d=>`<option value="${escHtml(d.key)}"${d.key===l?" selected":""}>${escHtml(d.name)}</option>`).join(""),c=o.map(l=>_btsRow({img:s(l.entity_id),tag:l.badge_name,name:a(l.entity_id),sub:l.beat,data:` data-bts-mid="${l.moment_id}"`,actsBelow:!0,extra:l.kind==="manual"?`<select class="bts-litm-badge" aria-label="Which badge">${r(l.badge)}</select>`:"",actions:[{act:"legend-crown",label:"Crown",title:"Make this last session's Legend in the Making"}]}));return _btsBlock("table","Legend in the Making","",{attrs:' data-bts-wait="1"',rule:i,below:n(c)+'<p class="bts-wait-foot"><button type="button" class="bts-link" data-bts-act="legend-none">None this session</button></p>'})}function _btsWaitingHtml(e,t){const n=u=>`<div class="bts-rows">${u.join("")}</div>`,a=e.proposals||[],s=a.filter(u=>u.kind!=="agenda").map(_btsProposalRow),i=a.filter(u=>u.kind==="agenda").map(_btsProposalRow),o=(e.rulings||[]).map(u=>_btsRow({name:u.ruling,sub:u.quote?`\u201C${_btsExcerpt(u.quote,160)}\u201D`:"",data:` data-bts-rid="${u.id}"`,actsBelow:!0,actions:[{act:"ruling-keep",label:"Keep",title:"Keep this ruling: it is written into your house rules"},{act:"ruling-discard",label:"Discard",cls:"btn-ghost btn-xs",title:"Not a ruling"}]})),r=' data-bts-wait="1"',c=_btsLegendHtml(e),l=c+(o.length?_btsBlock("table","Rulings","",{attrs:r,rule:"Keep writes it into your house rules, where the whole table can read it.",below:n(o)}):"")+(s.length?_btsBlock("table","Standing","",{attrs:r,rule:"Apply moves where the company stands with them. The table sees standing.",below:n(s)}):"")+(i.length?_btsBlock("gm","Agendas","",{attrs:r,rule:"Apply moves the agenda you keep for them. Players never see it.",below:n(i)}):""),d=e.last_session?"Nothing waiting on you right now.":"Nothing yet. Once you run a session, what the write-up proposes will wait here for your call.";return'<section class="bts-sec" id="bts-sec-waiting">'+_btsSecHead("Waiting on You","What the write-ups proposed. Nothing moves until you say so.")+`<div class="bts-wait">${l}</div><p class="placeholder placeholder--inline bts-wait-none${o.length||a.length||c?" hidden":""}">${d}</p>`+(e.last_session?'<p class="bts-wait-foot"><button type="button" class="bts-link" data-bts-act="open-updates">See what the last write-up changed</button></p>':"")+"</section>"}function _btsOffersHtml(e,t){const n=_btsOffers(e,t).map(a=>{const s=a.entityId?_btsEntity(a.entityId):null;return _btsRow({offer:!0,actsBelow:!0,img:s?_btsEntityImg(s):"",key:a.key,name:a.name,sub:a.sub,dismissT
3082itle:a.dismissTitle,data:a.entityId?` data-bts-eid="${a.entityId}"`:` data-bts-qid="${a.questId}"`,actions:[{act:a.entityId?"add-note":"open-quest",label:a.entityId?"Add a note":"Open"}],below:s?`<div class="bts-row-below hidden">${_btsNoteBox(s)}</div>`:""})});return n.length?_btsBlock("gm","From Last Session","",{attrs:' data-bts-wait="1" data-bts-offers="1"',below:`<div class="bts-rows">${n.join("")}</div>`}):""}function _btsWaitingPrune(){const e=document.getElementById("bts-sec-waiting");if(!e||!_btsSpec)return;(e.closest(".bts-band")||e).querySelectorAll(".bts-block[data-bts-wait]").forEach(o=>{o.querySelector(".bts-row")||o.remove()});const n=!!e.querySelector(".bts-block[data-bts-wait]:not([hidden])"),a=e.querySelector(".bts-wait-none");a&&a.classList.toggle("hidden",n);const s=!!e.querySelector(".bts-block[data-bts-wait]:not([hidden]):not([data-bts-offers])");_btsTabDot(s);const i=document.getElementById("bts-rail");i&&_tfRailDot(i,"bts-sec-waiting",s?"watch":null,s?"Something is waiting on a decision":"")}function _btsWaits(e){const t=e&&e.legend,n=t&&!t.decided&&(t.candidates||[]).length?1:0;return(e&&e.proposals||[]).length+(e&&e.rulings||[]).length+n}function _btsTile(e,t,n,a,s){return`<button type="button" class="tf-card tf-card--sm bts-tile${a?" is-selected":""}" data-bts-tile="${e}" aria-expanded="${a?"true":"false"}"><span class="bts-tile-top"><span class="bts-tile-title">${escHtml(t)}</span><span class="manage-section-chevron" aria-hidden="true">\u203A</span></span><span class="bts-tile-hint">${escHtml(n)}</span><span class="bts-tile-aud"><span class="bts-tile-aud-lbl">Visibility controls</span>${(s||[]).map(i=>_BTS_AUDIENCE[i]||"").join("")}</span></button>`+(a?`<div class="bts-panel" data-bts-area="${e}"><div class="bts-panel-hdr"><span class="bts-tile-title">${escHtml(t)}</span></div><div class="bts-panel-body">${_btsAreaHtml(e)}</div></div>`:"")}function _btsAreaHtml(e){const t=currentCampaign&&currentCampaign.id;return e==="party"?_btsPartyHtml(t):e==="honors"?_btsHonorsHtml():e==="npcs"?_btsNpcsHtml():e==="hidden"?_btsHiddenHtml():e==="quests"?_btsQuestsHtml(t):e==="atlas"?_btsMapSecretsHtml():e==="factions"?_btsFactionsHtml():e==="rules"?_btsRulesHtml():""}function _btsAreaRefresh(e,t){const n=document.querySelector(`#bts-root .bts-panel[data-bts-area="${e}"] .bts-panel-body`);n&&(n.querySelector(".gm-notes-ta")||[...n.querySelectorAll(".bts-story-ta")].some(a=>a.value.trim())||(n.innerHTML=_btsAreaHtml(e),t&&n.querySelector(t)?.focus()))}function _btsQuickAdd(e,t,n){return n=n||{},`<form class="bts-add${n.tone==="violet"?" gm-surface--staged bts-scope-flat bts-add--violet":""}" data-bts-add="${e}"${n.data||""} autocomplete="off"><input type="text" class="bts-add-name" maxlength="${n.max||120}" placeholder="${escHtml(t)}" aria-label="${escHtml(t)}"${n.list?` list="${n.list}"`:""}>`+(n.withType?`<select class="bts-add-type" aria-label="What it is">${_BTS_ADD_TYPES.map(([s,i])=>`<option value="${s}">${i}</option>`).join("")}</select>`:"")+(n.pick?`<select class="bts-add-pick" aria-label="${escHtml(n.pick.label)}"><option value="">${escHtml(n.pick.label)}</option>${n.pick.options.map(([s,i])=>`<option value="${s}">${escHtml(i)}</option>`).join("")}</select>`:"")+`<button type="submit" class="btn-new btn-xs" disabled>${escHtml(n.label||"Add")}</button></form>`}function _btsAddReady(e){const t=(e.querySelector(".bts-add-name")?.value||"").trim(),n=e.querySelector(".bts-add-pick"),a=e.querySelector('button[type="submit"]');if(a){if(e.dataset.btsHide){a.disabled=!(n&&n.value);return}a.disabled=!t||!!(n&&!n.value)}}function _btsBlock(e,t,n,a){a=a||{};const s=a.pill?e==="pc"?`<span class="gm-surface--staged bts-scope"><span class="gm-surface-seal">${escHtml(a.pill)}</span></span>`:`<span class="gm-surface-seal">${escHtml(a.pill)}</span>`:_BTS_AUDIENCE[e]||"";return`<div class="bts-block bts-block--${e}${e==="pc"?" gm-surface--staged bts-scope-flat":""}"${a.attrs||""}><div class="bts-block-act"><div class="bts-block-head"><span class="bts-block-lbl">${escHtml(t)}</span>${a.headExtra||""}<span class="bts-block-pill">${s}</span></div>`+(a.rule?`<p class="bts-block-rule">${escHtml(a.rule)}</p>`:"")+n+"</div>"+(a.below?`<div class="bts-block-below">${a.below}</div>`:"")+"</div>"}function _btsPartyCardHtml(){const e=_btsPartySpec;if(!e)return'<p class="placeholder placeholder--inline">Loading the party card\u2026</p>';const t=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart(),n='<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-party">Open the full assessment on Party \u2192</button></p>';if(t){const c=e.dh_stress;return!c||!c.rows||!c.rows.length?`<p class="placeholder placeholder--inline">Add character sheets to see the party's rest cadence.</p>`+n:`<p class="bts-card-lead">About how many scenes each character has before their Stress runs out and they become Vulnerable. A rest resets it.</p><div class="p2-d6-dcgrid">${c.rows.map(l=>`<span class="p2-d6-dc"><b>${escHtml(l.name)}
3082</b> ~${escHtml(String(l.scenes))}</span>`).join("")}</div>`+n}const a={concentration:"Conc",con:"CON",dex:"DEX",int:"INT",wis:"WIS",cha:"CHA",str:"STR"},s=Array.isArray(e.save_dcs)?e.save_dcs.filter(c=>c&&c.tip_dc!=null):[],i=s.length?s:(e.dice&&e.dice.d6||[]).filter(c=>c&&c.tip_dc!=null);if(!i.length)return'<p class="placeholder placeholder--inline">Add PC sheet data (a sheet import or stats) to compute these.</p>'+n;const o=e.dice?e.dice.with_data:null,r=e.dice?e.dice.active_total:null;return`<p class="bts-card-lead">Set a save at this DC, and about half the party will make it, the rest will not.</p><div class="p2-d6-dcgrid">${i.map(c=>`<span class="p2-d6-dc"><b>${a[c.key]||escHtml(String(c.key))}</b> ${escHtml(String(c.tip_dc))}+</span>`).join("")}</div>`+(o!=null&&r!=null&&o<r?`<p class="form-helper-text">Based on ${o} of ${r} PCs with sheet data.</p>`:"")+n}function _btsQuestRow(e){const t=e.pc_entity_id?(allEntities||[]).find(a=>a.id===e.pc_entity_id):null,n=!!e.pending_reveal;return _btsRow({tone:n?"reveal":"violet",tag:n?"Staged":`Private \xB7 ${_btsSharedWith(t)}`,tag2:n&&t?`For ${t.name}`:"",name:e.title,sub:_btsExcerpt(e.gm_notes||e.description||"",140),data:` data-bts-qid="${e.id}"`,actions:[{act:"open-quest",label:"Open"}],extra:n?'<span class="bts-reveal"><button type="button" class="btn-new btn-xs" data-bts-act="reveal-quest">Reveal</button></span>':""})}function _btsPartyHtml(e){const n=((typeof _splitPcsByLifecycle=="function"?_splitPcsByLifecycle().active:(allEntities||[]).filter(a=>a.type==="pc"))||[]).filter(a=>!_hiddenFromViewer(a)).map(a=>{const s=_btsSharedWith(a),i=[],o=!!(a.owner_private_notes||"").trim();let r=0;_q2LoadedCid===e&&(allQuests||[]).filter(h=>h.pc_entity_id===a.id&&h.status==="active"&&h.is_private&&!h.pending_reveal).forEach(h=>{r++,i.push(_btsQuestRow(h))});const c=!!(a.gm_notes||"").trim(),l=_btsPcOpen.has(a.id),d=(c?'<span class="gm-surface-seal">Note</span>':"")+(o?'<span class="gm-surface--staged bts-scope"><span class="gm-surface-seal">Backstory</span></span>':"")+(r?'<span class="gm-surface--staged bts-scope"><span class="gm-surface-seal">Private quest</span></span>':""),u=o?`<div class="bts-rows">${_btsRow({tone:"violet",name:`${a.name} has a backstory.`,data:` data-bts-eid="${a.id}"`,actions:[{act:"open-entity",label:"Open"}]})}</div>`:"",p=o?"":`<div class="bts-story" data-bts-story="${a.id}"><textarea class="bts-story-ta" maxlength="8000" rows="4" placeholder="Where ${escHtml(a.name)} comes from\u2026" aria-label="Backstory for ${escHtml(a.name)}"></textarea><div class="bts-story-acts"><button type="button" class="btn-new btn-xs" data-bts-act="story-save" data-bts-eid="${a.id}" disabled>Save backstory</button></div></div>`,m=i.length?`<div class="bts-rows">${i.join("")}</div>`:"",f=l?_btsBlock("gm","Your note",_btsNoteBox(a))+_btsBlock("pc","Backstory",p,{pill:s,below:u})+_btsBlock("pc","Private quests",_btsQuickAdd("pcquest",`A quest only ${a.name} knows about`,{tone:"violet",label:"Add private quest",data:` data-bts-eid="${a.id}"`}),{pill:s,below:m})+`<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-entity-plain" data-bts-eid="${a.id}">Open ${escHtml(a.name)} \u2192</button></p>`:"",g=a.image_url?`<img class="bts-pc-face" src="${escHtml(a.image_url)}" alt="" loading="lazy">`:`<span class="bts-pc-face bts-pc-face--none" aria-hidden="true">${escHtml(String(a.name||"?").charAt(0))}</span>`;return`<div class="tf-card tf-card--sm bts-pc${l?" is-open is-selected":""}" data-bts-pc="${a.id}"><button type="button" class="bts-pc-row" aria-expanded="${l?"true":"false"}">`+g+`<span class="bts-pc-id"><span class="bts-row-name">${escHtml(a.name)}</span><span class="bts-pc-marks">${d}</span></span><span class="manage-section-chevron" aria-hidden="true">\u203A</span></button><div class="bts-pc-body${l?"":" hidden"}">${f}</div></div>`}).join("");return _btsBlock("gm","Party card",`<div class="bts-card" id="bts-party-card">${_btsPartyCardHtml()}</div>`)+(n?`<div class="bts-pcs">${n}</div>`:'<p class="placeholder placeholder--inline">No characters in the party yet.</p>')}function _btsHiddenHtml(){const e=currentCampaign&&currentCampaign.id,t=typeof _discordRevealReady=="function"&&_discordRevealReady(),n=(allEntities||[]).filter(s=>s.pending_reveal&&!_hiddenFromViewer(s)).sort((s,i)=>i.id-s.id).map(s=>_btsRow({tone:"reveal",img:_btsEntityImg(s),tag:_BTS_TYPE_LABEL[s.type]||"Entry",tag2:(s.gm_notes||"").trim()?"Has a note":"",name:s.name,sub:_btsExcerpt(s.gm_notes||s.snippet||"",140),data:` data-bts-eid="${s.id}"`,actions:[{act:"open-entity",label:"Open"}],extra:`<span class="bts-reveal">${_tfRevealCastHtml(s,t)}<button type="button" class="btn-new btn-xs" data-bts-act="reveal-entity">Reveal</button></span>`})),a=_btsNotesHtml(e);return _btsBlock("reveal","To Reveal",_btsQuickAdd("entity","Name someone or something they have not met yet",{withType:!0,label:"Add hidden entry & stage for reveal"}),{rule:"Hidden now. Press Reveal when you're ready for players to see it.",below:n.length?`<div class="bts-rows">${n.join("")}</div>`:""})+_btsBlock("gm","Never Shown",a.add,{rule:"Players never see this. There is no Reveal button, ever.",below:a.below})}function _btsStepRow(e,t){const n=_q2LeadVis(t),a=e.pc_entity_id?(allEntities||[]).find(s=>s.id===e.pc_entity_id):null;return _btsRow({tone:n==="gm"?"reveal":"violet",tag:n==="gm"?"Staged":`Private \xB7 ${_btsSharedWith(a)}`,name:t.text||"A step",sub:`Step on ${e.title}`,data:` data-bts-qid="${e.id}" data-bts-lid="${escHtml(String(t.id||""))}"`,actions:[{act:"open-quest",label:"Open"}],extra:n==="gm"&&t.id?'<span class="bts-reveal"><button type="button" class="btn-new btn-xs" data-bts-act="reveal-step">Reveal</button></span>':""})}function _btsQuestsHtml(e){if(_q2LoadedCid!==e)return'<p class="placeholder placeholder--inline">Loading quests\u2026</p>';const t=(allQuests||[]).filter(u=>u.status==="active"),n=u=>{const p=u.pc_entity_id?(allEntities||[]).find(m=>m.id===u.pc_entity_id):null;return p?String(p.name):""},a=(u,p)=>String(u.title).localeCompare(String(p.title)),s=t.filter(u=>u.pending_reveal).sort(a).map(_btsQuestRow),i=t.filter(u=>u.is_private&&!u.pending_reveal).sort((u,p)=>n(u).localeCompare(n(p))||a(u,p)).map(_btsQuestRow),o=t.filter(u=>!u.pending_reveal&&!u.is_private),r=[];
3082o.slice().sort(a).forEach(u=>_q2Leads(u).forEach(p=>{(p.status||"open")==="open"&&_q2LeadVis(p)!=="all"&&r.push(_btsStepRow(u,p))}));const c=((typeof _splitPcsByLifecycle=="function"?_splitPcsByLifecycle().active:[])||[]).filter(u=>!_hiddenFromViewer(u)).map(u=>[u.id,u.name]),l=o.filter(u=>!u.is_background).sort(a).map(u=>[u.id,u.title]),d=u=>u.length?`<div class="bts-rows">${u.join("")}</div>`:"";return _btsBlock("reveal","Quests to reveal",_btsQuickAdd("quest","Name a quest they do not know about yet",{label:"Add hidden quest"}),{rule:"Hidden now. Press Reveal when you're ready for the table to see it.",below:d(s)})+_btsBlock("pc","Quests private to a character",_btsQuickAdd("privquest","Name a quest only one character knows about",{tone:"violet",label:"Add private quest",pick:{label:"Whose quest",options:c}}),{rule:"That character, and whoever plays them, can see these now. The rest of the table cannot.",below:d(i)})+_btsBlock("reveal","Hidden steps",_btsQuickAdd("step","The next step they do not know about yet",{label:"Add hidden step",max:300,pick:{label:"On which quest",options:l}}),{rule:"Planned steps in an existing public quest that the table can't see yet, waiting to be revealed.",below:d(r)})}function _btsHonorsHtml(){const e=_btsPartySpec;if(!e)return'<p class="placeholder placeholder--inline">Loading the Honors Roll\u2026</p>';const t=e.annals&&e.annals.deeds||[],n=t.length?`<div class="p2-deeds">${t.map(_p2DeedHtml).join("")}</div>`:`<p class="placeholder placeholder--inline">No honors recorded yet. Set down the company's legendary deeds.</p>`;return _btsBlock("table","Deeds",`<div><button type="button" class="btn-new btn-xs" data-bts-act="honors-edit">${t.length?"Add or edit deeds":"Add a deed"}</button></div>`,{rule:"The whole table reads these on the Honors Roll, on Party.",below:`<div data-bts-honors>${n}</div>`})+_btsBlock("table","Nominated","",{rule:"Moments a player nominated, or the write-up flagged. Enshrine one to put it on the roll.",below:'<div class="p2-hon-noms" data-p2-hon-noms><p class="placeholder placeholder--inline">Reading nominations\u2026</p></div>'})+'<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-party">Open the Honors Roll on Party \u2192</button></p>'}function _btsHonorNpcs(){return(_btsRnParty&&_btsRnParty.npc_members||[]).filter(e=>e.status==="confirmed"&&!e.is_dead).map(e=>({id:e.entity_id,name:e.name}))}function _btsHonorsWire(e){const t=document.querySelector('#bts-root .bts-panel[data-bts-area="honors"] .bts-panel-body');!t||!_btsPartySpec||_p2LoadHonorNoms(t,{after:()=>_btsHonorsReload(e,!0),empty:"Nothing nominated right now."})}async function _btsHonorsReload(e,t){if(_campaignStale(e))return;const n=document.querySelector('#bts-root .bts-panel[data-bts-area="honors"] .bts-panel-body');n&&n.querySelectorAll(".p2-honors-editor, .p2-hn-form").forEach(a=>a.remove()),t&&(_btsPartySpec=null,_btsPartyCid=null,typeof _party2LoadedCid<"u"&&_party2LoadedCid===e&&(_party2LoadedCid=null)),await _btsAreaEnsure("honors",e)}const _btsRestOpen=new Set;function _btsRecentSplit(e,t){const n=typeof _codex2LatestSession=="function"?_codex2LatestSession():null,a=(o,r)=>sortKey(o.name).localeCompare(sortKey(r.name)),s=o=>(allEntities||[]).find(r=>r.id===t(o)),i=o=>{const r=s(o);return!!(n&&r&&r.last_seen_session_id===n.id)};return{recent:e.filter(i).sort(a),rest:e.filter(o=>!i(o)).sort(a)}}function _btsGroupHtml(e,t){return t.length?_btsBlock("table",e,"",{below:`<div class="bts-rows">${t.join("")}</div>`}):""}function _btsRecentThenRest(e,t,n,a,s){const i=_btsRecentSplit(t,n);if(!i.recent.length)return _btsGroupHtml(s,i.rest.map(a));if(!i.rest.length)return _btsGroupHtml("Recently mentioned",i.recent.map(a));const o=_btsRestOpen.has(e),r=`<button type="button" class="bts-link" data-bts-act="rest-toggle" data-bts-rest="${e}" aria-expanded="${o}">${o?"Hide":"Show"}</button>`;return _btsGroupHtml("Recently mentioned",i.recent.map(a))+_btsBlock("table","Everyone else","",{headExtra:r,below:o?`<div class="bts-rows">${i.rest.map(a).join("")}</div>`:""})}function _btsNpcsHtml(){const e=currentCampaign&&currentCampaign.id;if(_npcStandCid!==e)return'<p class="placeholder placeholder--inline">Loading NPCs\u2026</p>';const t=(allEntities||[]).filter(a=>a.type==="npc"&&!a.is_dead);return t.length?_btsRecentThenRest("npcs",t,a=>a.id,a=>{const s=_npcStandOf(a.id)||{};return`<div class="tf-card tf-card--xs bts-row bts-npc" data-bts-npc="${a.id}"><div class="bts-row-head">`+(a.image_url?`<img class="bts-pc-face bts-fac-face" src="${escHtml(a.image_url)}" alt="" loading="lazy">`:"")+(a.pending_reveal?'<span class="gm-surface-seal bts-row-tag bts-seal--reveal">Hidden</span>':"")+`<span class="bts-row-name">${escHtml(a.name)}</span></div>`+_btsBlock("table","Disposition",_fmapToneScaleHtml(s.tone||null)+`<input type="text" class="bts-npc-field bts-npc-phrase" maxlength="200" value="${esc
3082Html(s.phrase||"")}" placeholder="What the party knows, e.g. owes them a life-debt" aria-label="What the party knows about how ${escHtml(a.name)} feels">`,{headExtra:`<span class="bts-fac-label">${escHtml(_npcToneLabel(s.tone))}</span>`})+_btsBlock("gm","Wants",`<input type="text" class="bts-npc-field bts-npc-agenda" maxlength="300" value="${escHtml(s.agenda||"")}" placeholder="What ${escHtml(a.name)} wants" aria-label="What ${escHtml(a.name)} wants, GM only">`)+"</div>"},"All NPCs"):'<p class="placeholder placeholder--inline">No NPCs in the Codex yet.</p>'}function _btsNpcsWire(e){document.querySelectorAll('#bts-root .bts-panel[data-bts-area="npcs"] .bts-npc').forEach(t=>{const n=+t.dataset.btsNpc,a=_npcStandOf(n)||{};_fmapToneWire(t.querySelector(".fmap-tone-scale"),a.tone||null,s=>_btsNpcSave(e,n,{tone:s}))})}async function _btsNpcSave(e,t,n){try{if(await _npcStandSave(e,t,n),_campaignStale(e))return;_btsAct("decide",`npc:${t}`);const a=document.querySelector(`#bts-root .bts-npc[data-bts-npc="${t}"] .bts-fac-label`);a&&(a.textContent=_npcToneLabel((_npcStandOf(t)||{}).tone))}catch(a){showToast("Could not save: "+(a&&a.message||"try again"))}}function _btsFactionsHtml(){const e=_btsFactions;return e?e.length?_btsRecentThenRest("factions",e,n=>n.entity_id,n=>{const a=n.standing==null?null:+n.standing,s=_P2_RN_LABELS[String(a)]||"Unknown";return`<div class="tf-card tf-card--xs bts-row bts-fac" data-bts-fid="${n.entity_id}"><div class="bts-row-head">`+(n.image_url?`<img class="bts-pc-face bts-fac-face" src="${escHtml(n.image_url)}" alt="" loading="lazy">`:"")+`<span class="bts-row-name">${escHtml(n.name)}</span></div>`+_btsBlock("table","Standing",_p2RnScaleHtml(n,!0),{headExtra:`<span class="bts-fac-label">${escHtml(s)}</span>`})+_btsBlock("gm","Agenda",`<input type="text" class="bts-fac-agenda" maxlength="200" value="${escHtml(n.agenda||"")}" placeholder="What it wants" aria-label="What ${escHtml(n.name)} wants, GM only">`)+"</div>"},"All factions")+'<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-standing">Open Standing \u2192</button></p>':'<p class="placeholder placeholder--inline">No factions are tracked yet. Give a Codex faction a Kind and it appears here and on Party.</p>':'<p class="placeholder placeholder--inline">Loading factions\u2026</p>'}function _btsMapSecretsHtml(){const e=_btsMapSecrets;if(!e)return'<p class="placeholder placeholder--inline">Reading what&rsquo;s hidden on your maps&hellip;</p>';const t='<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-atlas">Open the Atlas &rarr;</button></p>';if(!(e.maps||[]).length)return'<p class="placeholder placeholder--inline">No maps yet. Upload one in the Atlas and anything you keep off it will be listed here.</p>'+t;const n=d=>`On ${d.map_title||"the map"}`,a=d=>` data-bts-mid="${d.map_id}" data-bts-map-title="${escHtml(d.map_title||"the map")}"`,s=(e.pins||[]).map(d=>_btsRow({tone:"reveal",tag:"Place",name:d.name||"A place",sub:n(d)+(d.kind?` \xB7 ${d.kind}`:""),data:a(d)+` data-bts-eid="${d.entity_id}"`,actions:[{act:"open-atlas-at",label:"Open"}],extra:'<span class="bts-reveal"><button type="button" class="btn-new btn-xs" data-bts-act="reveal-pin">Reveal</button></span>'})),i=(e.routes||[]).map(d=>{const u=d.from_name&&d.to_name?`${d.from_name} \u2192 ${d.to_name}`:"",p=(d.label||"").trim(),m=d.route_type==="water";return _btsRow({tone:"reveal",tag:d.encounter_id?"Trail":"Route",name:_btsRouteLabel(d),sub:n(d)+(p&&u?` \xB7 ${u}`:"")+(d.encounter_id?" \xB7 a footpath to a mark the table can already see":"")+(m?" \xB7 by water":""),data:a(d)+` data-bts-rid="${d.id}"`,actions:[{act:"open-atlas-at",label:"Open"}],extra:'<span class="bts-reveal"><button type="button" class="btn-new btn-xs" data-bts-act="reveal-route">Reveal</button></span>'})}),o=(e.encounters||[]).map(d=>{const u=(d.session_title||d.chapter||"").trim(),p=+d.trail_count||0;return _btsRow({tone:"reveal",tag:"Encounter",name:_btsMarkLabel(d),sub:n(d)+(u?` \xB7 ${u}`:"")+(p?` \xB7 ${p} trail${p===1?"":"s"} revealed with it`:""),data:a(d)+` data-bts-enid="${d.id}"`,actions:[{act:"open-atlas-at",label:"Open"}],extra:'<span class="bts-reveal"><button type="button" class="btn-new btn-xs" data-bts-act="reveal-enc">Reveal</button></span>'})}
3082),r=e.candidates||{pins:[],routes:[],encounters:[]},c=d=>d.length?`<div class="bts-rows">${d.join("")}</div>`:"";return[_btsBlock("reveal","Hidden places",_btsHidePick("place","Which place to hide","Hide from players",(r.pins||[]).map(d=>[`${d.map_id}:${d.entity_id}`,`${d.name||"A place"} \xB7 on ${d.map_title}`])),{rule:"Pinned for you, absent from the players' map. A place can be in their Codex already and still be hidden here - they know it exists, not where.",below:c(s)}),_btsBlock("reveal","Hidden routes",_btsHidePick("route","Which route to hide","Hide from players",(r.routes||[]).map(d=>[`${d.map_id}:${d.id}`,`${_btsRouteLabel(d)} \xB7 on ${d.map_title}`])),{rule:"Roads, water routes and trails only you can see. Revealing one shows the table where it goes.",below:c(i)}),_btsBlock("reveal","Hidden encounter marks",_btsHidePick("mark","Which mark to hide","Hide from players",(r.encounters||[]).map(d=>[`${d.map_id}:${d.id}`,`${_btsMarkLabel(d)} \xB7 on ${d.map_title}`])),{rule:"Marks for encounters the table hasn't reached. Any trail drawn to a mark is revealed with it.",below:c(o)})].join("")+t}function _btsHidePick(e,t,n,a){return a.length?`<form class="bts-add bts-hide" data-bts-hide="${e}" autocomplete="off"><select class="bts-add-pick" aria-label="${escHtml(t)}"><option value="">${escHtml(t)}</option>`+a.map(([s,i])=>`<option value="${escHtml(s)}">${escHtml(i)}</option>`).join("")+`</select><button type="submit" class="btn-new btn-xs" disabled>${escHtml(n)}</button></form>`:'<p class="form-helper-text bts-hide-none">Nothing left to hide here.</p>'}function _btsRouteLabel(e){const t=(e.label||"").trim();if(t)return t;if(e.encounter_id){const n=String(e.enc_description||"").trim(),a=String(e.enc_no||"").trim();return`Trail to ${n?_btsExcerpt(n,60):a?`encounter ${a}`:"an encounter"}`}return e.from_name&&e.to_name?`${e.from_name} \u2192 ${e.to_name}`:"An unnamed route"}function _btsMarkLabel(e){const t=(e.description||"").trim(),n=String(e.encounter_no||"").trim();return t?_btsExcerpt(t,80):n?`Encounter ${n}`:"An encounter mark"}async function _btsMapSecretHide(e,t){const n=e.dataset.btsHide,a=e.querySelector(".bts-add-pick"),[s,i]=String(a.value||"").split(":").map(Number);if(!s||!i)return;const o=a.options[a.selectedIndex].textContent,r=n==="place"?`/api/campaigns/${t}/maps/${s}/placements/${i}`:n==="route"?`/api/campaigns/${t}/maps/${s}/roads/${i}`:`/api/campaigns/${t}/maps/${s}/encounters/${i}`,c=e.querySelector('button[type="submit"]');c.disabled=!0;try{if(await api("PATCH",r,{hidden:!0}),_campaignStale(t))return;if(n==="place"){const l=(allEntities||[]).find(d=>d.id===i);l&&l.map_id===s&&(l.map_pin_hidden=!0)}if(_btsAct("decide",{place:`map_place:${i}`,route:`map_route:${i}`,mark:`map_encounter:${i}`}[n]),_btsMapSecrets=null,_btsMapSecretsCid=null,await _btsAreaEnsure("atlas",t),_campaignStale(t))return;showToast(`Hidden from players: ${o}`)}catch(l){c.disabled=!1,showToast("Could not hide it: "+(l&&l.message||"try again"))}}async function _btsMapSecretReveal(e,t,n){const a=t&&t.dataset.btsMid?+t.dataset.btsMid:null;if(!a)return;const s=e.dataset.btsAct,i=(t.querySelector(".bts-row-name")||{}).textContent||"this",o=s==="reveal-pin"?"place":s==="reveal-route"?"route":"encounter mark",r=await confirmDialog({title:"Reveal to the party?",message:`Show ${i} to the players on ${t.dataset.btsMapTitle||"the map"}?`,confirmLabel:"Reveal",cancelLabel:"Keep hidden",danger:!1});if(!r||r.confirmed===!1)return;let c=null;const l=t.dataset.btsEid?+t.dataset.btsEid:null;if(s==="reveal-pin"&&l?c=`/api/campaigns/${n}/maps/${a}/placements/${l}`:s==="reveal-route"&&t.dataset.btsRid?c=`/api/campaigns/${n}/maps/${a}/roads/${+t.dataset.btsRid}`:s==="reveal-enc"&&t.dataset.btsEnid&&(c=`/api/campaigns/${n}/maps/${a}/encounters/${+t.dataset.btsEnid}`),!!c){e.disabled=!0,e.textContent="Revealing\u2026";try{if(await api("PATCH",c,{hidden:!1}),_campaignStale(n))return;if(s==="reveal-pin"&&l){const d=(allEntities||[]).find(u=>u.id===l);d&&d.map_id===a&&(d.map_pin_hidden=!1)}if(_btsAct("decide",{"reveal-pin":`map_place:${l||0}`,"reveal-route":`map_route:${+t.dataset.btsRid||0}`,"reveal-enc":`map_encounter:${+t.dataset.btsEnid||0}`}[s]),_btsMapSecrets=null,_btsMapSecretsCid=null,await _btsAreaEnsure("atlas",n),_campaignStale(n))return;showToast(`Revealed to the party on ${t.dataset.btsMapTitle||"the map"}.`)}catch(d){e.disabled=!1,e.textContent="Reveal",showToast("Reveal failed: "+(d&&d.message||"try again"))}}}function _btsRulesHtml(){const e=_btsRules;if(!e)return'<p class="placeholder placeholder--inline">Loading your table rules\u2026</p>';const t=n=>n?`<p class="bts-rules-text">${escHtml(n).replace(/\n/g,"<br>")}</p>`:"";return _btsBlock("table","House rules",_btsQuickAdd("rule","A house rule, in one line",{label:"Add a house rule",max:400,data:' data-bts-doc="house_rules"'}),{below:t((e.house_rules||"").trim())})+_btsBlock("table","Lines and veils",_btsQuickAdd("rule","A line or a veil, in one line",{label:"Add a line or veil",max:400,data:' data-bts-doc="lines_veils"'}),{below:t((e.lines_veils||"").trim())})+'<p class="bts-fold-foot"><button type="button" class="bts-link" data-bts-act="open-rules">Edit these in the Chronicle \u2192</button></p>'}function _btsNotesHtml(e){const t=(allEntities||[]).filter(o=>(o.gm_notes||"").trim()&&!o.pending_reveal&&!_hiddenFromViewer(o)).sort((o,r)=>String(o.name).localeCompare(String(r.name))).map(o=>_btsRow({img:_btsEntityImg(o),tag:_BTS_TYPE_LABEL[o.type]||"",name:o.name,sub:_btsExcerpt(o.gm_notes,140),data:` data-bts-eid="${o.id}"`,actions:[{act:"open-entity",label:"Open"}]}
3082));_q2LoadedCid===e&&(allQuests||[]).filter(o=>(o.gm_notes||"").trim()).forEach(o=>t.push(_btsRow({tag:"Quest",name:o.title,sub:_btsExcerpt(o.gm_notes,140),data:` data-bts-qid="${o.id}"`,actions:[{act:"open-quest",label:"Open"}]})));const n=(allEntities||[]).filter(o=>!(o.gm_notes||"").trim()&&!_hiddenFromViewer(o)&&o.type!=="spell").sort((o,r)=>String(o.name).localeCompare(String(r.name))),a=_btsNoteDraftEid?n.find(o=>o.id===_btsNoteDraftEid):null,s=a?`<div class="tf-card tf-card--xs bts-row bts-row--draft" data-bts-eid="${a.id}"><div class="bts-row-head"><span class="gm-surface-seal bts-row-tag">${escHtml(_BTS_TYPE_LABEL[a.type]||"Entry")}</span><span class="bts-row-name">${escHtml(a.name)}</span></div><div class="bts-row-below">${_btsNoteBox(a)}</div></div>`:"";return{add:`<datalist id="bts-note-names">${n.slice(0,400).map(o=>`<option value="${escHtml(o.name)}"></option>`).join("")}</datalist>`+_btsQuickAdd("note","Name a Codex entry to write a note on",{label:"Add GM only text to existing entry",list:"bts-note-names"}),below:s+(t.length?`<div class="bts-rows">${t.join("")}</div>`:a?"":'<p class="placeholder placeholder--inline">No notes yet. What you write here is kept on the entry, in its gold Behind the Screen box.</p>')}}function _btsBenchHtml(e){const t=_btsOpenRead(e)[0]||null;return`<section class="bts-sec" id="bts-sec-bench"><div class="bts-sec-hdr"><span class="form-section-label">Workbench</span></div><div class="bts-tiles">${_BTS_AREAS.map(([n,a,s,i])=>_btsTile(n,a,s,n===t,i)).join("")}</div></section>`}async function _btsEnsurePartySpec(e){if(typeof _party2Spec=="object"&&_party2Spec&&_party2LoadedCid===e&&_party2Spec._srvCanAssess)return _btsPartySpec=_party2Spec,_btsPartyCid=e,!0;if(_btsPartyCid===e&&_btsPartySpec)return!0;let t;try{t=await api("GET",`/api/campaigns/${e}/party-render-spec`)}catch{return!1}return _campaignStale(e)||!_isGm()?!1:(_btsPartySpec=t,_btsPartyCid=e,!0)}async function _btsAreaEnsure(e,t){if(e==="party"){if(!await _btsEnsurePartySpec(t))return;const n=document.getElementById("bts-party-card");n&&(n.innerHTML=_btsPartyCardHtml())}else if(e==="honors"){if(!await _btsEnsurePartySpec(t))return;if(_btsRnPartyCid!==t)try{const n=await api("GET",`/api/campaigns/${t}/renown`);if(_campaignStale(t)||!_isGm())return;_btsRnParty=n&&n.party||null,_btsRnPartyCid=t}catch{}if(_btsEditorOpen(document.getElementById("bts-root")))return;_btsAreaRefresh("honors"),_btsHonorsWire(t)}else if(e==="npcs"){if(await _npcStandLoad(t),_campaignStale(t)||!_isGm()||_btsEditorOpen(document.getElementById("bts-root")))return;_btsAreaRefresh("npcs"),_btsNpcsWire(t)}else if(e==="quests"||e==="party"){if(_q2LoadedCid!==t){let n;try{n=await api("GET",`/api/campaigns/${t}/quests`)}catch{return}if(_campaignStale(t)||!_isGm())return;_q2LoadedCid!==t&&(allQuests=n,_q2LoadedCid=t),_btsAreaRefresh(e)}}else if(e==="rules"){if(_btsRulesCid!==t||!_btsRules){try{_btsRules=await api("GET",`/api/campaigns/${t}/table-rules`)}catch{return}if(_campaignStale(t)||!_isGm()){_btsRules=null;return}_btsRulesCid=t}_btsAreaRefresh("rules")}else if(e==="factions"){if(_btsFactionsCid!==t||!_btsFactions){let n;try{n=await api("GET",`/api/campaigns/${t}/renown`)}catch{return}if(_campaignStale(t)||!_isGm()||n.readonly)return;_btsFactions=n.factions||[],_btsFactionsCid=t}_btsEditorOpen(document.getElementById("bts-root"))||_btsAreaRefresh("factions")}else if(e==="atlas"){if(_btsMapSecretsCid!==t||!_btsMapSecrets){let n;try{n=await api("GET",`/api/campaigns/${t}/map-secrets`)}catch{return}if(_campaignStale(t)||!_isGm())return;_btsMapSecrets=n,_btsMapSecretsCid=t}_btsAreaRefresh("atlas")}e==="factions"&&_btsFactionsWire(t)}function _btsPaint(e,t){const n=_btsSpec;if(!n)return;e.innerHTML='<div class="fates-page bts-page"><div class="index-inner">'+_p2Masthead({eyebrow:"GM Prep",titleHtml:"Behind the Screen",leadHtml:"This tab is yours alone. Nothing here reaches a player until you reveal it."})+`<div class="bts-band">${_btsPlanHtml(n,t)}${_btsWaitingHtml(n,t)}</div>`+_btsBenchHtml(t)+_p2Colophon("Behind the Screen")+"</div></div>";const a=_btsStage(),s=a&&a.querySelector("#bts-rail"),i=_btsWaits(n);s&&(_tfRailDot(s,"bts-sec-plan",n.plan&&n.plan.text?"clear":null,n.plan&&n.plan.text?"You have a plan":""),_tfRailDot(s,"bts-sec-waiting",i?"watch":null,i?"Something is waiting on a decision":""),_tfRailMarkActive(_btsRailCfg())),_bt
3082sTabDot(i>0),e.querySelectorAll(".bts-panel[data-bts-area]").forEach(o=>_btsAreaEnsure(o.dataset.btsArea,t))}function _btsFatesMasthead(){const e=document.getElementById("fates-masthead");!e||e.dataset.ready==="1"||(e.dataset.ready="1",e.innerHTML=_p2Masthead({eyebrow:"GM Prep",titleHtml:"Fates",leadHtml:"Loose threads from your sessions, woven into scenes you can run.",statsHtml:'<span id="fates-stats" class="bts-fates-stats"></span>'}))}async function _btsOpenEntity(e,t){if(!_btsEntity(e))return;_btsAct("jump",`entity:${e}`);try{_trackEntityView(e)}catch{}if(await renderCodexEntry(e),!t)return;const n=document.querySelector(`.gm-notes-display[data-eid="${e}"]`);n&&n.offsetParent!==null&&n.scrollIntoView({block:"center",behavior:"auto"})}function _btsAfterNote(e,t,n){if(!e||!e.isConnected)return;e.innerHTML=`<span class="gm-bs-mark" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>`+(n?escHtml(n).replace(/\n/g,"<br>"):'<span class="gm-notes-prompt">Click to write a note only you can see&hellip;</span>'),_btsAct("decide",`note:${t}`);const a=currentCampaign&&currentCampaign.id;e.closest(".bts-row--draft")&&(_btsNoteDraftEid=null);const s=e.closest(".bts-row--offer");s&&n&&s.remove(),a&&_btsAreaRefresh("hidden")}function _btsEditPlan(e){const t=currentCampaign&&currentCampaign.id;if(!e||!t||e.dataset.editing==="1")return;e.dataset.editing="1";const n=_btsSpec&&_btsSpec.plan&&_btsSpec.plan.text||"",a=document.createElement("textarea");a.className="gm-notes-ta cx2-prose bts-plan-ta",a.value=n,a.maxLength=4e3,a.placeholder=e.dataset.btsStarter||"",a.rows=Math.min(16,Math.max(4,n.split(`
3083`).length+2)),e.style.display="none",e.insertAdjacentElement("afterend",a),a.focus();try{a.setSelectionRange(a.value.length,a.value.length)}catch{}let s=!1;const i=async o=>{if(s)return;s=!0;const r=a.value.trim();if(!o&&r!==n.trim())try{const l=await api("PUT",`/api/campaigns/${t}/behind-the-screen/plan`,{text:r});!_campaignStale(t)&&_btsSpec&&(_btsSpec.plan={text:l.text||"",updated_at:l.updated_at||null})}catch(l){if(showToast("Save failed, your text is still here: "+(l&&l.message||"try again")),a.isConnected){s=!1;return}}a.remove();const c=document.getElementById("bts-root");c&&!_campaignStale(t)&&_btsLoadedCid===t&&!_btsEditorOpen(c)?_btsPaint(c,t):(e.dataset.editing="",e.style.display="")};a.addEventListener("blur",()=>i(!1)),a.addEventListener("keydown",o=>{o.key==="Escape"?(o.preventDefault(),i(!0)):o.key==="Enter"&&(o.ctrlKey||o.metaKey)&&(o.preventDefault(),a.blur())})}async function _btsSubmitAdd(e){const t=currentCampaign&&currentCampaign.id,n=e.querySelector(".bts-add-name"),a=(n.value||"").trim();if(!t||!a||e.dataset.busy==="1")return;e.dataset.busy="1";const s=e.dataset.btsAdd;try{if(s==="entity"){const i=e.querySelector(".bts-add-type").value||"npc",o=await api("POST",`/api/campaigns/${t}/entities`,{name:a,type:i,pending_reveal:!0});if(_campaignStale(t))return;o&&o.id&&!(allEntities||[]).some(r=>r.id===o.id)&&(allEntities=[...allEntities||[],o]),_btsAct("decide",o&&o.id?`entity:${o.id}`:null);try{typeof renderCompendium=="function"&&renderCompendium()}catch{}n.value="",_btsAreaRefresh("hidden",".bts-add-name")}else if(s==="quest"){const i=await api("POST",`/api/campaigns/${t}/quests`,{title:a,pending_reveal:!0});if(_campaignStale(t))return;i&&i.id&&_q2LoadedCid===t&&!(allQuests||[]).some(o=>o.id===i.id)&&(allQuests=[...allQuests||[],i]),_btsAct("decide",i&&i.id?`quest:${i.id}`:null),n.value="",_btsAreaRefresh("quests",".bts-add-name")}else if(s==="pcquest"){const i=+e.dataset.btsEid,o=await api("POST",`/api/campaigns/${t}/quests`,{title:a,pc_entity_id:i,is_private:!0});if(_campaignStale(t))return;o&&o.id&&_q2LoadedCid===t&&!(allQuests||[]).some(r=>r.id===o.id)&&(allQuests=[...allQuests||[],o]),_btsAct("decide",o&&o.id?`quest:${o.id}`:null),_btsAreaRefresh("party"),_btsAreaRefresh("quests")}else if(s==="privquest"){const i=+e.querySelector(".bts-add-pick").value;if(!i)return;const o=await api("POST",`/api/campaigns/${t}/quests`,{title:a,pc_entity_id:i,is_private:!0});if(_campaignStale(t))return;o&&o.id&&_q2LoadedCid===t&&!(allQuests||[]).some(r=>r.id===o.id)&&(allQuests=[...allQuests||[],o]),_btsAct("decide",o&&o.id?`quest:${o.id}`:null),_btsAreaRefresh("quests",'.bts-add[data-bts-add="privquest"] .bts-add-name')}else if(s==="step"){const i=+e.querySelector(".bts-add-pick").value,o=(allQuests||[]).find(c=>c.id===i);if(!o)return;const r=await api("PUT",`/api/campaigns/${t}/quests/${i}`,{leads:[..._q2Leads(o),{text:a,status:"open",visibility:"gm"}]});if(_campaignStale(t))return;allQuests=(allQuests||[]).map(c=>c.id===i?r:c),_btsAct("decide",`step:${i}`),_btsAreaRefresh("quests",'.bts-add[data-bts-add="step"] .bts-add-name')}else if(s==="note"){const i=(allEntities||[]).find(r=>!_hiddenFromViewer(r)&&String(r.name).toLowerCase()===a.toLowerCase());if(!i){showToast("No Codex entry has that name. Add it under Hidden Entries first.");return}if((i.gm_notes||"").trim()){showToast(`${i.name} already has a note. It is in the list below.`);return}_btsNoteDraftEid=i.id,_btsAreaRefresh("hidden");const o=document.querySelector("#bts-root .bts-row--draft .bts-note-display");o&&_cx2EditGmNotes(o,i.id)}else if(s==="rule"){const i=e.dataset.btsDoc;if(await api("POST",`/api/campaigns/${t}/table-rules/line`,{doc:i,new:a}),_campaignStale(t))return;_btsRules=null,_btsRulesCid=null,typeof _tableRulesLoadedCid<"u"&&(_tableRulesLoadedCid=null),_btsAct("decide",null),n.value="",await _btsAreaEnsure("rules",t),document.querySelector(`#bts-root .bts-add[data-bts-doc="${i}"] .bts-add-name`)?.focus()}}catch(i){showToast("Could not add it: "+(i&&i.message||"try again"))}finally{e.dataset.busy=""}}function _btsFactionsWire(e){const t=document.querySelector('#bts-root .bts-panel[data-bts-area="factions"] .bts-panel-body'),n=t&&t.querySelector(".bts-rows");!n||n.dataset.btsRailWired==="1"||!n.querySelector(".p2-rn-scale")||(n.dataset.btsRailWired="1",_p2RnWireScales(t,(a,s)=>{const i=t.querySelector(`.bts-fac[data-bts-fid="${a}"]`);i&&_isGm()&&_btsFactionSet(i,{standing:s})},()=>{showToast("A change is proposed for this faction. Review it on Party.",4e3)}
3083,(a,s)=>{const i=a.closest(".bts-fac")?.querySelector(".bts-fac-label");i&&(i.textContent=_P2_RN_LABELS[String(s)]||"Unknown")}))}async function _btsFactionSet(e,t){const n=currentCampaign&&currentCampaign.id,a=+e.dataset.btsFid,s=(_btsFactions||[]).find(i=>i.entity_id===a);if(!(!n||!s))try{if("standing"in t){if(await api("POST",`/api/campaigns/${n}/renown/${a}/standing`,{standing:t.standing}),_campaignStale(n))return;s.standing=t.standing}else{if(await api("POST",`/api/campaigns/${n}/renown/${a}/agenda`,{agenda:t.agenda||null}),_campaignStale(n))return;s.agenda=t.agenda||null}_btsAct("decide",`faction:${a}`),typeof _p2RnData<"u"&&(_p2RnData=null),_cx2RnCid=null,"standing"in t&&(_btsAreaRefresh("factions"),_btsFactionsWire(n))}catch(i){showToast("Could not save: "+(i&&i.message||"try again"))}}function _btsKeepTileInView(e){const t=_btsStage(),n=t&&t.querySelector(`.bts-tile[data-bts-tile="${e}"]`),a=t&&t.querySelector("#bts-rail");if(!n||!a)return;const s=t.getBoundingClientRect().top,i=a.getBoundingClientRect().bottom-s,o=n.getBoundingClientRect().top-s,r=n.getBoundingClientRect().bottom-s;o>=i&&r<=t.clientHeight||(t.scrollTop=Math.max(0,t.scrollTop+o-i-12))}function _btsWire(e){e._btsWired||(e._btsWired=!0,e.addEventListener("input",t=>{const n=t.target.closest&&t.target.closest(".bts-add");n&&_btsAddReady(n);const a=t.target.closest&&t.target.closest(".bts-story");if(a){const s=a.querySelector('[data-bts-act="story-save"]');s&&(s.disabled=!t.target.value.trim())}}),e.addEventListener("change",t=>{const n=t.target.closest&&t.target.closest(".bts-add");n&&_btsAddReady(n)}),e.addEventListener("submit",t=>{const n=t.target.closest(".bts-add");if(n&&(t.preventDefault(),!!_isGm())){if(n.dataset.btsHide){_btsM
3083apSecretHide(n,currentCampaign&&currentCampaign.id);return}_btsSubmitAdd(n)}}),e.addEventListener("focusout",t=>{const n=t.target.closest&&t.target.closest(".bts-npc-field");if(n&&_isGm()){const r=+n.closest(".bts-npc").dataset.btsNpc,c=n.classList.contains("bts-npc-agenda")?"agenda":"phrase",l=_npcStandOf(r)||{},d=(n.value||"").trim();d!==(l[c]||"")&&_btsNpcSave(currentCampaign&&currentCampaign.id,r,{[c]:d});return}const a=t.target.closest&&t.target.closest(".bts-fac-agenda");if(!a||!_isGm())return;const s=a.closest(".bts-fac"),i=s&&(_btsFactions||[]).find(r=>r.entity_id===+s.dataset.btsFid),o=(a.value||"").trim();i&&o!==(i.agenda||"")&&_btsFactionSet(s,{agenda:o})}),e.addEventListener("click",async t=>{const n=currentCampaign&&currentCampaign.id;if(!n||!_isGm())return;const a=t.target.closest(".bts-plan-display");if(a){_btsEditPlan(a);return}const s=t.target.closest(".bts-note-display");if(s){_cx2EditGmNotes(s,+s.dataset.eid);return}const i=t.target.closest(".bts-pc-row");if(i){const p=+i.closest(".bts-pc").dataset.btsPc;_btsPcOpen.has(p)?_btsPcOpen.delete(p):_btsPcOpen.add(p),_btsAreaRefresh("party");return}const o=t.target.closest(".bts-tile");if(o){const p=e.querySelector("#bts-sec-bench");if([...p.querySelectorAll(".bts-story-ta")].some(g=>g.value.trim())||p.querySelector(".gm-notes-ta")){showToast("Save or cancel what you are writing first.");return}const m=o.dataset.btsTile,f=!o.classList.contains("is-selected");if(_btsOpenWrite(n,f?[m]:[]),p.outerHTML=_btsBenchHtml(n),f){_btsAct("open",null),_btsAreaEnsure(m,n);const g=e.querySelector(`.bts-tile[data-bts-tile="${m}"]`);g&&g.focus({preventScroll:!0})}_btsKeepTileInView(m),_tfRailMarkActive(_btsRailCfg());return}const r=t.target.closest("[data-bts-act]");if(!r)return;const c=r.closest(".bts-row"),l=r.dataset.btsAct,d=r.dataset.btsEid?+r.dataset.btsEid:c&&c.dataset.btsEid?+c.dataset.btsEid:null,u=c&&c.dataset.btsQid?+c.dataset.btsQid:null;if(l==="honors-edit"){const p=r.closest(".bts-panel-body")?.querySelector("[data-bts-honors]");if(!p||!_btsPartySpec)return;r.classList.add("hidden"),_p2OpenHonorsEditor(p,_btsPartySpec,{bare:!0,npcs:_btsHonorNpcs(),after:()=>_btsHonorsReload(n,!0),onCancel:()=>_btsHonorsReload(n,!1)}),p.querySelector(".p2-de-title")?.focus();return}if(l==="rest-toggle"){const p=r.dataset.btsRest;_btsRestOpen.has(p)?_btsRestOpen.delete(p):_btsRestOpen.add(p),_btsAreaRefresh(p),p==="npcs"?_btsNpcsWire(n):p==="factions"&&_btsFactionsWire(n);return}if(l==="legend-crown"||l==="legend-none"||l==="legend-undo")return _btsLegendAct(l,r,c,e,n);if(l==="legend-share"){const p=_btsSpec&&_btsSpec.legend&&_btsSpec.legend.crowned;p&&p.card&&_tfLegendShareMenu(p.card,r,!0);return}if(l==="open-entity"&&d)return _btsOpenEntity(d,!0);if(l==="open-entity-plain"&&d)return _btsOpenEntity(d,!1);if(l==="add-note"&&c){const p=c.querySelector(".bts-row-below"),m=p&&p.querySelector(".bts-note-display");m&&(p.classList.remove("hidden"),_cx2EditGmNotes(m,+m.dataset.eid));return}if(l==="story-save"&&d){const p=e.querySelector(`.bts-story[data-bts-story="${d}"]`),m=p&&p.querySelector(".bts-story-ta"),f=m?m.value.trim():"";if(!f){m&&m.focus();return}r.disabled=!0;try{const g=await api("PUT",`/api/campaigns/${n}/entities/${d}/backstory`,{text:f});if(_campaignStale(n))return;const h=(allEntities||[]).find(v=>v.id===d);h&&(h.owner_private_notes=g.owner_private_notes,h.owner_private_source=g.owner_private_source),_btsAct("decide",`story:${d}`),m.value="",_btsAreaRefresh("party")}catch(g){r.disabled=!1,showToast("Could not save, your text is still here: "+(g&&g.message||"try again"))}return}if(l==="reveal-entity"&&d){const p=c.querySelector(".pending-reveal-cast-chk"),m=!!(p&&p.checked&&!p.disabled);r.disabled=!0,r.textContent="Revealing\u2026";try{await _tfRevealEntity(d,m),_btsAct("decide",`reveal:${d}`),_campaignStale(n)||_btsAreaRefresh("hidden")}catch(f){r.disabled=!1,r.textContent="Reveal",showToast("Reveal failed: "+(f&&f.message||"try again"))}return}if(l==="reveal-step"&&u&&c.dataset.btsLid){const p=`/api/campaigns/${n}/quests/${u}/leads/${encodeURIComponent(c.dataset.btsLid)}`;r.disabled=!0;try{const m=await api("POST",`${p}/reveal`);if(_campaignStale(n))return;allQuests=(allQuests||[]).map(g=>g.id===u?m:g),_btsAct("decide",`step:${u}`),_btsAreaRefresh("quests");const f=async()=>{try{const g=await api("POST",`${p}/unreveal`);if(_campaignStale(n))return;allQuests=(allQuests||[]).map(h=>h.id===u?g:h),_btsAreaRefresh("quests")}catch(g){showToast("Undo failed: "+(g&&g.message||"try again"))}};typeof window.tfToast=="function"?window.tfToast({type:"success",text:"Step revealed.",action:{label:"Undo",onClick:f}}
3083):showToast("Step revealed.")}catch(m){r.disabled=!1,showToast("Reveal failed: "+(m&&m.message||"try again"))}return}if(l==="reveal-quest"&&u){const p=`/api/campaigns/${n}/quests/${u}`;r.disabled=!0;try{const m=await api("POST",`${p}/reveal`);if(_campaignStale(n))return;allQuests=(allQuests||[]).map(g=>g.id===u?m:g),_btsAct("decide",`reveal:${u}`),_btsAreaRefresh("quests"),_btsAreaRefresh("party");const f=async()=>{try{const g=await api("POST",`${p}/unreveal`);if(_campaignStale(n))return;allQuests=(allQuests||[]).map(h=>h.id===u?g:h),_btsAreaRefresh("quests"),_btsAreaRefresh("party")}catch(g){showToast("Undo failed: "+(g&&g.message||"try again"))}};typeof window.tfToast=="function"?window.tfToast({type:"success",text:`Revealed \u201C${m.title||"the quest"}\u201D.`,action:{label:"Undo",onClick:f}}):showToast("Revealed.")}catch(m){r.disabled=!1,showToast("Reveal failed: "+(m&&m.message||"try again"))}return}if(l==="reveal-pin"||l==="reveal-route"||l==="reveal-enc"){c&&await _btsMapSecretReveal(r,c,n);return}if(l==="open-atlas"||l==="open-atlas-at"){_btsAct("jump",null);const p=l==="open-atlas-at"&&c&&c.dataset.btsMid?+c.dataset.btsMid:null,m=l==="open-atlas-at"&&c&&c.dataset.btsEid?+c.dataset.btsEid:null,f=`#atlas/${n}`+(p?`/${p}`+(m?`/${m}`:""):"");location.hash===f?typeof _atlasHandleHash=="function"&&_atlasHandleHash():location.hash=f;return}if(l==="open-quest"&&u)return _btsAct("jump",`quest:${u}`),_partyHandleHash(`#party/${n}/quests/${u}`);if(l==="open-standing")return _btsAct("jump",null),_partyHandleHash(`#party/${n}/standing`);if(l==="open-party")return _btsAct("jump",null),_partyHandleHash(`#party/${n}`);if(l==="open-updates"){_btsAct("jump","updates"),document.querySelector('.tab-btn[data-tab="manage"], #nav-manage-btn')?.click(),typeof _tfManageFeedSet=="function"&&_tfManageFeedSet(!0);return}if(l==="open-rules"){_btsAct("jump",null),document.getElementById("tab-btn-sessions")?.click();return}if(l==="schedule"){_btsAct("jump",null),document.querySelector('.tab-btn[data-tab="manage"], #nav-manage-btn')?.click();const p=document.getElementById("manage-next-session-section");if(p&&typeof _tfManageRailCfg=="function")try{_tfRailScrollTo(_tfManageRailCfg(),p,"auto")}catch{}return}if(l==="dismiss"&&c&&c.dataset.btsKey){const p=c.dataset.btsKey;r.disabled=!0;try{if(await api("POST",`/api/campaigns/${n}/behind-the-screen/event`,{kind:"dismiss",key:p}),_campaignStale(n)||!_btsSpec)return;_btsSpec.dismissed=[..._btsSpec.dismissed||[],p],c.remove(),_btsWaitingPrune()}catch(m){r.disabled=!1,showToast("Failed: "+(m&&m.message||"try again"))}return}if((l==="ruling-keep"||l==="ruling-discard")&&c&&c.dataset.btsRid){const p=+c.dataset.btsRid;c.querySelectorAll("button").forEach(m=>{m.disabled=!0});try{if(await api("POST",`/api/campaigns/${n}/table-rules/rulings/${p}`,{action:l==="ruling-keep"?"keep":"discard"}),_btsAct("decide",`ruling:${p}`),_campaignStale(n)||!_btsSpec)return;typeof _tableRulesLoadedCid<"u"&&(_tableRulesLoadedCid=null),_btsRules=null,_btsRulesCid=null,_btsSpec.rulings=(_btsSpec.rulings||[]).filter(m=>m.id!==p),c.remove(),_btsWaitingPrune()}catch(m){c.querySelectorAll("button").forEach(f=>{f.disabled=!1}),showToast("Failed: "+(m&&m.message||"try again"))}return}if((l==="prop-confirm"||l==="prop-dismiss")&&c&&c.dataset.btsPid){const p=+c.dataset.btsPid,m=l==="prop-confirm";c.querySelectorAll("button").forEach(f=>{f.disabled=!0});try{let f=null;try{f=await api("POST",`/api/campaigns/${n}/renown/events/${p}`,{action:m?"confirm":"dismiss"})}catch(h){if(!(h&&h.status===409))throw h}if(_btsAct("decide",`proposal:${p}`),_campaignStale(n)||!_btsSpec)return;const g=(_btsSpec.proposals||[]).find(h=>h.id===p);_btsSpec.proposals=(_btsSpec.proposals||[]).filter(h=>h.id!==p),f&&m&&g&&(_btsSpec.proposals||[]).forEach(h=>{if(h.faction_entity_id!==g.faction_entity_id)return;f.standing!=null&&h.kind!=="agenda"&&(h.standing=f.standing);const v=document.querySelector(`#bts-sec-waiting .bts-row[data-bts-pid="${h.id}"] .bts-prop-move`);v&&(v.textContent=_btsMoveLabel(h))}),_btsFactions=null,_btsFactionsCid=null,typeof _p2RnData<"u"&&(_p2RnData=null),c.remove(),_bt
3083sWaitingPrune(),document.querySelector('.bts-panel[data-bts-area="factions"]')&&_btsAreaEnsure("factions",n),f&&m&&showToast(f.label?`Standing set to ${f.label}.`:"Agenda moved.",4e3)}catch(f){c.querySelectorAll("button").forEach(g=>{g.disabled=!1}),showToast("Failed: "+(f&&f.message||"try again"))}return}}),e.addEventListener("keydown",t=>{const n=t.target.closest&&t.target.closest(".bts-plan-display, .bts-note-display");n&&(t.key==="Enter"||t.key===" ")&&(t.preventDefault(),n.classList.contains("bts-plan-display")?_btsEditPlan(n):_cx2EditGmNotes(n,+n.dataset.eid))}))}async function renderBehindScreen(e){if(e=e||{},!_isGm()){_btsTeardown();return}const t=currentCampaign&&currentCampaign.id,n=_btsStage();if(!t||!n)return;_btsFatesMasthead();let a=n.querySelector("#bts-rail");a||(a=document.createElement("nav"),a.className="manage-rail bts-rail",a.id="bts-rail",a.setAttribute("aria-label","GM Prep sections"),a.innerHTML=_BTS_SECS.map(([o,r])=>`<button type="button" class="manage-rail-chip" data-rail="${o}">${r}</button>`).join(""),n.insertBefore(a,n.firstChild),_tfRailInit(_btsRailCfg())),a.classList.remove("hidden");let s=n.querySelector("#bts-root");if(s||(s=document.createElement("div"),s.id="bts-root",a.insertAdjacentElement("afterend",s),_btsRailState.pinned=null,_btsRailState.active=null,_btsWire(s)),_btsEditorOpen(s))return;_btsLoadedCid!==t&&(_btsSpec=null,_btsPartySpec=null,_btsPartyCid=null,_btsRules=null,_btsRulesCid=null,_btsFactions=null,_btsFactionsCid=null,_btsRnParty=null,_btsRnPartyCid=null,_btsPcOpen.clear(),_btsNoteDraftEid=null,_btsMapSecrets=null,_btsMapSecretsCid=null,s.innerHTML='<div class="fates-page bts-page"><div class="index-inner"><p class="placeholder--padded">Loading&hellip;</p></div></div>');let i;try{i=await api("GET",`/api/campaigns/${t}/behind-the-screen`)}catch(o){if(_campaignStale(t))return;s.innerHTML=`<div class="fates-page bts-page"><div class="index-inner"><p class="placeholder--padded error">Could not load Behind the Screen. ${escHtml(o&&o.message||"")}</p></div></div>`;return}if(!(_campaignStale(t)||!_isGm())){if(_q2LoadedCid!==t)try{const o=await api("GET",`/api/campaigns/${t}/quests`);if(_campaignStale(t)||!_isGm())return;_q2LoadedCid!==t&&(allQuests=o,_q2LoadedCid=t)}catch{}_btsEditorOpen(s)||(_btsSpec=i,_btsLoadedCid=t,_btsDotCid=t,_btsPaint(s,t))}}async function loadThreads(){if(!currentCampaign)return;const e=document.getElementById("threads-body");if(!e)return;const t=document.getElementById("fates-colophon-camp");t&&(t.textContent=currentCampaign.name||""),e.innerHTML='<p class="placeholder placeholder--inline">Loading Loose Threads\u2026</p>';try{const n=`/api/campaigns/${currentCampaign.id}/threads`,a="hot,warm,cold,dormant,resolved",[s,i,o]=await Promise.all([api("GET",`${n}?status=${a}`),api("GET",`${n}?user_status=dismissed&status=${a}`).catch(()=>({threads:[]})),api("GET",`${n}?user_status=resolved&status=${a}`).catch(()=>({threads:[]}))]),r=!!s.preview,c=s.threads||[],l=r?[]:i.threads||[];_hooksResolvedThreads=r?[]:o.threads||[];const d=new Map;for(const u of[...c,...l])d.set(u.id,u);_hooksAllThreads=[...d.values()],_hooksLastResponseMeta={total:s.total||0,preview:r},_renderThreadsBody(),_tfThreadsLastTotal=s.total||0,_tfThreadsLoaded=!0,_isGm()&&(loadFatesClusters().catch(()=>{}),_loadSavedFates())}catch(n){e.classList.remove("fates-loom-table"),n.message&&/403/.test(n.message)?e.innerHTML='<p class="placeholder placeholder--padded">Fates requires a Master or Pro plan.</p>':e.innerHTML=`<p class="placeholder placeholder--padded error">Failed to load threads: ${escHtml(n.message||"Unknown error")}</p>`}}let _fatesWeavesState="idle",_fatesWeavesError="";async function loadFatesClusters(){if(!(!currentCampaign||!document.getElementById("fates-player-section"))){_fatesWeavesState="loading",_fatesSynthesisCache={},_fatesSynthesisInflight={},_renderFatesWeaves();try{const t=await api("GET",`/api/campaigns/${currentCampaign.id}/fates/clusters`);_fatesWeaves=t.weaves||t.clusters||[],_fatesNeighbors=t.thread_neighbors||{},_fatesBlocklist=t.blocklist||[],_fatesJunctionThreads=t.junction_threads||[],_fatesEntities=t.entities||[],_fatesEntityById={};for(const n of _fatesEntities)_fatesEntityById[n.id]=n;_fatesThreadsById={};for(const n of t.threads||[])_fatesThreadsById[n.thread_id]=n;_fatesPossibleIdx=0,_fatesWeavesState=_fatesWeaves.length?"ready":"empty",_renderFatesWeaves(),_renderThreadsBody()}catch(t){_fatesWeavesState="error",_fatesWeavesError=t&&t.message?t.message:"Unknown error",_renderFatesWeaves()}}}function _renderFatesWeaves(){_renderFatesPossible(),_renderFatesPlayerWeave()}function _renderLoomTable(e,t,n){n=n||{};const a=(t||[]).map(r=>
3083_fatesEntityById[r]).filter(Boolean),s=a.map(r=>{const c=r.is_hub?" fates-warp-head--hub":"",l=n.showEntitySnip?" fates-warp-head--snip":"",d=`${(r.type||"").toUpperCase()} \xB7 in ${r.thread_count||0} loose thread${(r.thread_count||0)===1?"":"s"}${r.is_hub?" (hub)":""}`,u=n.showEntitySnip?`<button class="fates-warp-snip" data-entity-snip="${r.id}" title="Mark as setting noise (excludes from suggestions)" aria-label="Mark as setting">\xD7</button>`:"";return`<span class="fates-warp-head fates-warp-head--${r.type}${c}${l}" title="${escHtml(d)}"><span class="fates-warp-head-name">${escHtml(r.name)}</span>`+u+"</span>"}).join(""),i=a.length?`<div class="fates-loom-info fates-loom-info--head"></div><div class="fates-loom-bar fates-loom-bar--warp">${s}</div>`:"",o=e.map(r=>{const c=new Set(r.touchpoints&&r.touchpoints.length?r.touchpoints:(_fatesThreadsById[r.id]||_fatesThreadsById[r.thread_id])?.touchpoints||[]),l=a.length?a.map(w=>c.has(w.id)?`<span class="fates-knot fates-knot--${w.type}" title="${escHtml(w.name)}"></span>`:'<span class="fates-knot fates-knot--empty" aria-hidden="true"></span>
3083').join(""):"",d=(r.status||"warm").toLowerCase(),u=r.id||r.thread_id,p=_fatesSelected.has(u),m=n.selectable?`<label class="thread-select" title="Add to weave"><input type="checkbox" class="thread-select-cb" data-thread-id="${u}"${p?" checked":""} /></label>`:"",f=n.showHeat&&r.heat_score!==null&&r.heat_score!==void 0?`<span class="thread-heat-score" title="Heat score">${r.heat_score}</span>`:"";let g="";if(n.weaveAdjacency&&n.weaveAdjacency.size&&!p){let w=0;for(const $ of c)n.weaveAdjacency.has($)&&w++;w>0&&(g=`<span class="fates-weave-adj-badge" title="Shares ${w} entit${w===1?"y":"ies"} with the Loose Threads being woven above">+${w}</span>`)}const h=n.showActions?`<span class="fates-loom-actions"><button class="fates-row-action-btn" data-thread-action="dismissed" data-thread-id="${u}" title="Dismiss thread (sticky)" aria-label="Dismiss">\u2715</button><button class="fates-row-action-btn" data-thread-action="resolved" data-thread-id="${u}" title="Mark resolved" aria-label="Mark resolved">\u2713</button></span>`:n.showRemove?`<span class="fates-loom-actions fates-loom-actions--snip"><button class="fates-row-action-btn fates-row-action-btn--snip" data-thread-snip="${u}" title="Remove from weave" aria-label="Remove from weave">\u2715</button></span>`:"",v=["fates-loom-info",p?"fates-loom-info--selected":"",n.compact?"fates-loom-info--compact":""].filter(Boolean).join(" "),b=["fates-loom-bar","fates-loom-bar--weft",p?"fates-loom-bar--selected":""].join(" "),y=(r.context||"").trim(),_=y?` data-thread-tooltip-title="${escHtml(r.title||"")}" data-thread-tooltip-context="${escHtml(y)}"`:"";return`<div class="${v}" data-thread-id="${u}"${_}>`+m+`<span class="thread-status-pill thread-status-pill--${d}">${escHtml(d)}</span><span class="fates-loom-title">${escHtml(r.title||"(untitled)")}</span>`+g+f+h+`</div><div class="${b}">${l}</div>`}).join("");return i+o}function _pickThreadsTableColumns(e,t){t=t||8;const n=new Map;for(const s of e){const i=s.touchpoints&&s.touchpoints.length?s.touchpoints:(_fatesThreadsById[s.id]||_fatesThreadsById[s.thread_id])?.touchpoints||[];for(const o of i)n.set(o,(n.get(o)||0)+1)}const a=[];for(const[s,i]of n)i<2||_fatesEntityById[s]&&a.push({eid:s,cnt:i});return a.sort((s,i)=>{if(i.cnt!==s.cnt)return i.cnt-s.cnt;const o=_fatesEntityById[s.eid],r=_fatesEntityById[i.eid];return(o?.name||"").localeCompare(r?.name||"")}),a.slice(0,t).map(s=>s.eid)}function _fatesVisibleWeaves(){if(!_fatesSelected.size)return _fatesWeaves;const e=_fatesSelected;return _fatesWeaves.map(t=>({w:t,overlap:(t.thread_ids||[]).reduce((n,a)=>n+(e.has(a)?1:0),0)})).filter(t=>t.overlap>0).sort((t,n)=>n.overlap-t.overlap).map(t=>t.w)}function _renderFatesPossible(){const e=document.getElementById("fates-suggestions-btn"),t=_fatesVisibleWeaves(),n=_fatesSelected.size>0;e&&(_fatesWeavesState==="idle"||_fatesWeavesState==="loading"?(e.textContent="Suggest combinations",e.disabled=!0,e.title="Computing suggested combinations\u2026"):_fatesWeavesState==="error"?(e.textContent="Suggest combinations",e.disabled=!1,e.title="Suggestion engine errored, open to retry"):_fatesWeaves.length?t.length?(e.textContent=`Suggest combinations (${t.length})`,e.disabled=!1,e.title=n?"Browse suggested combinations that build on your selected loose threads":"Browse algorithm-suggested loose-thread combinations to weave"):(e.textContent="Suggest combinations (0)",e.disabled=!0,e.title="No suggested combinations include your selected loose threads, uncheck to browse all"):(e.textContent="Suggest combinations",e.disabled=!0,e.title="No suggested loose-thread combinations at this time"));const a=document.getElementById("fates-suggestions-modal");a&&!a.classList.contains("hidden")&&_renderFatesSuggestionsModalBody()}function _renderFatesSuggestionsModalBody(){const e=document.getElementById("fates-possible-table"),t=document.getElementById("fates-possible-meta"),n=document.getElementById("fates-possible-synthesis"),a=document.getElementById("fates-suggestions-counter"),s=document.getElementById("fates-possible-prev"),i=document.getElementById("fates-possible-next"),o=document.getElementById("fates-suggestions-add");if(!e)return;const r=_fatesVisibleWeaves(),c=_fatesSelected.size>0;if(_fatesWeavesState==="loading"){e.innerHTML='<p class="placeholder placeholder--padded">Computing thread affinities\u2026</p>',a&&(a.textContent=""),t&&(t.textContent=""),n&&(n.innerHTML=""),s&&(s.disabled=!0),i&&(i.disabled=!0),o&&(o.disabled=!0);return}if(_fatesWeavesState==="error"){e.innerHTML=`<p class="placeholder placeholder--padded error">Couldn't compute suggested combinations: ${escHtml(_fatesWeavesError)}</p>`,a&&(a.textContent=""),t&&(t.textContent=""),n&&(n.innerHTML=""),s&&(s.disabled=!0),i&&(i.disabled=!0),o&&(o.disabled=!0);return}if(_fatesWeavesState==="empty"||!r.length){e.innerHTML=c&&_fatesWeaves.length?`<p class="placeholder placeholder--padded">No suggested combinations build on your selected loose threads. Uncheck them to browse all suggestions, or weave the ones you've picked directly.</p>`:`<p class="placeholder placeholder--padded">No suggestions yet. Your active threads don't share at least 2 entities (or 1 entity across 3 threads). Add cross-references between threads, or run another session and let detection re-fire.</p>`,a&&(a.textContent=""),t&&(t.textContent=""),n&&(n.innerHTML=""),s&&(s.disabled=!0),i&&(i.disabled=!0),o&&(o.disabled=!0);return}_fatesPossibleIdx>=r.length&&(_fatesPossibleIdx=0);const l=r[_fatesPossibleIdx],d=r.length;a&&(a.textContent=`Suggestion ${_fatesPossibleIdx+1} of ${d}`);const u=(l.thread_ids||[]).map(m=>{const f=_fatesThreadsById[m],g=(_hooksAllThreads||[]).find(h=>h.id===m);return{id:m,title:f?.title||g?.title||"(untitled)",status:g?.status||f?.status||"warm",heat_score:g?.heat_score??f?.heat_score,touchpoints:f?.touchpoints||[]}}),p=(l.shared_entities||[]).map(m=>m.id);if(e.innerHTML=_renderLoomTable(u,p,{selectable:!1,showHeat:!1,showActions:!1,showEntitySnip:!0,compact:!0}),t){const m=l.shared_entities||[],f=m.length===1?m[0].name:m.length===2?`${m[0].name} + ${m[1].name}`:`${m[0].name}, ${m[1].name} +${m.length-2} more`;t.textContent=`${l.thread_count} thread${l.thread_count===1?"":"s"} woven by ${f}`}o&&(o.disabled=!1,o.dataset.weaveThreads=(l.thread_ids||[]).join(",")),s&&(s.disabled=d<=1),i&&(i.disabled=d<=1),_renderWeaveSynthesisFor(l,n)}function _weaveSynthesisKey(e){return[...e.thread_ids||[]].map(t=>parseInt(t,10)).sort((t,n)=>t-n).join(",")}function _renderWeaveSynthesisFor(e,t){if(!t||!e)return;const n=_weaveSynthesisKey(e),a=_fatesSynthesisCache[n];if(a&&a.synthesis){t.innerHTML=`<span class="fates-suggestion-synthesis-label">
3083Possible scene</span><p class="fates-suggestion-synthesis-text">${escHtml(a.synthesis)}</p>`;return}if(a&&a.error){t.innerHTML=`<span class="fates-suggestion-synthesis-label fates-suggestion-synthesis-label--error">Synthesis failed</span><p class="fates-suggestion-synthesis-text fates-suggestion-synthesis-text--error">${escHtml(a.error)}</p>`;return}t.innerHTML='<span class="fates-suggestion-synthesis-label">Possible scene</span><p class="fates-suggestion-synthesis-text fates-suggestion-synthesis-text--loading">Synthesizing\u2026</p>',_fetchWeaveSynthesis(e)}async function _fetchWeaveSynthesis(e){if(!currentCampaign||!e)return;const t=_weaveSynthesisKey(e);if(_fatesSynthesisInflight[t])return _fatesSynthesisInflight[t];const n=(async()=>{try{const a=await api("POST",`/api/campaigns/${currentCampaign.id}/fates/weave-synthesis`,{thread_ids:e.thread_ids||[]});_fatesSynthesisCache[t]={synthesis:a.synthesis||"",error:null}}catch(a){_fatesSynthesisCache[t]={synthesis:"",error:a.message||"Unknown error"}}finally{delete _fatesSynthesisInflight[t];const a=document.getElementById("fates-suggestions-modal");if(a&&!a.classList.contains("hidden")){const s=_fatesWeaves[_fatesPossibleIdx];if(s&&_weaveSynthesisKey(s)===t){const i=document.getElementById("fates-possible-synthesis");_renderWeaveSynthesisFor(s,i)}}}})();return _fatesSynthesisInflight[t]=n,n}function _renderFatesPlayerWeave(){const e=document.getElementById("fates-player-section"),t=document.getElementById("fates-player-graph"),n=document.getElementById("fates-weave-thread-rail"),a=document.getElementById("fates-weave-entity-rail"),s=document.getElementById("fates-player-empty"),i=document.getElementById("fates-player-label"),o=document.getElementById("fates-weave-selected-btn"),r=document.getElementById("fates-player-clear");if(!e||!t||!n||!a)return;e.classList.remove("hidden");const c=_fatesSelected.size;i&&(i.textContent=c===0?"Weave Fates":`Weave Fates \xB7 ${c} loose thread${c===1?"":"s"}`),o&&(o.disabled=c<2,o.title=c<2?`Select ${2-c} more loose thread${2-c===1?"":"s"} to weave a Fate`:"Open the Weave Fate workspace with the selected Loose Threads"),r&&(r.disabled=c===0);const l=document.getElementById("fates-player-drag-hint");if(c===0){t.classList.add("hidden"),s&&s.classList.remove("hidden"),l&&l.classList.add("hidden"),n.innerHTML="",a.innerHTML="";const C=document.getElementById("fates-weave-ribbons");C&&(C.innerHTML="");return}t.classList.remove("hidden"),s&&s.classList.add("hidden"),l&&l.classList.remove("hidden");const d=(_hooksAllThreads||[]).filter(C=>_fatesSelected.has(C.id)).sort((C,S)=>(S.heat_score||0)-(C.heat_score||0)).map(C=>({id:C.id,title:C.title||"(untitled)",context:C.context||"",status:(C.status||"warm").toLowerCase(),heat_score:C.heat_score,touchpoints:(_fatesThreadsById[C.id]||{}).touchpoints&&(_fatesThreadsById[C.id]||{}).touchpoints.length?_fatesThreadsById[C.id].touchpoints:C.source_entity_ids||[]})),u=new Map;for(const C of d)for(const S of C.touchpoints)u.set(S,(u.get(S)||0)+1);const p=[...u.entries()].map(([C,S])=>({eid:C,cnt:S,e:_fatesEntityById[C]})).filter(C=>C.e).filter(C=>!_fatesHiddenEntitiesInWeave.has(C.eid)).sort((C,S)=>S.cnt!==C.cnt?S.cnt-C.cnt:(S.e.thread_count||0)-(C.e.thread_count||0)),m=p.filter(C=>C.cnt>=2),f=p.filter(C=>C.cnt<2),g=4,h=_fatesEntityRailExpanded?f:f.slice(0,g),v=f.length-h.length,b=[...m,...h].map(C=>C.eid),y=new Set(b),_=[..._fatesAddedEntities||[]].filter(C=>!y.has(C)).filter(C=>!_fatesHiddenEntitiesInWeave.has(C)).filter(C=>_fatesEntityById[C]||(allEntities||[]).some(S=>S.id===C)),w=[...b,..._],$=new Map;for(const C of m)$.set(C.eid,"anchor");for(const C of h)$.set(C.eid,"singleton");for(const C of _)$.set(C,"added");n.innerHTML=d.map(C=>{const S=C.heat_score!==null&&C.heat_score!==void 0?`<span class="weave-thread-heat" title="Heat score">${C.heat_score}</span>`:"",q=(C.context||"").trim(),I=q?` data-thread-tooltip-title="${escHtml(C.title)}" data-thread-tooltip-context="${escHtml(q)}"`:"";return`<div class="fates-weave-thread-row" data-thread-id="${C.id}"${I}><span class="thread-status-pill thread-status-pill--${C.status}">${escHtml(C.status)}</span><span class="weave-thread-title">${escHtml(C.title)}</span>`+S+`<button class="fates-row-action-btn fates-row-action-btn--snip" data-thread-snip="${C.id}" title="Remove from weave" aria-label="Remove from weave">\u2715</button></div>`}).join("");const E=w.map(C=>{const S=_fatesEntityById[C],q=$.get(C)||"anchor",I=S.is_hub?" fates-warp-head--hub":"",T=`${(S.type||"").toUpperCase()} \xB7 in ${S.thread_count||0} loose thread${(S.thread_count||0)===1?"":"s"} campaign-wide${S.is_hub?" (hub)":""}`,P=u.get(C)||0,B=P>=2?` \xB7 anchors ${P}
3083 of these Loose Threads`:P===1?" \xB7 touches 1 of these Loose Threads":" \xB7 added by you",H=T+B,A=q==="added"?'<span class="fates-weave-entity-added" title="Added by you via the picker">+</span>':"";return`<div class="fates-weave-entity-row fates-weave-entity-row--${q}" data-entity-id="${C}" draggable="true" title="Drag onto a thread to link them">`+A+`<span class="fates-warp-head fates-warp-head--${S.type}${I}" title="${escHtml(H)}">`+escHtml(S.name)+`</span><button class="fates-row-action-btn fates-row-action-btn--snip" data-entity-snip-weave="${C}" title="Remove from the Weave (drops from AI input too)" aria-label="Remove from weave">\u2715</button></div>`}).join("");let k="";if(v>0?k=`<button class="fates-weave-entity-expander" id="fates-weave-entity-expander"><span class="fates-weave-entity-expander-glyph">\u25BE</span> +${v} more touched (click to include in the AI input)</button>`:_fatesEntityRailExpanded&&f.length>g&&(k='<button class="fates-weave-entity-expander" id="fates-weave-entity-expander"><span class="fates-weave-entity-expander-glyph fates-weave-entity-expander-glyph--up">\u25BE</span> Show fewer</button>'),a.innerHTML=E+k,_fatesCurrentWeaveEntityIds=w,i&&w.length>0){const C=w.length;i.textContent=`Weave Fates \xB7 ${c} loose thread${c===1?"":"s"} \xB7 ${C} ${C===1?"entity":"entities"} \u2192 AI`}_renderFatesEntityPicker(w),requestAnimationFrame(()=>_renderWeaveRibbons(d,w)),_ensureWeaveResizeObserver()}let _fatesCurrentWeaveEntityIds=[];function _renderFatesEntityPicker(e){const t=document.getElementById("fates-entity-picker-list");if(!t)return;const n=typeof allEntities<"u"&&allEntities&&allEntities.length?allEntities:_fatesEntities||[];if(!n.length){t.innerHTML='<p class="placeholder fates-entity-picker-placeholder">No codex entities loaded yet.</p>';return}const a=new Set(e),s=_computeRecommendedEntities(e,n),i=new Set(s.map(m=>m.id)),o=["pc","npc","faction","item","location","companion","deity","lore","spell"],r={pc:"PCs",npc:"NPCs",faction:"Factions",item:"Items",location:"Locations",companion:"Companions",deity:"Deities",lore:"Lore",spell:"Spells"},c={};for(const m of n){const f=(m.type||"other").toLowerCase();c[f]||(c[f]=[]),c[f].push(m)}for(const m in c)c[m].sort((f,g)=>(f.name||"").localeCompare(g.name||""));const l=(_fatesPickerSearch||"").toLowerCase(),d=m=>!l||(m.name||"").toLowerCase().includes(l),u=[];!l&&s.length&&u.push(_renderPickerGroup("\u2605 Recommended",s,a));for(const m of o){if(!c[m])continue;const f=c[m].filter(d).filter(g=>l||!i.has(g.id));f.length&&u.push(_renderPickerGroup(r[m],f,a))}const p=Object.keys(c).filter(m=>!o.includes(m)).sort();for(const m of p){const f=c[m].filter(d).filter(g=>l||!i.has(g.id));if(f.length){const g=m.charAt(0).toUpperCase()+m.slice(1)+(m.endsWith("s")?"":"s");u.push(_renderPickerGroup(g,f,a))}}t.innerHTML=u.length?u.join(""):'<p class="placeholder fates-entity-picker-placeholder">No matches.</p>'}function _renderPickerGroup(e,t,n){const a=t.map(s=>{const i=n.has(s.id)?" checked":"";return`<label class="fates-entity-picker-row fates-entity-picker-row--${(s.type||"lore").toLowerCase()}" draggable="true" data-entity-id="${s.id}" title="Drag onto a thread to link them"><input type="checkbox" data-entity-id="${s.id}"${i} /><span class="fates-entity-picker-name">${escHtml(s.name||"(unnamed)")}</span></label>`}).join("");return`<div class="fates-entity-picker-group"><div class="form-section-label fates-entity-picker-eyebrow">${escHtml(e)}</div>`+a+"</div>"}function _computeRecommendedEntities(e,t){const n=t||(typeof allEntities<"u"?allEntities:null);if(!n||!n.length)return[];const a=new Set(e),s=n.filter(o=>a.has(o.id));if(!s.length)return[];const i=new Map;for(const o of s){const r=((o.description||"")+" "+(o.blurb||"")).toLowerCase();if(r.trim())for(const c of n){if(a.has(c.id)||c.id===o.id||_fatesHiddenEntitiesInWeave.has(c.id))continue;const l=(c.name||"").toLowerCase();l.length<=2||r.includes(l)&&i.set(c.id,(i.get(c.id)||0)+1)}}return[...i.entries()].sort((o,r)=>r[1]-o[1]).slice(0,8).map(([o])=>n.find(r=>r.id===o)).filter(Boolean)}function _renderWeaveRibbons(e,t){const n=document.getElementById("fates-weave-ribbons"),a=document.getElementById("fates-player-graph"),s=document.getElementById("fates-weave-thread-rail"),i=document.getElementById("fates-weave-entity-rail");if(!n||!a||!s||!i)return;const o=a.getBoundingClientRect();if(o.width<=0||o.height<=0)return;n.setAttribute("viewBox",`0 0 ${o.width} ${o.height}`);const r=s.getBoundingClientRect(),c=i.getBoundingClientRect(),l=r.right-o.left,d=c.left-o.left,u={};s.querySelectorAll(".fates-weave-thread-row").forEach(b=>{const y=b.getBoundingClientRect();u[b.dataset.threadId]=y.top+y.height/2-o.top});const p={};i.querySelectorAll(".fates-weave-entity-row").forEach(b=>{const y=b.getBoundingClientRect();p[b.dataset.entityId]=y.top+y.height/2-o.top});const f=(d-l)*.55,g={};for(const b of e){const y=new Set(b.touchpoints||[]);for(const _ of t)y.has(_)&&(g[_]=(g[_]||0)+1)}const h=b=>b>=4?"heavy":b===3?"bold":b===2?"medium":"thin";let v="";for(const b of e){const y=new Set(b.touchpoints||[]),_=u[b.id];if(_!==void 0)for(const w of t){if(!y.has(w))continue;const $=p[w];if($===void 0)continue;const E=_fatesEntityById[w],k=E&&E.type||"lore",C=h(g[w]||1),S=(l+f).toFixed(1),q=(d-f).toFixed(1),I=`M ${l.toFixed(1)} ${_.toFixed(1)} C ${S} ${_.toFixed(1)}, ${q} ${$.toFixed(1)}, ${d.toFixed(1)} ${$.toFixed(1)}`;v+=`<path class="fates-weave-ribbon fates-weave-ribbon--${k} fates-weave-ribbon--${C}" data-thread-id="${b.id}" data-entity-id="${w}" data-entity-type="${k}" data-weight-tier="${C}" d="${I}" fill="none" />`}}n.innerHTML=v}let _weaveResizeObserver=null;function _ensureWeaveResizeObserver(){if(_weaveResizeObserver)return;const e=document.getElementById("fates-player-graph");e&&(_weaveResizeObserver=new ResizeObserver(()=>{if(e.classList.contains("hidden")||!_fatesSelected.size)return;const t=(_hooksAllThreads||[]).filter(s=>_fatesSelected.has(s.id)).sort((s,i)=>(i.heat_score||0)-(s.heat_score||0)).map(s=>({id:s.id,status:(s.status||"warm").toLowerCase(),touchpoints:(_fatesThreadsById[s.id]||{}).touchpoints||[]})),n=document.getElementById("fates-weave-entity-rail"),a=n?Array.from(n.querySelectorAll(".fates-weave-entity-row")).map(s=>parseInt(s.dataset.entityId,10)).filter(s=>!isNaN(s)):[];_renderWeaveRibbons(t,a)}),_weaveResizeObserver.observe(e))}let _weaveHoverWired=!1;function _ensureWeaveHoverWired(){if(_weaveHoverWired)return;const e=document.getElementById("fates-player-graph");e&&(e.addEventListener("mouseover",t=>{const n=t.target.closest("[data-thread-id], [data-entity-id]");if(!n||t.target.closest(".fates-row-action-btn--snip"))return;const a=n.dataset.threadId||null,s=n.dataset.entityId||null;e.classList.add("is-highlighting"),e.querySelectorAll(".fates-weave-ribbon").forEach(i=>
3083{const o=a&&i.dataset.threadId===a,r=s&&i.dataset.entityId===s,c=o||r;i.classList.toggle("is-active",!!c),i.classList.toggle("is-dim",!c)})}),e.addEventListener("mouseout",t=>{e.contains(t.relatedTarget)||(e.classList.remove("is-highlighting"),e.querySelectorAll(".fates-weave-ribbon").forEach(n=>{n.classList.remove("is-active"),n.classList.remove("is-dim")}))}),_weaveHoverWired=!0)}function _gmWeaveEntitySet(){const e=new Set;for(const t of _fatesSelected){const n=_fatesThreadsById[t];if(!(!n||!n.touchpoints))for(const a of n.touchpoints)e.add(a)}return e}document.getElementById("fates-player-graph")?.addEventListener("click",e=>{const t=e.target.closest("[data-thread-snip]"),n=e.target.closest("[data-entity-snip-weave]");if(!t&&!n)return;if(e.preventDefault(),e.stopPropagation(),t){const s=parseInt(t.dataset.threadSnip,10);if(isNaN(s)||!_fatesSelected.has(s))return;_fatesSelected.delete(s),_renderFatesPlayerWeave(),_renderThreadsBody();return}const a=parseInt(n.dataset.entitySnipWeave,10);isNaN(a)||(_fatesHiddenEntitiesInWeave.add(a),_renderFatesPlayerWeave())}),_ensureWeaveHoverWired(),document.getElementById("fates-weave-entity-rail")?.addEventListener("click",e=>{e.target.closest("#fates-weave-entity-expander")&&(e.preventDefault(),e.stopPropagation(),_fatesEntityRailExpanded=!_fatesEntityRailExpanded,_renderFatesPlayerWeave())}),document.getElementById("fates-entity-picker-search")?.addEventListener("input",e=>{_fatesPickerSearch=(e.target.value||"").trim(),_renderFatesEntityPicker(_fatesCurrentWeaveEntityIds)});let _fatesPendingLink={entityId:null,threadId:null};document.addEventListener("dragstart",e=>{const t=e.target.closest('[data-entity-id][draggable="true"]');if(!t)return;const n=parseInt(t.dataset.entityId,10);isNaN(n)||(e.dataTransfer.setData("text/x-fates-entity-id",String(n)),e.dataTransfer.effectAllowed="link",document.body.classList.add("is-fates-dragging"))}),document.addEventListener("dragend",()=>{document.body.classList.remove("is-fates-dragging"),document.querySelectorAll(".fates-weave-thread-row.is-drop-target").forEach(e=>e.classList.remove("is-drop-target"))}),document.getElementById("fates-weave-thread-rail")?.addEventListener("dragover",e=>{const t=e.target.closest(".fates-weave-thread-row[data-thread-id]");t&&Array.from(e.dataTransfer.types).includes("text/x-fates-entity-id")&&(e.preventDefault(),e.dataTransfer.dropEffect="link",t.classList.add("is-drop-target"))}),document.getElementById("fates-weave-thread-rail")?.addEventListener("dragleave",e=>{const t=e.target.closest(".fates-weave-thread-row[data-thread-id]");t&&(e.relatedTarget&&t.contains(e.relatedTarget)||t.classList.remove("is-drop-target"))}),document.getElementById("fates-weave-thread-rail")?.addEventListener("drop",e=>{const t=e.target.closest(".fates-weave-thread-row[data-thread-id]");if(!t)return;e.preventDefault(),t.classList.remove("is-drop-target");const n=parseInt(e.dataTransfer.getData("text/x-fates-entity-id"),10),a=parseInt(t.dataset.threadId,10);isNaN(n)||isNaN(a)||_openManualLinkModal(n,a)});function _openManualLinkModal(e,t){const n=document.getElementById("fates-manual-link-modal");if(!n)return;const a=_fatesEntityById[e]||(typeof allEntities<"u"&&allEntities?allEntities.find(c=>c.id===e):null),s=(_hooksAllThreads||[]).find(c=>c.id===t);if(!a||!s){showToast("Couldn't resolve link, entity or thread missing.");return}const i=document.getElementById("fates-manual-link-entity"),o=document.getElementById("fates-manual-link-thread"),r=document.getElementById("fates-manual-link-note");i&&(i.textContent=a.name||"(unnamed)"),o&&(o.textContent=s.title||"(untitled)"),r&&(r.value=""),_fatesPendingLink={entityId:e,threadId:t},n.classList.remove("hidden"),setTimeout(()=>r?.focus(),60)}function _closeManualLinkModal(){document.getElementById("fates-manual-link-modal")?.classList.add("hidden"),_fatesPendingLink={entityId:null,threadId:null}}document.getElementById("fates-manual-link-close")?.addEventListener("click",_closeManualLinkModal),document.getElementById("fates-manual-link-cancel")?.addEventListener("click",_closeManualLinkModal),document.getElementById("fates-manual-link-modal")?.addEventListener("click",e=>{e.target.id==="fates-manual-link-modal"&&_closeManualLinkModal()}),document.getElementById("fates-manual-link-save")?.addEventListener("click",async()=>{const e=document.getElementById("fates-manual-link-note"),t=(e?.value||"").trim();if(!t){e?.focus();return}
3083const{entityId:n,threadId:a}=_fatesPendingLink;if(!n||!a||!currentCampaign)return;const s=document.getElementById("fates-manual-link-save");s&&(s.disabled=!0,s.textContent="Saving\u2026");try{await api("POST",`/api/campaigns/${currentCampaign.id}/threads/${a}/manual-link`,{entity_id:n,note:t}),_closeManualLinkModal(),showToast("Link saved (GM-only).",2500),await loadThreads()}catch(i){showToast("Save failed: "+(i.message||"Unknown"))}finally{s&&(s.disabled=!1,s.textContent="Save link")}}),document.getElementById("fates-entity-picker-list")?.addEventListener("change",e=>{const t=e.target.closest('input[type="checkbox"][data-entity-id]');if(!t)return;const n=parseInt(t.dataset.entityId,10);isNaN(n)||(t.checked?(_fatesHiddenEntitiesInWeave.delete(n),_fatesAddedEntities.add(n)):(_fatesAddedEntities.delete(n),_fatesHiddenEntitiesInWeave.add(n)),_renderFatesPlayerWeave())});let _threadTooltipShowTimer=null,_threadTooltipHideTimer=null;const _THREAD_TOOLTIP_SHOW_DELAY=180,_THREAD_TOOLTIP_HIDE_DELAY=80;function _positionThreadTooltip(e){const t=document.getElementById("fates-thread-tooltip");if(!t)return;const n=e.getBoundingClientRect(),a=t.getBoundingClientRect(),s=8,i=28,o=window.innerWidth,r=window.innerHeight;let c="right",l=n.right+i,d=n.top+n.height/2-a.height/2;if(l+a.width>o-s&&(c="left",l=n.left-i-a.width,l<s&&(c="below",l=n.left+n.width/2-a.width/2,d=n.bottom+i,d+a.height>r-s&&(c="above",d=n.top-i-a.height))),l=Math.max(s,Math.min(l,o-a.width-s)),d=Math.max(s,Math.min(d,r-a.height-s)),t.style.left=`${l}px`,t.style.top=`${d}px`,t.dataset.placement=c,c==="right"||c==="left"){const u=n.top+n.height/2;t.style.setProperty("--leader-y",`${u-d}px`),t.style.removeProperty("--leader-x")}else{const u=n.left+n.width/2;t.style.setProperty("--leader-x",`${u-l}px`),t.style.removeProperty("--leader-y")}}function _showThreadTooltip(e){const t=document.getElementById("fates-thread-tooltip");if(!t)return;const n=e.dataset.threadTooltipTitle||"",a=e.dataset.threadTooltipContext||"";a&&(t.innerHTML=(n?`<div class="fates-thread-tooltip-title">${escHtml(n)}</div>`:"")+`<div class="fates-thread-tooltip-context">${escHtml(a)}</div>`,t.classList.remove("hidden"),t.setAttribute("aria-hidden","false"),_positionThreadTooltip(e))}function _hideThreadTooltip(){const e=document.getElementById("fates-thread-tooltip");e&&(e.classList.add("hidden"),e.setAttribute("aria-hidden","true"))}document.addEventListener("mouseover",e=>{const t=e.target.closest("[data-thread-tooltip-context]");t&&(e.target.closest(".fates-row-action-btn, .thread-select")||(clearTimeout(_threadTooltipHideTimer),clearTimeout(_threadTooltipShowTimer),_threadTooltipShowTimer=setTimeout(()=>_showThreadTooltip(t),_THREAD_TOOLTIP_SHOW_DELAY)))}),document.addEventListener("mouseout",e=>{const t=e.target.closest("[data-thread-tooltip-context]");t&&(t.contains(e.relatedTarget)||(clearTimeout(_threadTooltipShowTimer),clearTimeout(_threadTooltipHideTimer),_threadTooltipHideTimer=setTimeout(_hideThreadTooltip,_THREAD_TOOLTIP_HIDE_DELAY)))}),window.addEventListener("scroll",_hideThreadTooltip,{passive:!0}),window.addEventListener("resize",_hideThreadTooltip);function hideAllHoverPopups(){try{clearTimeout(_threadTooltipShowTimer),clearTimeout(_threadTooltipHideTimer)}catch{}try{_hideThreadTooltip()}catch{}try{_chr2HideStatPop()}catch{}try{_anthHideEntityPreview()}catch{}}document.addEventListener("pointerdown",hideAllHoverPopups,!0),document.addEventListener("click",hideAllHoverPopups,!0),document.addEventListener("keydown",e=>{e.key==="Escape"&&hideAllHoverPopups()},!0);function _toggleThreadSelection(e){_fatesSelected.has(e)?_fatesSelected.delete(e):_fatesSelected.add(e),_renderFatesPlayerWeave(),_renderThreadsBody(),_fatesPossibleIdx=0,_renderFatesPossible()}function _selectClusterThreads(e){_fatesSelected=new Set(e),_renderThreadsBody(),_renderFatesPlayerWeave(),_fatesPossibleIdx=0,_renderFatesPossible(),document.getElementById("fates-player-section")?.scrollIntoView({behavior:"smooth",block:"start"})}function _applyHooksFilters(e){let t=e.filter(s=>{const i=(s.user_status||"").toLowerCase();
3083return _hooksMode==="active"?i!=="dismissed":_hooksMode==="dismissed"?i==="dismissed":!0});_hooksTypeFilter&&(t=t.filter(s=>(s.status||"").toLowerCase()===_hooksTypeFilter));const n=_hooksSearch.trim().toLowerCase();n&&(t=t.filter(s=>(s.title||"").toLowerCase().includes(n)||(s.context||"").toLowerCase().includes(n)));const a={hot:4,warm:3,cold:2,dormant:1,resolved:0};return t.sort((s,i)=>{if(_hooksSort==="heat"){const o=a[(s.status||"").toLowerCase()]??-1,r=a[(i.status||"").toLowerCase()]??-1;return r!==o?r-o:(i.heat_score||0)-(s.heat_score||0)}return _hooksSort==="newest"?(i.id||0)-(s.id||0):_hooksSort==="oldest"?(s.id||0)-(i.id||0):_hooksSort==="alpha"?(s.title||"").localeCompare(i.title||""):0}),t}function _renderThreadsDetectingState(e){const t=document.getElementById("threads-body");t&&(t.classList.remove("fates-loom-table"),t.innerHTML=`<div class="thread-upgrade-banner"><span class="form-section-label"><span class="threads-spinner">\u21BB</span> Detecting</span><p class="thread-upgrade-body">${escHtml(e)}</p></div>`)}async function _triggerDetectAndPoll(){const e=_tfThreadsLastTotal,t=currentCampaign?.id;try{await api("POST",`/api/campaigns/${currentCampaign.id}/threads/detect`,{})}catch(n){const a=document.getElementById("threads-body");n.message&&/429/.test(n.message)?a&&(a.innerHTML='<p class="placeholder placeholder--padded">Detection rate limit reached (3/day). Try again tomorrow.</p>'):a&&(a.innerHTML=`<p class="placeholder placeholder--padded error">Detection failed: ${escHtml(n.message||"Unknown")}</p>`);return}setTimeout(async()=>{await loadThreads(),currentCampaign?.id===t&&_tfThreadsLastTotal<=e&&showToast("Detection still running, refresh in a minute.",8e3)},18e3)}function _renderFatesStats(){const e=document.getElementById("fates-stats");if(!e)return;const t=Array.isArray(_hooksAllThreads)?_hooksAllThreads:[],n=!!(_hooksLastResponseMeta&&_hooksLastResponseMeta.preview),a=d=>(d.user_status||"").toLowerCase(),s=t.filter(d=>a(d)==="dismissed").length,i=Array.isArray(_hooksResolvedThreads)?_hooksResolvedThreads.length:0,o=n?_hooksLastResponseMeta.total||0:t.filter(d=>a(d)!=="dismissed").length,r=Array.isArray(_fatesSavedList)?_fatesSavedList.length:0,c=(d,u)=>`<span><b>${d}</b> ${u}</span>`,l=[c(r,r===1?"Fate":"Fates"),c(o,o===1?"Loose Thread":"Loose Threads")];n||(l.push(c(i,"Resolved")),l.push(c(s,"Dismissed"))),e.innerHTML=l.join('<span class="dot"></span>')}function _renderThreadsBody(){const e=document.getElementById("threads-body");if(!e)return;_renderFatesStats();const{total:t,preview:n}=_hooksLastResponseMeta,a=document.querySelector("#tab-threads .compendium-toolbar");if(a&&a.classList.toggle("hidden",!!n),n){e.classList.remove("fates-loom-table"),e.innerHTML=_renderPreviewBanner(t)+_renderPreviewCards(_hooksAllThreads,t);return}const s=document.getElementById("threads-detect-btn");s&&s.classList.remove("hidden");const i=_applyHooksFilters(_hooksAllThreads);if(!i.length){e.classList.remove("fates-loom-table"),e.innerHTML=`<p class="placeholder placeholder--inline">${_hooksEmptyMessage()}</p>`;return}const o=_pickThreadsTableColumns(i,8),r=_gmWeaveEntitySet();e.classList.add("fates-loom-table"),e.innerHTML=_renderLoomTable(i,o,{selectable:!0,showHeat:!0,showActions:!0,compact:!1,weaveAdjacency:r}),_wireThreadCardActions()}function _hooksEmptyMessage(){return _hooksSearch.trim()?"No Loose Threads match the search.":_hooksTypeFilter?`No ${escHtml(_hooksTypeFilter)} Loose Threads${_hooksMode==="dismissed"?" in dismissed":""}.`:_hooksMode==="dismissed"?"No dismissed Loose Threads.":_hooksAllThreads.length?"No Loose Threads match the current filter.":"No Loose Threads yet; they appear automatically as you recap sessions, or click <strong>\uFF0B Add Loose Thread</strong> to create your own."}function _renderPreviewBanner(e){const t=e>0?`These two Loose Threads are on the house. Threadfall found ${e} in your campaign.`:"Fates surfaces unresolved narrative threads from your campaign as sessions accumulate.";return`<div class="thread-upgrade-banner"><span class="form-section-label">Fates: The Prep Suite</span><p class="thread-upgrade-body">${escHtml(t)} Standard keeps them all in view, with dismissals and detection on demand.</p><button class="btn-new" id="threads-upgrade-btn">See Standard</button></div>`}function _renderPreviewCards(e,t){if(!e.length)return"";const n=e.slice(0,2).map(i=>_renderThreadCard(i,!1,!0)).join(""),a=Math.max(0,Math.min(3,t-e.length)),s=Array.from({length:a},()=>'<div class="thread-card thread-card--locked"><div class="thread-card-hdr"><span class="thread-status-pill thread-status-pill--warm">warm</span><span class="thread-card-title">Locked thread</span></div><p class="thread-context">Standard reveals the rest.</p></div>').join("");return n+s}const _TF_PROVENANCE_SURFACE={open_quest:"Quests",pc_backstory:"Quests",absent_npc:"Codex",unused_item:"Codex",faction_dormancy:"Codex",foreshadowing:"Sessions",player_intent:"Sessions"};function _buildProvenanceLine(e){const t=_TF_PROVENANCE_SURFACE[e.source_type]||"Source",n=(e.source_entity_ids||[])[0],a=n?(allEntities||[]).find(l=>l.id===n):null,s=a?`<button class="thread-prov-entity entity-link" data-id="${a.id}" data-type="${a.type}">${escHtml(a.name)}</button>`:"";let i="";const o=e.last_referenced_session_number!=null?e.last_referenced_session_number:e.last_referenced_session_id;if(e.last_referenced_session_id){let l;e.source_type==="open_quest"||e.source_type==="pc_backstory"?l=`introduced Session ${o}`:e.source_type==="foreshadowing"?l=`flagged Session ${o}`:l=`last seen Session ${o}`,i=`<span class="thread-prov-ref">${escHtml(l)}</span>`}else e.source_type==="unused_item"&&(i='<span class="thread-prov-ref">never referenced in play</span>');const r=`<span class="thread-prov-surface">${escHtml(t)}</span>`;let c=r;return s&&(c+='<span class="thread-prov-sep"> \u203A </span>'+s),i&&(c+=(s||r?'<span class="thread-prov-sep"> \xB7 </span>':"")+i),`<div class="thread-provenance">${c}</div>`}function _renderThreadCard(e,t,n){const a=e.status||"warm",s=e.heat_score===null||e.heat_score===void 0?"":`<span class="thread-heat-score" title="Heat score">${e.heat_score}</span>`,i=(e.source_entity_ids||[]).slice(1).map(d=>{const u=(allEntities||[]).find(p=>p.id===d);return u?`<button class="thread-entity-chip entity-link" data-id="${u.id}" data-type="${u.type}">${escHtml(u.name)}
3083</button>`:""}).filter(Boolean).join("");if(n)return`<div class="thread-card" data-thread-id="${e.id}"><div class="thread-card-hdr"><span class="thread-status-pill thread-status-pill--${a}">${a}</span><span class="thread-card-title">${escHtml(e.title||"")}</span></div></div>`;const o=t?"":`<div class="thread-card-actions"><button class="btn-new" data-thread-action="dismissed" data-thread-id="${e.id}" title="Hide this thread (sticky)">Dismiss</button><button class="btn-new" data-thread-action="resolved" data-thread-id="${e.id}" title="Mark as resolved">Mark resolved</button></div>`;let r="";if(!t){const d=_fatesNeighbors[e.id]||[],u=[],p=new Set;for(const m of d)for(const f of m.shared_entity_names||[])p.has(f.toLowerCase())||(p.add(f.toLowerCase()),u.push(f));u.length&&(r='<div class="thread-shared-tags"><span class="thread-shared-label">shares:</span>'+u.slice(0,4).map(f=>`<span class="thread-shared-tag">${escHtml(f)}</span>`).join("")+"</div>")}const c=_fatesSelected.has(e.id),l=t?"":`<label class="thread-select" title="Add to weave"><input type="checkbox" class="thread-select-cb" data-thread-id="${e.id}"${c?" checked":""} /></label>`;return`<div class="thread-card${t?" thread-card--locked":""}${c?" thread-card--selected":""}" data-thread-id="${e.id}"><div class="thread-card-hdr">`+l+`<span class="thread-status-pill thread-status-pill--${a}">${a}</span><span class="thread-card-title">${escHtml(e.title||"")}</span>`+s+"</div>"+(e.context?`<p class="thread-context">${escHtml(e.context)}</p>`:"")+_buildProvenanceLine(e)+r+(i?`<div class="thread-entity-chips">${i}</div>`:"")+o+"</div>"}function _wireThreadCardActions(){document.querySelectorAll("[data-thread-action]").forEach(e=>{e.addEventListener("click",async t=>{t.preventDefault();const n=e.dataset.threadId,a=e.dataset.threadAction;try{await api("PATCH",`/api/campaigns/${currentCampaign.id}/threads/${n}`,{user_status:a}),await loadThreads()}catch(s){showToast("Update failed: "+(s.message||"Unknown"))}})}),document.querySelectorAll("[data-thread-brainstorm]").forEach(e=>{e.addEventListener("click",t=>{t.preventDefault();const n=parseInt(e.dataset.threadBrainstorm,10);isNaN(n)||openBrainstormModal(n)})}),document.getElementById("threads-upgrade-btn")?.addEventListener("click",()=>{showToast("Upgrade coming soon, see your account page for billing once available.")})}async function _threadsDetectNow(){if(!currentCampaign||!_tfRequireFeature("loose_threads"))return;const e=document.getElementById("threads-detect-btn");e&&(e.disabled=!0,e.textContent="\u21BB Detecting\u2026"),_tfHooksAutoDetected.add(currentCampaign.id),_renderThreadsDetectingState("Detecting threads\u2026 this takes about 15 seconds.");try{await _triggerDetectAndPoll()}catch(t){if(t&&t.status===409){showToast(t.message||"Loose Threads detection already running, wait for it to finish.",5e3);try{const n=await api("GET",`/api/campaigns/${currentCampaign.id}`);Object.assign(currentCampaign,n)}catch{}}else if(t)throw t}e&&setTimeout(()=>{e.disabled=!1,e.textContent="\u21BA Detect Now"},18e3),_maybeStartCampaignAiPoll()}document.getElementById("threads-detect-btn")?.addEventListener("click",_threadsDetectNow);function _openAddThreadModal(){const e=document.getElementById("threads-add-modal");if(!e)return;const t=document.getElementById("threads-add-title-input"),n=document.getElementById("threads-add-context-input"),a=document.getElementById("threads-add-status-input"),s=document.getElementById("threads-add-heat-input"),i=document.getElementById("threads-add-error");t&&(t.value=""),n&&(n.value=""),a&&(a.value="warm"),s&&(s.value=""),i&&(i.textContent="",i.classList.add("hidden")),e.classList.remove("hidden"),setTimeout(()=>t?.focus(),0)}function _closeAddThreadModal(){document.getElementById("threads-add-modal")?.classList.add("hidden")}async function _submitAddThread(){if(!currentCampaign)return;const e=document.getElementById("threads-add-title-input"),t=document.getElementById("threads-add-context-input"),n=document.getElementById("threads-add-status-input"),a=document.getElementById("threads-add-heat-input"),s=document.getElementById("threads-add-error"),i=document.getElementById("threads-add-save"),o=(e?.value||"").trim();if(!o){s&&(s.textContent="Title is required.",s.classList.remove("hidden")),e?.focus();return}const r={title:o,status:n?.value||"warm"},c=(t?.value||"").trim();c&&(r.context=c);const l=(a?.value||"").trim();if(l!==""){const d=parseInt(l,10);if(Number.isNaN(d)||d<0||d>100){s&&(s.textContent="Heat must be a number between 0 and 100.",s.classList.remove("hidden")),a?.focus();return}r.heat_score=d}i&&(i.disabled=!0,i.textContent="Saving\u2026");try{await api("POST",`/api/campaigns/${currentCampaign.id}/threads`,r),_closeAddThreadModal(),await loadThreads(),showToast("Loose Thread added.")}catch(d){s&&(s.textContent=`Couldn't save: ${d.message||"Unknown error"}`,s.classList.remove("hidden"))}finally{i&&(i.disabled=!1,i.textContent="\uFF0B Add Loose Thread")}}document.getElementById("threads-add-btn")?.addEventListener("click",_openAddThreadModal),document.getElementById("threads-add-close")?.addEventListener("click",_closeAddThreadModal),document.getElementById("threads-add-cancel")?.addEventListener("click",_closeAddThreadModal),document.getElementById("threads-add-save")?.addEventListener("click",_submitAddThread),document.getElementById("threads-add-modal")?.addEventListener("click",e=>{e.target.id==="threads-add-modal"&&_closeAddThreadModal()}),document.getElementById("threads-add-title-input")?.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),_submitAddThread())});let _hooksSearchTimer;document.getElementById("hooks-search")?.addEventListener("input",e=>{_hooksSearch=e.target.value,clearTimeout(_hooksSearchTimer),_hooksSearchTimer=setTimeout(_renderThreadsBody,150)}),document.getElementById("hooks-sort-btn")?.addEventListener("click",()=>{const e=_HOOK_SORT_CYCLE.indexOf(_hooksSort);_hooksSort=_HOOK_SORT_CYCLE[(e+1)%_HOOK_SORT_CYCLE.length],document.getElementById("hooks-sort-btn").textContent=_HOOK_SORT_LABELS[_hooksSort],_renderThreadsBody()}),document.getElementById("hooks-mode-btn")?.addEventListener("click",()=>{const e=_HOOK_MODE_CYCLE.indexOf(_hooksMode);_hooksMode=_HOOK_MODE_CYCLE[(e+1)%_HOOK_MODE_CYCLE.length],document.getElementById("hooks-mode-btn").textContent=_HOOK_MODE_LABELS[_hooksMode],_renderThreadsBody()}),document.querySelectorAll("#hooks-type-filters .type-filter-btn").forEach(e
3083=>{e.addEventListener("click",()=>{document.querySelectorAll("#hooks-type-filters .type-filter-btn").forEach(t=>t.classList.remove("active")),e.classList.add("active"),_hooksTypeFilter=e.dataset.status||"",_renderThreadsBody()})}),document.getElementById("fates-suggestions-btn")?.addEventListener("click",()=>{if(_fatesWeavesState==="loading")return;const e=document.getElementById("fates-suggestions-modal");e&&(e.classList.remove("hidden"),_renderFatesSuggestionsModalBody())});function _closeFatesSuggestionsModal(){document.getElementById("fates-suggestions-modal")?.classList.add("hidden")}document.getElementById("fates-suggestions-close")?.addEventListener("click",_closeFatesSuggestionsModal),document.getElementById("fates-suggestions-modal")?.addEventListener("click",e=>{e.target.id==="fates-suggestions-modal"&&_closeFatesSuggestionsModal()}),document.getElementById("fates-possible-prev")?.addEventListener("click",e=>{e.stopPropagation();const t=_fatesVisibleWeaves().length;t&&(_fatesPossibleIdx=(_fatesPossibleIdx-1+t)%t,_renderFatesSuggestionsModalBody())}),document.getElementById("fates-possible-next")?.addEventListener("click",e=>{e.stopPropagation();const t=_fatesVisibleWeaves().length;t&&(_fatesPossibleIdx=(_fatesPossibleIdx+1)%t,_renderFatesSuggestionsModalBody())}),document.getElementById("fates-suggestions-add")?.addEventListener("click",e=>{const t=(e.currentTarget.dataset.weaveThreads||"").split(",").map(n=>parseInt(n,10)).filter(n=>!isNaN(n));t.length<2||(_selectClusterThreads(t),_closeFatesSuggestionsModal())}),document.getElementById("fates-player-clear")?.addEventListener("click",()=>{!_fatesSelected.size&&!_fatesHiddenEntitiesInWeave.size&&!_fatesAddedEntities.size||(_fatesSelected.clear(),_fatesHiddenEntitiesInWeave=new Set,_fatesAddedEntities=new Set,_fatesEntityRailExpanded=!1,_renderThreadsBody(),_renderFatesPlayerWeave(),_fatesPossibleIdx=0,_renderFatesPossible())}),document.getElementById("fates-possible-table")?.addEventListener("click",async e=>{const t=e.target.closest("[data-entity-snip]");if(!t||(e.preventDefault(),e.stopPropagation(),!currentCampaign))return;const n=parseInt(t.dataset.entitySnip,10);if(isNaN(n)||_fatesBlocklist.includes(n))return;t.disabled=!0,t.style.opacity="0.4";const a=[..._fatesBlocklist,n];try{await api("PUT",`/api/campaigns/${currentCampaign.id}/fates/setting-blocklist`,{entity_ids:a}),_fatesBlocklist=a,await loadFatesClusters()}catch(s){t.disabled=!1,t.style.opacity="",showToast("Failed to mark setting: "+(s.message||"Unknown"))}}),document.getElementById("threads-body")?.addEventListener("change",e=>{const t=e.target.closest(".thread-select-cb");if(!t)return;const n=parseInt(t.dataset.threadId,10);isNaN(n)||_toggleThreadSelection(n)});let _fatesSavedList=[],_savedFatesSelectedId=null,_fatesWorkspaceMode=null,_fatesWorkspaceState={threadIds:[],entityIds:[],premises:[],selectedPremise:null,scene:null,cost:0,savedFateId:null};async function _loadSavedFates(){if(currentCampaign){try{_fatesSavedList=(await api("GET",`/api/campaigns/${currentCampaign.id}/fates`)).fates||[]}catch{_fatesSavedList=[]}_renderSavedFates(),typeof _renderNotebookFates=="function"&&_renderNotebookFates()}}function _partyLabel(){return(currentCampaign&&currentCampaign.party_name?String(currentCampaign.party_name).trim():"")||"Party"}function _fateTarget(e){if(!e)return{kind:"party",name:_partyLabel()};const t=e.body&&e.body.target_entity_id||null;if(t){const o=(allEntities||[]).find(r=>r.id===t);if(o)return{kind:"pc",name:o.name}}const n=String(e.premise||"").toLowerCase();if(!n)return{kind:"party",name:_partyLabel()};const a=(allEntities||[]).filter(o=>o.type==="pc"&&!o.is_dead&&!o.is_inactive);let s=-1,i=null;for(const o of a){const r=String(o.name||"").toLowerCase().trim();if(r.length<2)continue;const c=new RegExp(`\\b${r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"i"),l=n.match(c);l&&(s===-1||l.index<s)&&(s=l.index,i=o)}return i?{kind:"pc",name:i.name}:{kind:"party",name:_partyLabel()}}function _renderSavedFates(){const e=document.getElementById("fates-saved-section"),t=document.getElementById("fates-saved-list"),n=document.getElementById("fates-saved-label"),a=document.getElementById("fates-saved-detail");if(!(!e||!t)){if(!_fatesSavedList.length){e.classList.remove("hidden"),n&&(n.textContent="Fates"),_renderFatesStats(),document.getElementById("fates-saved-layout")?.classList.add("hidden"),document.getElementById("fates-saved-empty")?.classList.remove("hidden"),t.innerHTML="",_savedFatesSelectedId=null,typeof _renderNotebookFates=="function"&&_renderNotebookFates();return}document.getElementById("fates-saved-layout")?.classList.remove("hidden"),document.getElementById("fates-saved-empty")?.classList.add("hidden"),e.classList.remove("hidden"),n&&(n.textContent=`Fates \xB7 ${_fatesSavedList.length} saved`),_renderFatesStats(),_savedFatesSelectedId!==null&&(_fatesSavedList.some(i=>i.id===_savedFatesSelectedId)||(_savedFatesSelectedId=null)),t.innerHTML=_fatesSavedList.map(s=>{const i=s.id===_savedFatesSelectedId,o=s.body||{},r=(o.foretelling_applicability||"").toLowerCase(),c=Array.isArray(o.foretelling)?o.foretelling:[];let l=0;if(r!=="none"&&c.length){const m=_loadPlantedSeeds(s.id);
3083c.forEach((f,g)=>{m.has(g)||l++})}const d=l>0?`<span class="fates-saved-card-count" title="${l} foreshadow${l===1?"":"s"} pending">${l}</span>`:"",u=_fateTarget(s),p=`<span class="fates-saved-card-pc" title="Target: ${escHtml(u.name)}">${escHtml(u.name)}</span>`;return`<button type="button" class="tf-card tf-card--sm tf-card--watermarked fates-saved-card${i?" is-selected":""}" data-fate-id="${s.id}"><div class="fates-saved-card-hdr"><span class="fates-saved-card-title">${escHtml(s.title||"Untitled Fate")}</span>`+p+d+"</div></button>"}).join(""),_renderSavedFatesDetail(),_applyTfAiGating(),typeof _renderNotebookFates=="function"&&_renderNotebookFates()}}function _renderSavedFatesDetail(){const e=document.getElementById("fates-saved-detail");if(!e)return;const t=(_fatesSavedList||[]).find(i=>i.id===_savedFatesSelectedId);if(!t){e.innerHTML='<p class="placeholder placeholder--inline">Select a Fate to view details.</p>';return}const n=(t.premise_type||"custom").toLowerCase(),a=n.charAt(0).toUpperCase()+n.slice(1),s=_fateTarget(t);e.innerHTML=`<div class="journey-summary-card tf-entering"><div class="journey-summary-hdr"><span class="fates-saved-card-type fates-saved-card-type--${n}">${escHtml(a)}</span><span class="journey-summary-title">${escHtml(t.title||"Untitled Fate")}</span><div class="quest-detail-meta-actions"><span class="fates-saved-card-pc fates-saved-detail-pc" title="Target: ${escHtml(s.name)}">${escHtml(s.name)}</span><button class="btn-ghost btn-xs" data-fate-edit="${t.id}">\u270E Edit</button><button class="btn-ghost btn-xs" data-fate-copy="${t.id}">Copy MD</button><button class="btn-ghost btn-xs btn-ghost--danger" data-fate-delete="${t.id}">Delete</button></div></div><div class="journey-summary-body">${_renderInlineFateScene(t)}</div></div>`}function _renderSeedsToPlant(){const e=document.getElementById("fates-seeds-section"),t=document.getElementById("fates-seeds-list"),n=document.getElementById("fates-seeds-label");if(!e||!t)return;const a=[];let s=0;for(const o of _fatesSavedList||[]){const r=o.body||{};if((r.foretelling_applicability||"").toLowerCase()==="none")continue;const l=Array.isArray(r.foretelling)?r.foretelling:[];if(!l.length)continue;const d=_loadPlantedSeeds(o.id),u=[];l.forEach((p,m)=>{d.has(m)||u.push({...p,idx:m})}),u.length&&(a.push({fate:o,pending:u}),s+=u.length)}if(!s){e.classList.add("hidden"),t.innerHTML="";return}e.classList.remove("hidden"),n&&(n.textContent=`Foreshadows \xB7 ${s} pending`);const i={same_session:0,late:1,mid:2,early:3};for(const o of a)o.pending.sort((r,c)=>(i[(r.placement||"mid").toLowerCase()]??9)-(i[(c.placement||"mid").toLowerCase()]??9));t.innerHTML=a.map(o=>{const r=o.fate.title||"Untitled Fate",c=(o.fate.premise_type||"custom").toLowerCase(),l=o.pending.map(d=>{const u=(d.placement||"mid").toLowerCase(),p=u==="same_session"?"this session":u;return`<li class="fates-foretelling-seed" data-fate-id="${o.fate.id}" data-seed-idx="${d.idx}"><label class="fates-foretelling-check"><input type="checkbox" data-seeds-fate-id="${o.fate.id}" data-seeds-idx="${d.idx}" aria-label="Mark foreshadow planted" /><span class="fates-foretelling-placement fates-foretelling-placement--${u}">${escHtml(p)}</span></label><div class="fates-foretelling-body"><p class="fates-foretelling-seed-text">${escHtml(d.seed||"")}</p>`+(d.plausible_cover?`<p class="fates-foretelling-cover"><span class="fates-foretelling-cover-label">If asked:</span> ${escHtml(d.plausible_cover)}</p>`:"")+"</div></li>"}).join("");return`<div class="fates-seeds-group"><div class="fates-seeds-group-hdr"><button class="fates-seeds-group-title" data-seeds-open-fate="${o.fate.id}" title="Open this Fate"><span class="fates-saved-card-type fates-saved-card-type--${c}">${escHtml(c)}</span><span class="fates-seeds-group-name">${escHtml(r)}</span><span class="fates-seeds-group-count">${o.pending.length} left</span></button></div><ul class="fates-foretelling-list">${l}</ul></div>`}).join("")}document.getElementById("fates-seeds-list")?.addEventListener("change",e=>{const t=e.target.closest("[data-seeds-fate-id]");if(!t)return;const n=parseInt(t.dataset.seedsFateId,10),a=parseInt(t.dataset.seedsIdx,10);if(Number.isNaN(n)||Number.isNaN(a))return;const s=_loadPlantedSeeds(n);t.checked?s.add(a):s.delete(a),_savePlantedSeeds(n,s),_renderSeedsToPlant(),_fatesWorkspaceState&&_fatesWorkspaceState.savedFateId===n&&!_fatesEditMode&&_renderSceneStage()}),document.getElementById("fates-seeds-list")?.addEventListener("click",e=>{const t=e.target.closest("[data-seeds-open-fate]");if(!t)return;const n=parseInt(t.dataset.seedsOpenFate,10);if(isNaN(n))return;const a=(_fatesSavedList||[]).find(s=>s.id===n);a&&_openWorkspaceForSavedFate(a)}),document.getElementById("fates-saved-list")?.addEventListener("click",e=>{const t=e.target.closest("[data-fate-id]");if(!t)return;const n=parseInt(t.dataset.fateId,10);isNaN(n)||(_savedFatesSelectedId=_savedFatesSelectedId===n?null:n,_renderSavedFates())}),document.getElementById("fates-saved-detail")?.addEventListener("click",async e=>{const t=e.target.closest("[data-fate-edit]");if(t){e.stopPropagation();const o=parseInt(t.dataset.fateEdit,10);if(isNaN(o))return;const r=_fatesSavedList.find(c=>c.id===o);if(!r)return;_openWorkspaceForSavedFate(r,!0);return}const n=e.target.closest("[data-fate-copy]");if(n){e.stopPropagation();const o=parseInt(n.dataset.fateCopy,10);if(isNaN(o))return;const r=_fatesSavedList.find(d=>d.id===o);if(!r)return;const c={type:r.premise_type,premise:r.premise,title:r.title},l=_fateAsMarkdown(r.body||{},c,r.thread_ids||[]);try{await navigator.clipboard.writeText(l),showToast("Fate copied as Markdown.",2200)}catch{showToast("Copy failed, clipboard permission denied.")}return}const a=e.target.closest("[data-fate-delete]");
3083if(a){if(e.stopPropagation(),a.disabled)return;const o=parseInt(a.dataset.fateDelete,10);if(isNaN(o))return;const r=_fatesSavedList.find(c=>c.id===o);if(!r)return;a.disabled=!0;try{if(!await confirmDialog({title:"Delete Fate",message:`Delete "${r.title||"this Fate"}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))return;try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/fates/${o}`),_savedFatesSelectedId===o&&(_savedFatesSelectedId=null),showToast("Fate deleted.",2e3),await _loadSavedFates()}catch(l){showToast("Delete failed: "+(l.message||"Unknown"))}}finally{a.disabled=!1}return}const s=e.target.closest("[data-fate-seed-delete-fid]");if(s){e.stopPropagation();const o=parseInt(s.dataset.fateSeedDeleteFid,10),r=parseInt(s.dataset.fateSeedDeleteIdx,10);if(isNaN(o)||isNaN(r))return;await _deleteFateForeshadowSeed(o,r);return}const i=e.target.closest("[data-fate-generate-foreshadow]");if(i){if(e.stopPropagation(),i.disabled)return;const o=parseInt(i.dataset.fateGenerateForeshadow,10);if(isNaN(o))return;await _generateFateForeshadow(o,i);return}}),document.getElementById("fates-saved-detail")?.addEventListener("change",e=>{const t=e.target.closest('input[type="checkbox"][data-ln-fate-id]');if(!t)return;const n=parseInt(t.dataset.lnFateId,10),a=parseInt(t.dataset.lnSeedIdx,10);if(Number.isNaN(n)||Number.isNaN(a))return;const s=_loadPlantedSeeds(n);t.checked?s.add(a):s.delete(a),_savePlantedSeeds(n,s),_renderSavedFatesDetail();const i=document.querySelector(`#fates-saved-list .fates-saved-card[data-fate-id="${n}"]`);if(i){const o=_fatesSavedList.find(r=>r.id===n);if(o){const r=o.body||{},c=Array.isArray(r.foretelling)?r.foretelling:[],l=_loadPlantedSeeds(n),d=c.filter((p,m)=>!l.has(m)).length,u=i.querySelector(".fates-saved-card-count");d>0?u?u.textContent=`${d}`:i.insertAdjacentHTML("beforeend",`<span class="fates-saved-card-count">${d}</span>`):u&&u.remove()}}typeof _renderNotebookFates=="function"&&_renderNotebookFates()});let _tableRulesData=null,_tableRulesLoadedCid=null;async function _loadTableRulesForNotebook(e){const t=currentCampaign?.id;if(!(t==null||!_isGm())&&!(!e&&_tableRulesLoadedCid===t&&_tableRulesData))try{const n=await api("GET",`/api/campaigns/${t}/table-rules`);if(_campaignStale(t))return;_tableRulesData=n,_tableRulesLoadedCid=t}catch{}}let _lnRulesExpanded=new Set,_lnRulesShowAll=!1;function _lnRuleKind(e){const t=(e||"").toLowerCase();return t.includes("veil")?"veil":t.includes("line")?"line":t==="ruling"?"ruling":"house"}function _tfTableRulesBullets(e){const t=[];let n=0;const a=(s,i)=>{let o=i,r=null;for(const c of(s||"").split(`
3084`)){if(!c.trim())continue;const l=/^(\s{2,}|\t+)-\s/.test(c),d=c.trim();if(!l&&/^[^-].{0,40}:$/.test(d)){o=d.slice(0,-1),r=null;continue}const u=d.replace(/^-\s*/,"");if(l&&r){(r.subs=r.subs||[]).push(u);continue}r={id:"b"+n++,kind:_lnRuleKind(o),text:u},t.push(r)}};a(e.lines_veils,"Lines"),a(e.house_rules,"House rule");for(const s of e.rulings||[])s.status==="kept"&&t.push({id:"r"+s.id,rid:s.id,kind:"ruling",text:s.ruling,quote:s.quote,status:s.status});return t}function _lnHi(e,t){let n=escHtml(e);for(const a of t||[]){const s=new RegExp("("+a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+")","ig");n=n.replace(s,'<mark class="ln-rule-hit">$1</mark>')}return n}function _lnRuleGlyph(e){return e==="line"?'<svg class="ln-rule-glyph ln-rule-glyph--line" aria-label="Line: never depicted"><use href="#tf-glyph-line"/></svg>':e==="veil"?'<svg class="ln-rule-glyph ln-rule-glyph--veil" aria-label="Veil: off-screen"><use href="#tf-glyph-veil"/></svg>':e==="house"?'<svg class="ln-rule-glyph ln-rule-glyph--house" aria-hidden="true"><use href="#tf-glyph-rule"/></svg>':'<span class="ln-rule-glyph" aria-hidden="true"></span>'}function _lnRuleRowHtml(e,t,n){const a=!!e.quote,s=a&&_lnRulesExpanded.has(e.id),i=a?`<button type="button" class="ln-rule-receipt-toggle" title="${s?"Hide":"Show"} the words as spoken" aria-expanded="${s}"><svg width="15" height="15" aria-hidden="true"><use href="#tf-glyph-quote"/></svg></button>`:"",o=e.status==="promoted"?' <span class="ln-rule-promoted">promoted</span>':"",r=i||n?`<span class="ln-rule-acts">${n||""}${i}</span>
3084`:"",c=s?`<div class="ln-rule-expanded"><span class="ln-rule-receipt">&ldquo;${_lnHi(e.quote,t)}&rdquo;</span></div>`:"",l=e.subs&&e.subs.length?`<span class="ln-rule-subs">${e.subs.map(d=>`<span class="ln-rule-sub">${_lnHi(d,t)}</span>`).join("")}</span>`:"";return`<div class="ln-rule-card${a?" ln-rule-card--has-receipt":""}${s?" ln-rule-card--expanded":""}" data-bid="${e.id}">
3085    <div class="ln-rule-row">${_lnRuleGlyph(e.kind)}<span class="ln-rule-text">${_lnHi(e.text,t)}${o}${l}</span>${r}</div>
3086    ${c}
3087  </div>`}function _lnRulesMatchesHtml(e,t){const n=t.toLowerCase().split(/\s+/).filter(Boolean),a=_tfTableRulesBullets(e).filter(s=>{const i=(s.kind+" "+s.text+" "+(s.subs||[]).join(" ")+" "+(s.quote||"")).toLowerCase();return n.every(o=>i.includes(o))});return a.length?a.map(s=>(s.quote&&n.some(i=>s.quote.toLowerCase().includes(i))&&_lnRulesExpanded.add(s.id),_lnRuleRowHtml(s,n))).join(""):'<p class="live-notebook-empty">Nothing on file matches.</p>'}function _lnRulesGroupedHtml(e){const t=_tfTableRulesBullets(e),n=t.filter(p=>p.kind==="line"||p.kind==="veil"),a=t.filter(p=>p.kind==="house"||p.kind==="ruling"),s=(e.rulings||[]).filter(p=>p.status==="proposed"),i=n.length?`<div class="ln-rules-lv"><span class="ln-rules-lv-lbl">Lines &amp; veils</span>${n.map(p=>_lnRuleRowHtml(p)).join("")}</div>`:"",o=s.length?'<div class="ln-rules-props">'+s.map(p=>`<div class="ln-rule-card ln-rule-card--proposed" data-rid="${p.id}">
3088        <div class="ln-rule-row"><span class="ln-rule-glyph" aria-hidden="true"></span>
3089          <span class="ln-rule-text">${escHtml(p.ruling)}<br><span class="ln-rule-receipt">&ldquo;${escHtml(p.quote||"")}&rdquo;</span></span>
3090          <span class="ln-rule-acts">
3091            <button type="button" class="btn-ghost btn-xs ln-rule-keep" data-rid="${p.id}" title="Keep this ruling">\u2713</button>
3092            <button type="button" class="btn-ghost btn-xs ln-rule-discard" data-rid="${p.id}" title="Not a ruling \u2014 discard">\u2715</button>
3093          </span>
3094        </div>
3095      </div>`).join("")+"</div>":"",r=7,c=_lnRulesShowAll?a:a.slice(0,r),l=a.length-c.length,d=p=>p.kind==="ruling"&&p.status==="kept"?`<button type="button" class="btn-ghost btn-xs ln-rule-promote" data-rid="${p.rid}" title="Promote into house rules">\u2191</button>`:"",u=c.length?'<span class="ln-rules-hr-lbl">House rules &amp; rulings</span>'+c.map(p=>_lnRuleRowHtml(p,null,d(p))).join("")+(l>0?`<button type="button" class="ln-thread-expand-btn" id="ln-rules-more-btn">\u25BE +${l} more</button>`:_lnRulesShowAll&&a.length>r?'<button type="button" class="ln-thread-expand-btn" id="ln-rules-more-btn">Show fewer</button>':""):"";return i+o+u+(!i&&!o&&!u?'<p class="live-notebook-empty">No table rules yet. Rulings you make on-mic are caught automatically; set lines &amp; veils on Session 0 or with \u270E Edit.</p>':"")+'<button type="button" class="btn-ghost btn-xs ln-rules-edit" id="ln-rules-edit-btn">\u270E Edit</button>'}function _renderNotebookRules(){const e=document.getElementById("ln-rules-body");if(!e)return;if(!_isGm()){e.innerHTML="",_setNotebookCount("rules",0);return}const t=_tableRulesData||{house_rules:"",lines_veils:"",rulings:[]},n=(t.rulings||[]).filter(i=>i.status==="proposed");e.innerHTML='<input type="text" class="ln-rules-search" id="ln-rules-search" placeholder="Find a rule or veil\u2026" autocomplete="off"><div class="ln-rules-content" id="ln-rules-content"></div>';const a=e.querySelector("#ln-rules-content"),s=i=>{a.innerHTML=i?_lnRulesMatchesHtml(t,i):_lnRulesGroupedHtml(t),_wireNotebookRules(a)};_lnRulesRepaint=()=>s((e.querySelector("#ln-rules-search")?.value||"").trim()),s(""),e.querySelector("#ln-rules-search").addEventListener("input",i=>s(i.target.value.trim())),_setNotebookCount("rules",n.length)}let _lnRulesRepaint=null;function _wireNotebookRules(e){const t=currentCampaign?.id,n=async(a,s,i)=>{i.disabled=!0;try{await api("POST",`/api/campaigns/${t}/table-rules/rulings/${a}`,{action:s}),await _loadTableRulesForNotebook(!0),_campaignStale(t)||_renderNotebookRules()}catch(o){i.disabled=!1,showToast("Failed: "+(o.message||"try again"))}};e.querySelectorAll(".ln-rule-keep").forEach(a=>
3095a.addEventListener("click",s=>{s.stopPropagation(),n(a.dataset.rid,"keep",a)})),e.querySelectorAll(".ln-rule-discard").forEach(a=>a.addEventListener("click",s=>{s.stopPropagation(),n(a.dataset.rid,"discard",a)})),e.querySelectorAll(".ln-rule-promote").forEach(a=>a.addEventListener("click",s=>{s.stopPropagation(),n(a.dataset.rid,"promote",a)})),e.querySelector("#ln-rules-edit-btn")?.addEventListener("click",a=>{a.stopPropagation(),_openTableRulesModal()}),e.querySelectorAll(".ln-rule-card--has-receipt .ln-rule-row").forEach(a=>a.addEventListener("click",s=>{if(s.target.closest(".ln-rule-keep, .ln-rule-discard, .ln-rule-promote"))return;s.stopPropagation();const i=a.closest(".ln-rule-card")?.dataset.bid;i&&(_lnRulesExpanded.has(i)?_lnRulesExpanded.delete(i):_lnRulesExpanded.add(i),_lnRulesRepaint&&_lnRulesRepaint())})),e.querySelector("#ln-rules-more-btn")?.addEventListener("click",a=>{a.stopPropagation(),_lnRulesShowAll=!_lnRulesShowAll,_lnRulesRepaint&&_lnRulesRepaint()})}function _openTableRulesModal(){const e=document.getElementById("table-rules-modal");if(!e)return;const t=_tableRulesData||{};document.getElementById("table-rules-lv").value=t.lines_veils||"",document.getElementById("table-rules-hr").value=t.house_rules||"",e.classList.remove("hidden")}(function(){const t=()=>document.getElementById("table-rules-modal")?.classList.add("hidden");document.getElementById("table-rules-close")?.addEventListener("click",t),document.getElementById("table-rules-cancel")?.addEventListener("click",t),document.getElementById("table-rules-save")?.addEventListener("click",async()=>{const n=currentCampaign?.id;if(n==null)return;const a=document.getElementById("table-rules-save");a.disabled=!0;try{await api("PUT",`/api/campaigns/${n}/table-rules`,{lines_veils:document.getElementById("table-rules-lv").value,house_rules:document.getElementById("table-rules-hr").value}),t(),await _loadTableRulesForNotebook(!0),_campaignStale(n)||_renderNotebookRules()}catch(s){showToast("Save failed: "+(s.message||"try again"))}a.disabled=!1})})();async function _loadNotebookData(){try{(!_fatesSavedList||!_fatesSavedList.length)&&await _loadSavedFates()}catch{}await _loadThreadsForNotebook(),await _loadTableRulesForNotebook(),_renderNotebookRecap(),_renderNotebookFates(),_renderNotebookThreads(),_renderNotebookRules()}async function _loadThreadsForNotebook(){if(currentCampaign&&!_tfThreadsLoaded)try{const e=`/api/campaigns/${currentCampaign.id}/threads`;_hooksAllThreads=(await api("GET",`${e}?status=hot,warm,cold,dormant,resolved`)).threads||[],_tfThreadsLoaded=!0}catch{}}function _renderNotebookThreads(){const e=document.getElementById("ln-threads-body");if(!e)return;const t=(_hooksAllThreads||[]).filter(o=>{const r=(o.status||"").toLowerCase(),c=(o.user_status||"").toLowerCase();return["hot","warm","cold"].includes(r)&&c!=="dismissed"}).sort((o,r)=>(r.heat_score||0)-(o.heat_score||0));if(!t.length){e.innerHTML='<p class="live-notebook-empty">No active Loose Threads. They appear automatically as you recap sessions.</p>',_setNotebookCount("threads",0);return}const n=7,a=_lnThreadsShowAll?t:t.slice(0,n),s=t.length-a.length;e.innerHTML=a.map(o=>{const r=(o.status||"warm").toLowerCase(),c=o.heat_score!==null&&o.heat_score!==void 0?o.heat_score:"",l=c!==""?`<span class="ln-thread-heat" title="Heat score">${c}</span>`:"",d=r==="hot"?"\u25CF":r==="warm"?"\u25D0":"\u25CB",u=_lnThreadsExpanded.has(o.id),p=(o.context||"").trim(),m=u?'<div class="ln-thread-expanded">'+(p?`<p class="ln-thread-context">${linkifyText(escHtml(p),allEntities,null)}</p>`:'<p class="ln-thread-context ln-thread-context--empty">No additional context.</p>')+"</div>":"";return`<div class="ln-thread-card${u?" ln-thread-card--expanded":""}" data-ln-thread-id="${o.id}"><div class="ln-thread-row"><span class="ln-thread-status ln-thread-status--${r}" title="${escHtml(r)}">${d}</span><span class="ln-thread-title">${escHtml(o.title||"(untitled thread)")}</span>`+l+"</div>"+m+"</div>"}).join("");let i="";s>0?i=`<button type="button" class="ln-thread-expand-btn" data-ln-thread-expand="show-all"><span class="ln-thread-expand-glyph">\u25BE</span> +${s} more</button>`:_lnThreadsShowAll&&t.length>n&&(i='<button type="button" class="ln-thread-expand-btn" data-ln-thread-expand="show-fewer"><span class="ln-thread-expand-glyph ln-thread-expand-glyph--up">\u25BE</span> Show fewer</button>'),e.innerHTML+=i,_setNotebookCount("threads",t.length)}document.getElementById("ln-threads-body")?.addEventListener("click",e=>{const t=e.target.closest("[data-ln-thread-expand]");if(t){e.stopPropagation(),_lnThreadsShowAll=t.dataset.lnThreadExpand==="show-all",_renderNotebookThreads();return}const n=e.target.closest("[data-ln-thread-id]");if(!n)return;const a=parseInt(n.dataset.lnThreadId,10);
3095isNaN(a)||(_lnThreadsExpanded.has(a)?_lnThreadsExpanded.delete(a):_lnThreadsExpanded.add(a),_renderNotebookThreads())});let _liveContextMentions=new Map,_liveContextDismissed=new Set,_liveContextExpanded=new Set,_liveContextPatterns=null;const _LIVE_CONTEXT_HALFLIFE_MS=20*60*1e3,_LIVE_CONTEXT_MIN_MENTIONS=2,_LIVE_CONTEXT_VISIBLE_CAP=8;let _liveContextScanIdx=0,_liveContextScanTimer=null,_liveContextRenderTick=0;function _resetLiveContextState(){_liveContextMentions=new Map,_liveContextDismissed=new Set,_liveContextExpanded=new Set,_liveContextPatterns=null,_liveContextScanIdx=0,_liveContextRenderTick=0,_lnFatesExpanded=new Set,_lnThreadsExpanded=new Set,_lnThreadsShowAll=!1}function _startLiveContextScan(){_stopLiveContextScan(),_liveContextScanTimer=setInterval(()=>{if(!liveSession){_stopLiveContextScan();return}const e=liveTranscriptSegments||[];for(;_liveContextScanIdx<e.length;){const t=e[_liveContextScanIdx++];t&&t.text&&_processSegmentForLiveContext(t.text,t.committedAt)}if(++_liveContextRenderTick>=30){_liveContextRenderTick=0;try{_liveContextMentions.size&&_renderNotebookLiveContext()}catch{}}},2e3)}function _stopLiveContextScan(){_liveContextScanTimer&&(clearInterval(_liveContextScanTimer),_liveContextScanTimer=null)}function _ensureLiveContextPatterns(){if(_liveContextPatterns)return _liveContextPatterns;const e=[];for(const t of allEntities||[]){const n=(t.name||"").trim();if(!(n.length<=2))try{e.push({pattern:new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"i"),entityId:t.id})}catch{}}return _liveContextPatterns=e,e}function _processSegmentForLiveContext(e,t){if(!e||!liveSession)return;const n=_ensureLiveContextPatterns();if(!n.length)return;const a=(liveRecordingStartTime||Date.now())+(t||0);let s=!1;for(const{pattern:i,entityId:o}of n)if(!_liveContextDismissed.has(o)&&i.test(e)){const r=_liveContextMentions.get(o)||[];r.length>=20&&r.shift(),r.push({text:e,ts:a}),_liveContextMentions.set(o,r),s=!0}s&&_renderNotebookLiveContext()}function _liveContextScore(e,t){return e.reduce((n,a)=>n+Math.pow(.5,(t-a.ts)/_LIVE_CONTEXT_HALFLIFE_MS),0)}function _renderNotebookLiveContext(){const e=document.getElementById("ln-context-body");if(!e)return;const t=Date.now(),n=[],a=new Set(["npc","faction","location"]);for(const[i,o]of _liveContextMentions){if(o.length<_LIVE_CONTEXT_MIN_MENTIONS||_liveContextDismissed.has(i))continue;const r=(allEntities||[]).find(c=>c.id===i);r&&(_hiddenFromViewer(r)||a.has((r.type||"npc").toLowerCase())&&n.push({entityId:i,ent:r,mentions:o,score:_liveContextScore(o,t)}))}if(!n.length){e.innerHTML='<p class="live-notebook-empty">NPCs, factions, and locations mentioned twice or more this session will surface here.</p>',_setNotebookCount("context",0);return}n.sort((i,o)=>o.score-i.score);const s=n.slice(0,_LIVE_CONTEXT_VISIBLE_CAP);e.innerHTML=s.map(({entityId:i,ent:o,mentions:r})=>{const c=(o.type||"npc").toLowerCase(),l=(o.description||"").replace(/^#{1,6}\s+.+\n?/gm,"").replace(/\*\*/g,"").trim(),d=l?`<p class="ln-context-snippet">${escHtml(l.slice(0,140))}${l.length>140?"\u2026":""}</p>`:"",u=thumbUrl(_entityImageUrl(o)),p=ENTITY_CFG[o.type]||ENTITY_CFG.location,m=u?`<img src="${u}" alt="${escHtml(o.name)}" class="ln-context-portrait" data-ln-context-preview="${i}" title="Open ${escHtml(o.name)} preview" />`:`<button type="button" class="ln-context-portrait ln-context-portrait--icon entity-icon entity-icon-tinted" style="--entity-color:${p.color}" data-ln-context-preview="${i}" title="Open ${escHtml(o.name)} preview">${entityGlyphRef(o.type,"")}</button>`;return`<div class="ln-context-card" data-ln-context-id="${i}">`+m+`<div class="ln-context-card-body"><div class="ln-context-card-hdr"><span class="fates-warp-head fates-warp-head--${c}" title="${escHtml(c)}">${escHtml(o.name)}</span><span class="ln-context-mentions" title="${r.length} mentions">${r.length}\xD7</span><button class="ln-context-dismiss" data-ln-context-dismiss="${i}" title="Hide for this session (false positive)">\u2715</button></div>`+d+"</div></div>"}).join(""),_setNotebookCount("context",n.length)}document.getElementById("ln-context-body")?.addEventListener("click",e=>{const t=e.target.closest("[data-ln-context-dismiss]");if(t){e.stopPropagation();const a=parseInt(t.dataset.lnContextDismiss,10);
3095Number.isNaN(a)||(_liveContextDismissed.add(a),_renderNotebookLiveContext());return}const n=e.target.closest("[data-ln-context-preview]");if(n){e.stopPropagation();const a=parseInt(n.dataset.lnContextPreview,10);!Number.isNaN(a)&&typeof _showEntityPreview=="function"&&_showEntityPreview(a)}});function _notebookRecapDismissKey(e){return`tf_notebook_recap_dismissed_${e}`}function _isNotebookRecapDismissed(){return!liveSession||!liveSession.id?!1:localStorage.getItem(_notebookRecapDismissKey(liveSession.id))==="1"}function _renderNotebookRecap(){const e=document.getElementById("live-preamble");e&&_isNotebookRecapDismissed()&&e.classList.add("hidden")}function _computeNotebookFatesData(){const e=[],t=[];for(const a of _fatesSavedList||[]){const s=a.body||{},i=(s.foretelling_applicability||"").toLowerCase(),o=Array.isArray(s.foretelling)?s.foretelling:[];let r=0;if(i!=="none"&&o.length){const c=_loadPlantedSeeds(a.id),l=[];o.forEach((d,u)=>{c.has(u)||l.push({...d,idx:u,fateId:a.id,fateTitle:a.title})}),r=l.length,t.push(...l)}e.push({fate:a,pendingCount:r})}const n={same_session:0,late:1,mid:2,early:3};return t.sort((a,s)=>(n[(a.placement||"mid").toLowerCase()]??9)-(n[(s.placement||"mid").toLowerCase()]??9)),e.sort((a,s)=>{const i=a.fate.created_at?new Date(a.fate.created_at).getTime():0;return(s.fate.created_at?new Date(s.fate.created_at).getTime():0)-i}),{openFates:e,pendingForetellings:t}}function _renderNotebookFates(){const e=_computeNotebookFatesData(),t=document.getElementById("ln-fates-body");t&&(e.openFates.length?t.innerHTML=e.openFates.map(({fate:n,pendingCount:a})=>{const s=(n.premise_type||"custom").toLowerCase(),i=_lnFatesExpanded.has(n.id),o=i?`<div class="ln-fate-expanded">${_renderInlineFateScene(n)}</div>`:"",r=a>0?`<span class="ln-fate-row-count">${a} pending</span>`:"";return`<div class="ln-fate-card${i?" ln-fate-card--expanded":""}" data-ln-fate-id="${n.id}"><div class="ln-fate-card-hdr"><span class="fates-saved-card-type fates-saved-card-type--${s}">${escHtml(s)}</span><span class="ln-fate-row-title">${escHtml(n.title||"Untitled Fate")}</span>`+r+`<button type="button" class="ln-fate-popout" data-ln-fate-popout="${n.id}" title="Open in workspace (edit / copy / delete)">\u2197</button></div>`+o+"</div>"}).join(""):t.innerHTML='<p class="live-notebook-empty">No saved Fates yet. Weave one from the Fates tab.</p>',_setNotebookCount("fates",e.openFates.length))}let _lnFatesExpanded=new Set,_lnThreadsExpanded=new Set,_lnThreadsShowAll=!1;function _renderInlineFateScene(e){const t=e.body||{},n={type:e.premise_type,premise:e.premise},a=t.gm_reality||t.what_players_dont_see,s=t.the_trigger||t.the_pivot,o=(Array.isArray(t.npc_moves)?t.npc_moves:[]).map(c=>{const l=(c.presence||"").toLowerCase(),d=l&&["in-scene","incoming","offstage"].includes(l)?` <span class="fates-scene-npc-presence fates-scene-npc-presence--${l}">${escHtml(l)}</span>`:"";return`<li><strong>${escHtml(c.who||"?")}</strong>${d}<span class="fates-scene-npc-sep">:</span> ${escHtml(c.what||"")}</li>`}).join(""),r=(c,l,d)=>l?`<section class="fates-scene-section${d?" fates-scene-section--secret":""}"><span class="form-section-label">${escHtml(c)}</span><p>${escHtml(l)}</p></section>`:"";return(n.premise?`<blockquote class="fates-scene-premise">${escHtml(n.premise)}</blockquote>`:"")+r("Opening beat",t.opening_beat)+r("What the players see",t.what_players_see)+r("Behind the screen",a,!0)+(o?`<section class="fates-scene-section"><span class="form-section-label">NPC moves</span><ul class="fates-scene-npc">${o}</ul></section>`:"")+r("The trigger",s)+r("Irreversible act",t.irreversible_act,!0)+_renderInlineFateForeshadow(e)+r("If the party engages",t.consequences_if_party_engages)+r("If the party walks",t.consequences_if_party_walks)}function _renderInlineFateForeshadow(e){const t=e.body||{},n=(t.foretelling_applicability||"").toLowerCase(),a=Array.isArray(t.foretelling)?t.foretelling:[],s=typeof _isGm=="function"?!!_isGm():!0,i=(t.foretelling_skipped_because||"").toLowerCase(),o=n==="none"&&i.startsWith("deferred");if(!a.length)return n==="none"&&!o||!s?"":`<section class="fates-scene-section ln-fate-foreshadow-section"><span class="form-section-label">Foreshadows</span><div class="fates-foreshadow-empty"><p class="form-helper-text">No foreshadows yet. Have the AI propose 3-5 seeds priming this scene's emotional core.</p><button type="button" class="btn-ghost btn-xs fates-generate-foreshadow-btn" data-fate-generate-foreshadow="${e.id}">Generate Foreshadows</button></div></section>`;
3095const r=_loadPlantedSeeds(e.id),c={same_session:0,late:1,mid:2,early:3},d=a.map((p,m)=>({...p,idx:m})).sort((p,m)=>(c[(p.placement||"mid").toLowerCase()]??9)-(c[(m.placement||"mid").toLowerCase()]??9)).map(p=>{const m=(p.placement||"mid").toLowerCase(),f=m==="same_session"?"this session":m,g=r.has(p.idx),h=s?`<button type="button" class="fates-foreshadow-row-delete" data-fate-seed-delete-fid="${e.id}" data-fate-seed-delete-idx="${p.idx}" title="Delete this foreshadow seed" aria-label="Delete this foreshadow seed">\u2715</button>`:"";return`<div class="ln-foretelling-row${g?" ln-foretelling-row--planted":""}" data-ln-fate-id="${e.id}" data-ln-seed-idx="${p.idx}"><label class="ln-foretelling-check"><input type="checkbox" data-ln-fate-id="${e.id}" data-ln-seed-idx="${p.idx}"${g?" checked":""} aria-label="Mark foreshadow planted" /><span class="fates-foretelling-placement fates-foretelling-placement--${m}">${escHtml(f)}</span></label><div class="ln-foretelling-body"><p class="ln-foretelling-text">${escHtml(p.seed||"")}</p>`+(p.plausible_cover?`<p class="ln-foretelling-cover"><span class="ln-foretelling-cover-label">If asked:</span> ${escHtml(p.plausible_cover)}</p>`:"")+"</div>"+h+"</div>"}).join(""),u=s?`<div class="fates-foreshadow-regen"><button type="button" class="btn-ghost btn-xs fates-generate-foreshadow-btn" data-fate-generate-foreshadow="${e.id}" title="Discard the current seeds and ask the AI for a new batch">Regenerate</button></div>`:"";return`<section class="fates-scene-section ln-fate-foreshadow-section"><span class="form-section-label">Foreshadows</span><div class="ln-fate-foreshadow-list">${d}</div>`+u+"</section>"}async function _deleteFateForeshadowSeed(e,t){const n=(_fatesSavedList||[]).find(i=>i.id===e);if(!n)return;const a={...n.body||{}},s=Array.isArray(a.foretelling)?[...a.foretelling]:[];if(!(t<0||t>=s.length)){s.splice(t,1),a.foretelling=s;try{const i=_loadPlantedSeeds(e),o=new Set;i.forEach(r=>{r<t?o.add(r):r>t&&o.add(r-1)}),_savePlantedSeeds(e,o)}catch{}try{const i=await api("PUT",`/api/campaigns/${currentCampaign.id}/fates/${e}`,{body:a}),o=(_fatesSavedList||[]).findIndex(r=>
3095r.id===e);o>=0&&i?.fate&&(_fatesSavedList[o]=i.fate),_renderSavedFatesDetail(),typeof _renderNotebookFates=="function"&&_renderNotebookFates()}catch(i){showToast("Delete failed: "+(i.message||"Unknown"))}}}async function _generateFateForeshadow(e,t){const n=(_fatesSavedList||[]).find(s=>s.id===e);if(!n)return;const a=t?t.textContent:"";t&&(t.disabled=!0,t.textContent="Generating\u2026");try{const s=await api("POST",`/api/campaigns/${currentCampaign.id}/fates/generate-foreshadow`,{thread_ids:n.thread_ids||[],entity_ids:n.pivot_entity_ids||[],premise_type:n.premise_type||null,premise:n.premise||"",scene:n.body||{}}),i={...n.body||{}};i.foretelling_applicability=s.foretelling_applicability||"prospective",i.foretelling=Array.isArray(s.foretelling)?s.foretelling:[],i.foretelling_skipped_because=s.foretelling_skipped_because||"";try{_savePlantedSeeds(e,new Set)}catch{}const o=await api("PUT",`/api/campaigns/${currentCampaign.id}/fates/${e}`,{body:i}),r=(_fatesSavedList||[]).findIndex(c=>c.id===e);r>=0&&o?.fate&&(_fatesSavedList[r]=o.fate),_renderSavedFatesDetail(),typeof _renderNotebookFates=="function"&&_renderNotebookFates(),showToast("Foreshadows generated.",2200)}catch(s){showToast("Foreshadows failed: "+(s.message||"Unknown"))}finally{t&&(t.disabled=!1,t.textContent=a)}}document.getElementById("ln-fates-body")?.addEventListener("click",async e=>{const t=e.target.closest("[data-ln-fate-popout]");if(t){e.stopPropagation();const o=parseInt(t.dataset.lnFatePopout,10);if(isNaN(o))return;const r=(_fatesSavedList||[]).find(c=>c.id===o);if(!r)return;_openWorkspaceForSavedFate(r);return}const n=e.target.closest("[data-fate-seed-delete-fid]");if(n){e.stopPropagation();const o=parseInt(n.dataset.fateSeedDeleteFid,10),r=parseInt(n.dataset.fateSeedDeleteIdx,10);if(isNaN(o)||isNaN(r))return;await _deleteFateForeshadowSeed(o,r);return}const a=e.target.closest("[data-fate-generate-foreshadow]");if(a){if(e.stopPropagation(),a.disabled)return;const o=parseInt(a.dataset.fateGenerateForeshadow,10);if(isNaN(o))return;await _generateFateForeshadow(o,a);return}const s=e.target.closest("[data-ln-fate-id]");if(!s)return;const i=parseInt(s.dataset.lnFateId,10);isNaN(i)||(_lnFatesExpanded.has(i)?_lnFatesExpanded.delete(i):_lnFatesExpanded.add(i),_renderNotebookFates())}),document.getElementById("ln-fates-body")?.addEventListener("change",e=>{const t=e.target.closest('input[type="checkbox"][data-ln-fate-id]');if(!t)return;const n=parseInt(t.dataset.lnFateId,10),a=parseInt(t.dataset.lnSeedIdx,10);if(Number.isNaN(n)||Number.isNaN(a))return;const s=_loadPlantedSeeds(n);t.checked?s.add(a):s.delete(a),_savePlantedSeeds(n,s),_renderNotebookFates(),typeof _renderSavedFates=="function"&&_renderSavedFates()}),document.getElementById("fates-weave-selected-btn")?.addEventListener("click",()=>{_fatesSelected.size<2||_openWorkspaceFresh()});function _openWorkspaceFresh(){const e=[..._fatesSelected],t=[..._fatesCurrentWeaveEntityIds||[]];if(_fatesEditMode=!1,_fatesEditSnapshot=null,_fatesWorkspaceMode="fresh",_fatesWorkspaceState={threadIds:e,entityIds:t,premises:[],selectedPremise:null,scene:null,cost:0,savedFateId:null},!_tfFatesAiAllowed()){_fatesWorkspaceState.selectedPremise={type:"custom",premise:"",title:""},_fatesWorkspaceState.scene={},_showWorkspaceStage("scene"),_openWorkspaceModal(),_enterEditMode();return}_showWorkspaceStage("premises"),_openWorkspaceModal(),_fetchPremises()}function _openWorkspaceForSavedFate(e,t=!1){_fatesEditMode=!1,_fatesEditSnapshot=null,_fatesWorkspaceMode="saved",_fatesWorkspaceState={threadIds:e.thread_ids||[],entityIds:e.pivot_entity_ids||[],premises:[],selectedPremise:{type:e.premise_type,premise:e.premise,title:e.title},scene:e.body,cost:0,savedFateId:e.id},_showWorkspaceStage("scene"),_renderSceneStage(),_openWorkspaceModal(),t&&_enterEditMode()}function _openWorkspaceModal(){document.getElementById("fates-weave-modal")?.classList.remove("hidden")}function _closeWorkspaceModal(){document.getElementById("fates-weave-modal")?.classList.add("hidden"),_fatesWorkspaceMode=null,_fatesEditMode=!1,_fatesEditSnapshot=null}document.getElementById("fates-weave-close")?.addEventListener("click",_closeWorkspaceModal),document.getElementById("fates-weave-modal")?.addEventListener("click",e=>{e.target.id==="fates-weave-modal"&&_closeWorkspaceModal()}),document.addEventListener("keydown",e=>{const t=document.getElementById("fates-weave-modal");_tfModalEsc(e,t)&&_closeWorkspaceModal()});function _showWorkspaceStage(e){const t=document.getElementById("fates-workspace-premises"),n=document.getElementById("fates-workspace-scene");t&&t.classList.toggle("hidden",e!=="premises"),n&&n.classList.toggle("hidden",e!=="scene")}let _fatesPremiseInflight=!1,_fatesSceneInflight=!1;async function _fetchPremises(){if(_fatesPremiseInflight)return;
3095if(!_tfRequireFeature("weave")){document.getElementById("fates-weave-modal")?.classList.add("hidden");return}_fatesPremiseInflight=!0;const e=document.getElementById("fates-premise-status"),t=document.getElementById("fates-premise-cards");e&&(e.innerHTML='<p class="placeholder">Proposing premises\u2026 (~8s)</p>'),t&&(t.innerHTML=["collision","convergence","catalyst"].map(n=>`<div class="fates-skeleton-card fates-skeleton-card--${n}"><div class="fates-skeleton-line fates-skeleton-line--title"></div><div class="fates-skeleton-line"></div><div class="fates-skeleton-line fates-skeleton-line--short"></div></div>`).join(""));try{const n=await api("POST",`/api/campaigns/${currentCampaign.id}/fates/propose-premises`,{thread_ids:_fatesWorkspaceState.threadIds,entity_ids:_fatesWorkspaceState.entityIds});_fatesWorkspaceState.premises=n.premises||[],e&&(e.innerHTML=""),_renderPremiseCards()}catch(n){e&&(e.innerHTML=`<p class="placeholder error">Premise proposal failed: ${escHtml(n.message||"Unknown")}</p>`)}finally{_fatesPremiseInflight=!1}}const _PREMISE_TYPE_COLORS={collision:"collision",convergence:"convergence",catalyst:"catalyst"},_REWRITE_PRESETS=[{label:"darker",instruction:"Make it darker and more morally costly. Same scene-engine, harsher stakes."},{label:"raise stakes",instruction:"Raise the stakes: what gets locked in if the party fails or hesitates?"},{label:"political",instruction:"Lean political: fewer swords, more leverage, alliances, and unspoken debts."},{label:"clock",instruction:"Sharpen the clock: make the time pressure more visible in the prose."}],_rewritePremiseInflight=new Set,_premiseUndoStack=[[],[],[]],_UNDO_STACK_MAX=5;function _renderPremiseCards(){const e=document.getElementById("fates-premise-cards");if(!e)return;const t=_fatesWorkspaceState.premises||[];if(!t.length){e.innerHTML='<p class="placeholder">No premises returned.</p>';return}e.innerHTML=t.map((n,a)=>_renderOnePremiseCard(n,a)).join("")}function _renderOnePremiseCard(e,t){const n=(e.type||"custom").toLowerCase(),a=_PREMISE_TYPE_COLORS[n]||"custom";if(!e.premise)return`<div class="fates-premise-card fates-premise-card--${a} fates-premise-card--unavailable"><div class="fates-premise-card-hdr"><span class="fates-premise-card-type">${escHtml(n)}</span></div><p class="fates-premise-card-tension">${escHtml(e.tension||"Not enough context to construct this premise type.")}</p></div>`;const s=_rewritePremiseInflight.has(t),i=(_premiseUndoStack[t]||[]).length>0,o=_REWRITE_PRESETS.map(r=>`<button type="button" class="fates-premise-chip" data-premise-rewrite-chip="${t}" data-instruction="${escHtml(r.instruction)}"${s?" disabled":""}>${escHtml(r.label)}</button>`).join("");return`<div class="fates-premise-card fates-premise-card--${a}${s?" fates-premise-card--rewriting":""}" data-premise-idx="${t}"><div class="fates-premise-card-hdr"><span class="fates-premise-card-type">${escHtml(n)}</span><span class="fates-premise-card-title">${escHtml(e.title||"")}</span>`+(i?`<button type="button" class="fates-premise-undo-btn" data-premise-undo="${t}" title="Undo last rewrite">\u21B6</button>`:"")+`</div><p class="fates-premise-card-text">${escHtml(e.premise)}</p>`+(e.tension?`<p class="fates-premise-card-tension">Tension: ${escHtml(e.tension)}</p>`:"")+`<div class="fates-premise-rewrite-row"><div class="fates-premise-chip-row">${o}</div><div class="fates-premise-rewrite-input-row"><input type="text" class="fates-premise-rewrite-input" data-premise-rewrite-input="${t}" placeholder="\u270E Rewrite but\u2026 (e.g. involve the wombat)" maxlength="500"${s?" disabled":""} /><button type="button" class="btn-ghost btn-xs fates-premise-rewrite-submit" data-premise-rewrite-submit="${t}"${s?" disabled":""} title="Rewrite this premise with your instruction">\u21BB</button></div><div class="fates-premise-rewrite-status" data-premise-rewrite-status="${t}">${s?"Rewriting\u2026":""}</div></div><div class="fates-premise-card-pick-row"><button type="button" class="btn-new fates-premise-pick-btn" data-premise-pick="${t}"${s?" disabled":""}>Use this premise \u2192</button></div></div>`}async function _rewritePremise(e,t){if(_rewritePremiseInflight.has(e))return;const a=(_fatesWorkspaceState.premises||[])[e];if(!a||!a.premise)return;const s=(t||"").trim();if(s){_rewritePremiseInflight.add(e),_renderPremiseCards();try{const o=(await api("POST",`/api/campaigns/${currentCampaign.id}/fates/rewrite-premise`,{premise:a,instruction:s,thread_ids:_fatesWorkspaceState.threadIds,entity_ids:_fatesWorkspaceState.entityIds})).premise;if(!o||!o.premise)throw new Error("Empty rewrite returned");const r=_premiseUndoStack[e]||(_premiseUndoStack[e]=[]);r.push(a),r.length>_UNDO_STACK_MAX&&r.shift(),_fatesWorkspaceState.premises[e]=o}catch(i){showToast(`Rewrite failed: ${i.message||"Unknown error"}`,4e3)}finally{_rewritePremiseInflight.delete(e),_renderPremiseCards()}}}function _undoPremiseRewrite(e){const n=(_premiseUndoStack[e]||[]).pop();n&&(_fatesWorkspaceState.premises[e]=n,_renderPremiseCards())}document.getElementById("fates-premise-cards")?.addEventListener("click",e=>{const t=e.target.closest("[data-premise-pick]");if(t){const i=parseInt(t.dataset.premisePick,10),o=_fatesWorkspaceState.premises[i];if(!o||!o.premise)return;_fatesWorkspaceState.selectedPremise={type:o.type,premise:o.premise,title:o.title},_showWorkspaceStage("scene"),_generateScene();return}const n=e.target.closest("[data-premise-rewrite-chip]");if(n){const i=parseInt(n.dataset.premiseRewriteChip,10);_rewritePremise(i,n.dataset.instruction);return}const a=e.target.closest("[data-premise-rewrite-submit]");if(a){const i=parseInt(a.dataset.premiseRewriteSubmit,10),r=(document.querySelector(`[data-
3095premise-rewrite-input="${i}"]`)?.value||"").trim();r&&_rewritePremise(i,r);return}const s=e.target.closest("[data-premise-undo]");if(s){const i=parseInt(s.dataset.premiseUndo,10);_undoPremiseRewrite(i);return}}),document.getElementById("fates-premise-cards")?.addEventListener("keydown",e=>{if(e.key!=="Enter")return;const t=e.target.closest("[data-premise-rewrite-input]");if(!t)return;e.preventDefault();const n=parseInt(t.dataset.premiseRewriteInput,10),a=t.value.trim();a&&_rewritePremise(n,a)});const _customInput=document.getElementById("fates-premise-custom-input"),_customUseBtn=document.getElementById("fates-premise-custom-use");_customInput?.addEventListener("input",()=>{_customUseBtn&&(_customUseBtn.disabled=!_customInput.value.trim())}),_customInput?.addEventListener("keydown",e=>{e.key==="Enter"&&(e.metaKey||e.ctrlKey)&&(e.preventDefault(),_customUseBtn&&!_customUseBtn.disabled&&_customUseBtn.click())}),_customUseBtn?.addEventListener("click",()=>{const e=(_customInput?.value||"").trim();e&&(_fatesWorkspaceState.selectedPremise={type:"custom",premise:e,title:null},_showWorkspaceStage("scene"),_generateScene())});async function _generateScene(){if(_fatesSceneInflight)return;_fatesSceneInflight=!0;const e=document.getElementById("fates-scene-status"),t=document.getElementById("fates-scene-render"),n=document.getElementById("fates-scene-actions");if(e&&(e.innerHTML='<p class="placeholder">Generating scene\u2026 (~12s)</p>'),t){const s=["Opening beat","What the players see","Behind the screen","NPC moves","The trigger","Foreshadows","If the party engages","If the party walks"];t.innerHTML='<div class="fates-skeleton-scene"><div class="fates-skeleton-line fates-skeleton-line--title"></div><div class="fates-skeleton-line"></div>'+s.map(i=>`<div class="fates-skeleton-section"><span class="form-section-label">${escHtml(i)}</span><div class="fates-skeleton-line"></div><div class="fates-skeleton-line fates-skeleton-line--short"></div></div>`).join("")+"</div>"}n&&n.classList.add("hidden");const a=_fatesWorkspaceState.selectedPremise;if(!a){_fatesSceneInflight=!1;return}try{const s=await api("POST",`/api/campaigns/${currentCampaign.id}/fates/generate-scene`,{thread_ids:_fatesWorkspaceState.threadIds,entity_ids:_fatesWorkspaceState.entityIds,premise_type:a.type,premise:a.premise,title:a.title});_fatesWorkspaceState.scene=s.scene,_fatesWorkspaceState.selectedPremise={type:s.premise_type,premise:s.premise,title:s.title},_fatesWorkspaceState.cost=(_fatesWorkspaceState.cost||0)+(s.cost_usd||0),_fatesWorkspaceState.savedFateId=null,e&&(e.innerHTML=""),_renderSceneStage()}catch(s){e&&(e.innerHTML=`<p class="placeholder error">Scene generation failed: ${escHtml(s.message||"Unknown")}</p>`)}finally{_fatesSceneInflight=!1}}function _renderSceneStage(){if(_fatesEditMode){_renderSceneEditForm(),_renderSceneActionsForEdit();return}_renderSceneReadOnly(),_renderSceneActionsForRead()}function _renderSceneReadOnly(){const e=document.getElementById("fates-scene-render"),t=_fatesWorkspaceState.scene||{},n=_fatesWorkspaceState.selectedPremise||{};if(!e)return;const a=(n.type||"custom").toLowerCase(),s=a.charAt(0).toUpperCase()+a.slice(1),o=_fatesWorkspaceMode==="saved"?'<span class="form-section-label fates-scene-saved-eyebrow">Saved Fate</span>':"",r=t.gm_reality||t.what_players_dont_see,c=t.the_trigger||t.the_pivot,d=(Array.isArray(t.npc_moves)?t.npc_moves:[]).map(p=>{const m=(p.presence||"").toLowerCase(),f=m&&["in-scene","incoming","offstage"].includes(m)?` <span class="fates-scene-npc-presence fates-scene-npc-presence--${m}">${escHtml(m)}</span>`:"";return`<li><strong>${escHtml(p.who||"?")}</strong>${f}<span class="fates-scene-npc-sep">:</span> ${escHtml(p.what||"")}</li>`}).join(""),u=_renderForetelling(t);if(e.innerHTML=o+`<header class="fates-scene-hdr"><span class="fates-scene-type fates-scene-type--${a}">${escHtml(s)}</span>`+_inlineTitleHtml(t.title||n.title||"Untitled Fate")+_inlinePremiseHtml(n.premise||"")+"</header>"+_sceneSectionEditable("Opening beat",t.opening_beat,"opening_beat")+_sceneSectionEditable("What the players see",t.what_players_see,"what_players_see")+_sceneSectionEditable("Behind the screen (the secret driving the scene)",r,"gm_reality",{secret:!0})+(d?`<section class="fates-scene-section"><span class="form-section-label">NPC moves</span><ul class="fates-scene-npc">${d}</ul></section>`:"")+_sceneSectionEditable("The trigger",c,"the_trigger")+_sceneSectionEditable("Irreversible act (the loaded gun)",t.irreversible_act,"irreversible_act",{secret:!0})+u+_sceneSectionEditable("If the party engages",t.consequences_if_party_engages,"consequences_if_party_engages")+_sceneSectionEditable("If the party walks",t.consequences_if_party_walks,"consequences_if_party_walks"),_inlineSectionField){const p=e.querySelector(`[data-inline-edit-field="${_inlineSectionField}"]`);if(p&&(p.focus(),typeof p.selectionStart=="number")){const m=p.value||"";p.setSelectionRange(m.length,m.length)}}}function _renderForetelling(e){const t=(e.foretelling_applicability||"").toLowerCase(),n=Array.isArray(e.foretelling)?e.foretelling:[];
3095if(t==="none"||!n.length&&t!=="retrospective")return t==="none"&&e.foretelling_skipped_because?`<section class="fates-scene-section fates-scene-section--foretelling-skipped"><span class="form-section-label">Foreshadows</span><p class="form-helper-text">No foreshadows: ${escHtml(e.foretelling_skipped_because)}</p></section>`:"";const a=_fatesWorkspaceState.savedFateId||"fresh",s=_loadPlantedSeeds(a),i=t==="retrospective"?"World context (what the party may have heard / seen)":"Drop in prior sessions",o=n.map((r,c)=>{const l=s.has(c),d=(r.placement||"mid").toLowerCase();return`<li class="fates-foretelling-seed${l?" fates-foretelling-seed--planted":""}"><label class="fates-foretelling-check"><input type="checkbox" data-foretelling-idx="${c}"${l?" checked":""} aria-label="Mark foreshadow planted" /><span class="fates-foretelling-placement fates-foretelling-placement--${d}">${escHtml(d==="same_session"?"this session":d)}</span></label><div class="fates-foretelling-body"><p class="fates-foretelling-seed-text">${escHtml(r.seed||"")}</p>`+(r.plausible_cover?`<p class="fates-foretelling-cover"><span class="fates-foretelling-cover-label">If asked:</span> ${escHtml(r.plausible_cover)}</p>`:"")+"</div></li>"}).join("");return`<section class="fates-scene-section fates-scene-section--foretelling"><span class="form-section-label">Foreshadows: ${escHtml(i)}</span><ul class="fates-foretelling-list">${o}</ul></section>`}function _seedKey(e){return`tf_foretelling_planted_${e}`}function _loadPlantedSeeds(e){try{const t=localStorage.getItem(_seedKey(e));if(!t)return new Set;const n=JSON.parse(t);return new Set(Array.isArray(n)?n:[])}catch{return new Set}}function _savePlantedSeeds(e,t){try{localStorage.setItem(_seedKey(e),JSON.stringify([...t]))}catch{}}function _renderSceneActionsForRead(){const e=document.getElementById("fates-scene-actions"),t=document.getElementById("fates-scene-back"),n=document.getElementById("fates-scene-regenerate"),a=document.getElementById("fates-scene-edit"),s=document.getElementById("fates-scene-save"),i=document.getElementById("fates-scene-delete"),o=document.getElementById("fates-scene-copy"),r=document.getElementById("fates-scene-edit-cancel"),c=document.getElementById("fates-scene-edit-done");e&&e.classList.remove("hidden");const l=_fatesWorkspaceMode==="saved";t&&t.classList.toggle("hidden",l),n&&n.classList.toggle("hidden",l),a&&a.classList.remove("hidden"),s&&s.classList.toggle("hidden",l),i&&i.classList.toggle("hidden",!l),r&&r.classList.add("hidden"),c&&c.classList.add("hidden"),o&&(o.disabled=!1),s&&(s.disabled=!1),_applyTfAiGating()}function _renderSceneEditForm(){const e=document.getElementById("fates-scene-render");if(!e)return;const t=_fatesWorkspaceState.scene||{},n=_fatesWorkspaceState.selectedPremise||{},a=(n.type||"custom").toLowerCase(),s=t.gm_reality||t.what_players_dont_see||"",i=t.the_trigger||t.the_pivot||"",r=(Array.isArray(t.npc_moves)?t.npc_moves:[]).map((u,p)=>_renderNpcEditRow(u,p)).join(""),c=t.target_entity_id||"",l=(allEntities||[]).filter(u=>u.type==="pc"&&!u.is_dead&&!u.is_inactive).sort((u,p)=>String(u.name||"").localeCompare(String(p.name||""))),d=[`<option value="">${escHtml(_partyLabel())} (auto-detect from premise)</option>`].concat(l.map(u=>`<option value="${u.id}"${u.id===c?" selected":""}>${escHtml(u.name)}
3095</option>`)).join("");e.innerHTML=`<div class="fates-scene-edit-form"><div class="form-row fates-edit-row-meta"><label>Target<select data-edit-field="target_entity_id" class="fates-edit-input">${d}</select></label><label>Premise type<select data-edit-field="premise_type" class="fates-edit-input">`+["collision","convergence","catalyst","custom"].map(u=>`<option value="${u}"${u===a?" selected":""}>${u.charAt(0).toUpperCase()+u.slice(1)}</option>`).join("")+`</select></label></div><div class="form-row"><label>Title<input type="text" data-edit-field="title" class="fates-edit-input" value="${escHtml(t.title||n.title||"")}" placeholder="Scene title (e.g. The Witch Calls His Name)" /></label></div><div class="form-row"><label>Premise<textarea data-edit-field="premise" class="fates-edit-input fates-edit-textarea" rows="3" placeholder="One sentence describing the dramatic moment. Premise is the only required field, fill in the rest now or come back later.">${escHtml(n.premise||"")}</textarea></label></div>`+(_fatesWorkspaceMode==="fresh"?'<p class="form-helper-text fates-quick-save-hint">Tip: only Title + Premise are required. Save now and flesh out the rest before your session.</p>':"")+_editFieldRow("Opening beat","opening_beat",t.opening_beat||"",3)+_editFieldRow("What the players see","what_players_see",t.what_players_see||"",3)+_editFieldRow("Behind the screen (the secret driving the scene)","gm_reality",s,3)+`<fieldset class="fates-edit-fieldset"><legend>NPC moves</legend><div id="fates-edit-npc-list" class="fates-edit-npc-list">${r}</div><button type="button" id="fates-edit-add-npc" class="btn-ghost fates-edit-add-npc-btn">+ Add NPC move</button></fieldset>`+_editFieldRow("The trigger","the_trigger",i,3)+_editFieldRow("Irreversible act (the loaded gun)","irreversible_act",t.irreversible_act||"",2)+_renderForetellingEditFieldset(t)+_editFieldRow("If the party engages","consequences_if_party_engages",t.consequences_if_party_engages||"",3)+_editFieldRow("If the party walks","consequences_if_party_walks",t.consequences_if_party_walks||"",3)+"</div>",_applyTfAiGating(e)}function _renderForetellingEditFieldset(e){const t=(e.foretelling_applicability||"prospective").toLowerCase(),n=Array.isArray(e.foretelling)?e.foretelling:[],a=e.foretelling_skipped_because||"",s=[["prospective","Prospective: plant in prior sessions"],["retrospective","Retrospective: what the party may have heard"],["none","None: no foreshadowing possible (justify)"]].map(([o,r])=>`<option value="${o}"${o===t?" selected":""}>${escHtml(r)}</option>`).join(""),i=n.map((o,r)=>
3095_renderForetellingEditRow(o,r)).join("");return`<fieldset class="fates-edit-fieldset"><legend>Foreshadows</legend><div class="form-row fates-edit-row-meta"><label>Applicability<select data-edit-field="foretelling_applicability" class="fates-edit-input">${s}</select></label></div><div class="form-row fates-foretelling-skipped-row${t==="none"?"":" hidden"}"><label>Why no foreshadowing?<input type="text" data-edit-field="foretelling_skipped_because" class="fates-edit-input" maxlength="120" value="${escHtml(a)}" placeholder="\u226420 words: pure ambush, divine intervention, etc." /></label></div><div id="fates-edit-foretelling-list" class="fates-edit-foretelling-list${t==="none"?" hidden":""}">${i}</div><div class="fates-edit-foretelling-actions${t==="none"?" hidden":""}"><button type="button" id="fates-edit-add-seed" class="btn-ghost btn-xs">+ Add foreshadow</button><button type="button" id="fates-edit-generate-seeds" class="btn-ghost btn-xs" title="${n.length?"Discard the current seeds and ask the AI for a new batch":"Have the AI propose 3-5 seeds priming this scene"}">${n.length?"Regenerate":"Generate Foreshadows"}</button></div></fieldset>`}function _renderForetellingEditRow(e,t){const n=e&&e.seed||"",a=e&&e.plausible_cover||"",s=(e&&e.placement||"mid").toLowerCase(),i=["early","mid","late","same_session"].map(o=>`<option value="${o}"${o===s?" selected":""}>${o==="same_session"?"this session":o}</option>`).join("");return`<div class="fates-edit-foretelling-row" data-seed-idx="${t}"><div class="fates-edit-foretelling-row-hdr"><select class="fates-edit-input fates-edit-foretelling-placement" data-seed-field="placement">${i}</select><button type="button" class="fates-edit-npc-remove" data-action="remove-seed" title="Remove this seed">\u2715</button></div><textarea class="fates-edit-input fates-edit-textarea" data-seed-field="seed" rows="2" placeholder="The foreshadow (1-2 sentences of table-ready prose)">${escHtml(n)}</textarea><textarea class="fates-edit-input fates-edit-textarea" data-seed-field="plausible_cover" rows="2" placeholder="Plausible cover: the mundane reading a player will land on">${escHtml(a)}</textarea></div>`}const _FATES_FIELD_HINTS={opening_beat:"What's already in motion as the party arrives: open with action, not setup. (e.g. 'The witch has Mira pinned, the chant a third through, the bone-knife at her throat.')",what_players_see:"Sensory details: what the party can SEE / hear / smell. The chant in an unknown tongue, the locket glowing, the smell of burnt rosemary.",gm_reality:"What's ACTUALLY happening that the players can't see yet. The hidden truth that changes how YOU run the room.",the_trigger:"The moment of no return: write what HAPPENS, not what they decide. (e.g. 'The chant reaches the penultimate line and the witch turns to look at Calder.')",irreversible_act:"The loaded gun in the room: what cannot be undone if any character takes it.",consequences_if_party_engages:"World events that follow if they act, not info rewards. Make this asymmetric from the walks branch.",consequences_if_party_walks:"World events that follow if they don't act, must lead to a genuinely different future."};function _editFieldRow(e,t,n,a){const s=_FATES_FIELD_HINTS[t]||e;return`<div class="form-row"><label>${escHtml(e)}<textarea data-edit-field="${t}" class="fates-edit-input fates-edit-textarea" rows="${a||2}" placeholder="${escHtml(s)}">${escHtml(n)}</textarea></label></div>`}function _renderNpcEditRow(e,t){const n=e&&e.who||"",a=e&&e.what||"",s=(e&&e.presence||"in-scene").toLowerCase(),i=["in-scene","incoming","offstage"].map(o=>`<option value="${o}"${o===s?" selected":""}>${o}</option>`).join("");return`<div class="fates-edit-npc-row" data-npc-idx="${t}"><input type="text" class="fates-edit-input fates-edit-npc-who" data-npc-field="who" value="${escHtml(n)}" placeholder="Who" /><select class="fates-edit-input fates-edit-npc-presence" data-npc-field="presence">${i}</select><textarea class="fates-edit-input fates-edit-textarea fates-edit-npc-what" data-npc-field="what" rows="2" placeholder="What this NPC does in this scene">${escHtml(a)}</textarea><button type="button" class="fates-edit-npc-remove" data-action="remove-npc" title="Remove this NPC move">\u2715</button></div>`}function _renderSceneActionsForEdit(){const e=document.getElementById("fates-scene-actions"),t=document.getElementById("fates-scene-back"),n=document.getElementById("fates-scene-regenerate"),a=document.getElementById("fates-scene-edit"),s=document.getElementById("fates-scene-save"),i=document.getElementById("fates-scene-delete"),o=document.getElementById("fates-scene-copy"),r=document.getElementById("fates-scene-edit-cancel"),c=document.getElementById("fates-scene-edit-done");
3095e&&e.classList.remove("hidden"),[t,n,a,s,i,o].forEach(l=>l&&l.classList.add("hidden")),r&&r.classList.remove("hidden"),c&&c.classList.remove("hidden")}function _harvestSceneEdits(){const e=document.getElementById("fates-scene-render");if(!e)return;const t=_fatesWorkspaceState.scene||{},n=_fatesWorkspaceState.selectedPremise||{};["title","opening_beat","what_players_see","gm_reality","the_trigger","irreversible_act","consequences_if_party_engages","consequences_if_party_walks"].forEach(p=>{const m=e.querySelector(`[data-edit-field="${p}"]`);if(!m)return;const f=(m.value||"").trim();t[p]=f});const s=e.querySelector('[data-edit-field="premise"]'),i=e.querySelector('[data-edit-field="premise_type"]');s&&(n.premise=(s.value||"").trim()),i&&(n.type=(i.value||"").toLowerCase());const o=e.querySelector('[data-edit-field="target_entity_id"]');if(o){const p=(o.value||"").trim();t.target_entity_id=p&&parseInt(p,10)||null}const r=e.querySelectorAll(".fates-edit-npc-row"),c=[];r.forEach(p=>{const m=p.querySelector('[data-npc-field="who"]'),f=p.querySelector('[data-npc-field="what"]'),g=p.querySelector('[data-npc-field="presence"]'),h=(m&&m.value||"").trim();h&&c.push({who:h,what:(f&&f.value||"").trim(),presence:(g&&g.value||"in-scene").toLowerCase()})}),t.npc_moves=c;const l=e.querySelector('[data-edit-field="foretelling_applicability"]'),d=e.querySelector('[data-edit-field="foretelling_skipped_because"]'),u=(l&&l.value||"prospective").toLowerCase();if(t.foretelling_applicability=u,u==="none")t.foretelling_skipped_because=(d&&d.value||"").trim(),t.foretelling=[];else{delete t.foretelling_skipped_because;const p=e.querySelectorAll(".fates-edit-foretelling-row"),m=[];p.forEach(f=>{const g=f.querySelector('[data-seed-field="seed"]'),h=f.querySelector('[data-seed-field="plausible_cover"]'),v=f.querySelector('[data-seed-field="placement"]'),b=(g&&g.value||"").trim();b&&m.push({seed:b,plausible_cover:(h&&h.value||"").trim(),placement:(v&&v.value||"mid").toLowerCase()})}),t.foretelling=m}_fatesWorkspaceState.scene=t,_fatesWorkspaceState.selectedPremise=n}function _enterEditMode(){_inlineSectionField=null,_fatesEditSnapshot=JSON.parse(JSON.stringify({scene:_fatesWorkspaceState.scene||{},selectedPremise:_fatesWorkspaceState.selectedPremise||{}})),_fatesEditMode=!0,_renderSceneStage()}function _exitEditMode(e){if(e?_harvestSceneEdits():_fatesEditSnapshot&&(_fatesWorkspaceState.scene=_fatesEditSnapshot.scene,_fatesWorkspaceState.selectedPremise=_fatesEditSnapshot.selectedPremise),_fatesEditSnapshot=null,_fatesEditMode=!1,_renderSceneStage(),e){const t=document.getElementById("fates-scene-status");t&&(t.innerHTML='<p class="placeholder fates-scene-status--applied">Edits applied, click Save to keep them.</p>',setTimeout(()=>{t.querySelector(".fates-scene-status--applied")&&(t.innerHTML="")},4e3))}}const _INLINE_SECTION_FIELDS=new Set(["title","premise","opening_beat","what_players_see","gm_reality","the_trigger","irreversible_act","consequences_if_party_engages","consequences_if_party_walks"]);function _sceneSectionEditable(e,t,n,a){const i=!!(a&&a.secret)?" fates-scene-section--secret":"";if(_inlineSectionField===n)return`<section class="fates-scene-section fates-scene-section--editing${i}"><span class="form-section-label">${escHtml(e)}</span><textarea class="fates-scene-inline-edit" rows="${n==="irreversible_act"?2:3}" data-inline-edit-field="${escHtml(n)}" maxlength="2000">${escHtml(t||"")}</textarea>`+_inlineEditActionsHtml()+"</section>";const r=!(t&&t.trim&&t.trim());return`<section class="fates-scene-section${i}${r?" fates-scene-section--empty":""}"><div class="fates-scene-section-hdr"><span class="form-section-label">${escHtml(e)}</span>`+_inlinePencilHtml(n)+"</div>"+(r?'<p class="placeholder fates-scene-section-empty">Not written yet, click \u270E to fill in.</p>':`<p>${escHtml(t)}</p>`)+"</section>"}function _inlineTitleHtml(e){return _inlineSectionField==="title"?`<div class="fates-scene-inline-title-edit"><input type="text" class="fates-scene-inline-edit fates-scene-inline-edit--single" data-inline-edit-field="title" maxlength="240" value="${esc
3095Html(e||"")}" placeholder="Scene title" />`+_inlineEditActionsHtml({singleLine:!0})+"</div>":`<div class="fates-scene-title-row"><h3 class="fates-scene-title">${escHtml(e||"Untitled Fate")}</h3>`+_inlinePencilHtml("title")+"</div>"}function _inlinePremiseHtml(e){return _inlineSectionField==="premise"?`<div class="fates-scene-inline-premise-edit"><textarea class="fates-scene-inline-edit" rows="2" data-inline-edit-field="premise" maxlength="500" placeholder="One sentence describing the dramatic moment.">${escHtml(e||"")}</textarea>`+_inlineEditActionsHtml()+"</div>":`<div class="fates-scene-premise-row"><blockquote class="fates-scene-premise">${escHtml(e||"")}</blockquote>`+_inlinePencilHtml("premise")+"</div>"}function _inlinePencilHtml(e){return _fatesEditMode?"":`<button type="button" class="fates-scene-edit-pencil" data-inline-edit-open="${escHtml(e)}" title="Edit this section inline" aria-label="Edit ${escHtml(e)}">\u270E</button>`}function _inlineEditActionsHtml(e){const n=!!(e&&e.singleLine)?"Enter to save \xB7 Esc to cancel":"\u2318/Ctrl + Enter to save \xB7 Esc to cancel";return`<div class="fates-scene-inline-edit-actions"><button type="button" class="btn-new btn-xs" data-inline-edit-save>\u2713 Save</button><button type="button" class="btn-ghost btn-xs" data-inline-edit-cancel>Cancel</button><span class="form-helper-text fates-scene-inline-edit-hint">${escHtml(n)}</span></div>`}async function _saveInlineSection(e,t){if(!_INLINE_SECTION_FIELDS.has(e))return;const n=(t||"").trim(),a=_fatesWorkspaceState.scene||(_fatesWorkspaceState.scene={}),s=_fatesWorkspaceState.selectedPremise||(_fatesWorkspaceState.selectedPremise={});e==="title"?a.title=n:e==="premise"?s.premise=n:a[e]=n;const i=_fatesWorkspaceMode==="saved";if(_inlineSectionField=null,!i){_renderSceneReadOnly();return}const o=_fatesWorkspaceState.savedFateId;if(!o){_renderSceneReadOnly();return}_renderSceneReadOnly();try{const r=await api("PUT",`/api/campaigns/${currentCampaign.id}/fates/${o}`,{title:a.title||s.title||"Untitled Fate",premise_type:s.type,premise:s.premise,body:a}),c=(_fatesSavedList||[]).findIndex(l=>l.id===o);c>=0&&r?.fate&&(_fatesSavedList[c]=r.fate),_renderSavedFates()}catch(r){showToast("Save failed: "+(r.message||"Unknown"))}}document.getElementById("fates-scene-render")?.addEventListener("click",e=>{if(_fatesEditMode)return;const t=e.target.closest("[data-inline-edit-open]");if(t){_inlineSectionField=t.dataset.inlineEditOpen,_renderSceneReadOnly();return}if(e.target.closest("[data-inline-edit-cancel]")){_inlineSectionField=null,_renderSceneReadOnly();return}const a=e.target.closest("[data-inline-edit-save]");if(a){const s=a.closest("section,div")?.querySelector("[data-inline-edit-field]");if(!s)return;_saveInlineSection(s.dataset.inlineEditField,s.value);return}}),document.getElementById("fates-scene-render")?.addEventListener("keydown",e=>{if(_fatesEditMode)return;const t=e.target.closest("[data-inline-edit-field]");if(t){if(e.key==="Escape"){e.preventDefault(),_inlineSectionField=null,_renderSceneReadOnly();return}e.key==="Enter"&&(t.tagName==="INPUT"||e.metaKey||e.ctrlKey)&&(e.preventDefault(),_saveInlineSection(t.dataset.inlineEditField,t.value))}}),document.getElementById("fates-scene-back")?.addEventListener("click",()=>{_fatesWorkspaceMode==="fresh"&&_showWorkspaceStage("premises")}),document.getElementById("fates-scene-regenerate")?.addEventListener("click",()=>{_fatesWorkspaceMode==="fresh"&&_generateScene()}),document.getElementById("fates-scene-edit")?.addEventListener("click",()=>{_fatesWorkspaceMode!=="fresh"&&_fatesWorkspaceMode!=="saved"||_enterEditMode()}),document.getElementById("fates-scene-edit-cancel")?.addEventListener("click",()=>{_fatesEditMode&&_exitEditMode(!1)}),document.getElementById("fates-scene-edit-done")?.addEventListener("click",async()=>{if(!_fatesEditMode)return;if(_fatesWorkspaceMode!=="saved"){_exitEditMode(!0);return}_harvestSceneEdits();const e=document.getElementById("fates-scene-edit-done"),t=e?e.textContent:"Done";e&&(e.disabled=!0,e.textContent="Saving\u2026");try{const n=_fatesWorkspaceState.selectedPremise||{},a=_fatesWorkspaceState.scene||{},s=_fatesWorkspaceState.savedFateId,i=await api("PUT",`/api/campaigns/${currentCampaign.id}/fates/${s}`,{title:a.title||n.title||"Untitled Fate",premise_type:n.type,premise:n.premise,body:a}),o=(_fatesSavedList||[]).findIndex(r=>
3095r.id===s);o>=0&&i?.fate&&(_fatesSavedList[o]=i.fate),_fatesEditSnapshot=null,_fatesEditMode=!1,_renderSceneStage(),_renderSavedFates(),showToast("Fate updated.",2200)}catch(n){showToast("Save failed: "+(n.message||"Unknown"))}finally{e&&(e.disabled=!1,e.textContent=t)}}),document.getElementById("fates-scene-render")?.addEventListener("click",e=>{if(!_fatesEditMode)return;const t=e.target.closest('[data-action="remove-npc"]');if(t){e.preventDefault();const n=t.closest(".fates-edit-npc-row");n&&n.remove();return}}),document.getElementById("fates-scene-render")?.addEventListener("click",e=>{if(!_fatesEditMode)return;if(e.target&&e.target.id==="fates-edit-add-npc"){e.preventDefault();const n=document.getElementById("fates-edit-npc-list");if(!n)return;const a=n.querySelectorAll(".fates-edit-npc-row").length;n.insertAdjacentHTML("beforeend",_renderNpcEditRow({who:"",what:"",presence:"in-scene"},a))}if(e.target&&e.target.id==="fates-edit-add-seed"){e.preventDefault();const n=document.getElementById("fates-edit-foretelling-list");if(!n)return;const a=n.querySelectorAll(".fates-edit-foretelling-row").length;n.insertAdjacentHTML("beforeend",_renderForetellingEditRow({seed:"",plausible_cover:"",placement:"mid"},a))}if(e.target&&e.target.id==="fates-edit-generate-seeds"){e.preventDefault();const n=e.target;if(n.disabled)return;_harvestSceneEdits();const a=_fatesWorkspaceState.selectedPremise||{},s=_fatesWorkspaceState.scene||{},i=n.textContent;n.disabled=!0,n.textContent="Generating\u2026",(async()=>{try{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/fates/generate-foreshadow`,{thread_ids:_fatesWorkspaceState.threadIds||[],entity_ids:_fatesWorkspaceState.entityIds||[],premise_type:a.type||null,premise:a.premise||"",scene:s});s.foretelling_applicability=o.foretelling_applicability||"prospective",s.foretelling=Array.isArray(o.foretelling)?o.foretelling:[],s.foretelling_skipped_because=o.foretelling_skipped_because||"",_fatesWorkspaceState.scene=s;const r=_fatesWorkspaceState.savedFateId;if(r)try{_savePlantedSeeds(r,new Set)}catch{}_renderSceneEditForm(),showToast("Foreshadows generated. Click Done to keep.",2500)}catch(o){showToast("Foreshadow failed: "+(o.message||"Unknown")),n.disabled=!1,n.textContent=i}})()}const t=e.target.closest('[data-action="remove-seed"]');if(t){e.preventDefault();const n=t.closest(".fates-edit-foretelling-row");n&&n.remove()}}),document.getElementById("fates-scene-render")?.addEventListener("change",e=>{if(!_fatesEditMode)return;const t=e.target.closest('[data-edit-field="foretelling_applicability"]');if(!t)return;const n=t.value==="none",a=document.querySelector(".fates-foretelling-skipped-row"),s=document.getElementById("fates-edit-foretelling-list"),i=document.querySelector(".fates-edit-foretelling-actions");a&&a.classList.toggle("hidden",!n),s&&s.classList.toggle("hidden",n),i&&i.classList.toggle("hidden",n)}),document.getElementById("fates-scene-render")?.addEventListener("change",e=>{const t=e.target.closest("input[data-foretelling-idx]");if(!t)return;const n=parseInt(t.dataset.foretellingIdx,10);if(Number.isNaN(n))return;const a=_fatesWorkspaceState.savedFateId||"fresh",s=_loadPlantedSeeds(a);t.checked?s.add(n):s.delete(n),_savePlantedSeeds(a,s);const i=t.closest(".fates-foretelling-seed");i&&i.classList.toggle("fates-foretelling-seed--planted",t.checked),_renderSeedsToPlant()}),document.getElementById("fates-scene-save")?.addEventListener("click",async()=>{if(_fatesWorkspaceMode!=="fresh")return;const e=_fatesWorkspaceState.selectedPremise,t=_fatesWorkspaceState.scene;if(!e||!t)return;const n=document.getElementById("fates-scene-save");n&&(n.disabled=!0,n.textContent="Saving\u2026");try{const s=(await api("POST",`/api/campaigns/${currentCampaign.id}/fates`,{title:t.title||e.title||"Untitled Fate",thread_ids:_fatesWorkspaceState.threadIds,pivot_entity_ids:_fatesWorkspaceState.entityIds,premise_type:e.type,premise:e.premise,body:t,cost_usd:_fatesWorkspaceState.cost||0})).fate?.id||null;if(_fatesWorkspaceState.savedFateId=s,showToast("Fate saved.",2500),_closeWorkspaceModal(),s&&(_savedFatesSelectedId=s),await _loadSavedFates(),s){const i=document.querySelector(`#fates-saved-list .fates-saved-card[data-fate-id="${s}"]`);i&&(i.scrollIntoView({behavior:"smooth",block:"start"}),i.classList.add("fates-saved-card--just-saved"),setTimeout(()=>i.classList.remove("fates-saved-card--just-saved"),1800))}}catch(a){showToast("Save failed: "+(a.message||"Unknown"))}finally{n&&(n.disabled=!1,n.textContent="Save")}}),document.getElementById("fates-scene-delete")?.addEventListener("click",async()=>{const e=document.getElementById("fates-scene-delete");if(!(!e||e.disabled)){e.disabled=!0;try{const t=_fatesWorkspaceState.savedFateId;if(!t||_fatesWorkspaceMode!=="saved")return;const n=_fatesWorkspaceState.scene&&_fatesWorkspaceState.scene.title||"this Fate";if(!await confirmDialog({title:"Delete Fate",message:`Delete "${n}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))return;try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/fates/${t}`),_closeWorkspaceModal(),showToast("Fate deleted.",2e3),await _loadSavedFates()}catch(s){showToast("Delete failed: "+(s.message||"Unknown"))}}finally{e.disabled=!1}}}),document.getElementById("fates-scene-copy")?.addEventListener("click",async()=>{const e=_fatesWorkspaceState.selectedPremise||{},t=_fatesWorkspaceState.scene||{},n=_fateAsMarkdown(t,e,_fatesWorkspaceState.threadIds);try{await navigator.clipboard.writeText(n),showToast("Fate copied as Markdown.",2500)}catch{showToast("Copy failed, clipboard permission denied.")}});function _fateAsMarkdown(e,t,n){const a=t.type||"custom",s=a.charAt(0).toUpperCase()+a.slice(1),i=(n||[]).map(u=>(_hooksAllThreads||[]).find(p=>
3095p.id===u)).filter(Boolean).map(u=>u.title||"(untitled)"),o=[];o.push(`## ${e.title||t.title||"Untitled Fate"}`),o.push(`*${s}${i.length?" \xB7 "+i.join(" \xD7 "):""}*`),o.push(""),o.push(`> ${(t.premise||"").replace(/\n/g,`
3096> `)}`);const r=(u,p)=>{p&&(o.push(""),o.push(`### ${u}`),o.push(p))},c=e.gm_reality||e.what_players_dont_see,l=e.the_trigger||e.the_pivot;if(r("Opening beat",e.opening_beat),r("What the players see",e.what_players_see),r("Behind the screen",c),Array.isArray(e.npc_moves)&&e.npc_moves.length){o.push(""),o.push("### NPC moves");for(const u of e.npc_moves){const p=u.presence?` (${u.presence})`:"";o.push(`- **${u.who||"?"}**${p}: ${u.what||""}`)}}r("The trigger",l),r("Irreversible act",e.irreversible_act);const d=(e.foretelling_applicability||"").toLowerCase();if(d==="none")e.foretelling_skipped_because&&(o.push(""),o.push("### Foreshadow"),o.push(`*No foreshadowing: ${e.foretelling_skipped_because}*`));else if(Array.isArray(e.foretelling)&&e.foretelling.length){o.push("");const u=d==="retrospective"?"Foreshadow: what the party may have heard":"Foreshadow: drop in prior sessions";o.push(`### ${u}`);for(const p of e.foretelling){const m=p.placement==="same_session"?"this session":p.placement||"mid";o.push(`- [ ] **${m}**: ${p.seed||""}`),p.plausible_cover&&o.push(`  - *If asked:* ${p.plausible_cover}`)}}return r("If the party engages",e.consequences_if_party_engages),r("If the party walks",e.consequences_if_party_walks),o.join(`
3097`)}let _brainstormThreadId=null,_brainstormSending=!1;async function openBrainstormModal(e){if(!currentCampaign)return;_brainstormThreadId=e;const t=document.getElementById("brainstorm-modal");if(!t)return;const n=(_hooksAllThreads||[]).find(a=>a.id===e);document.getElementById("brainstorm-modal-title").textContent=n&&n.title?n.title:"Hook",document.getElementById("brainstorm-conversation").innerHTML='<p class="placeholder placeholder--padded">Generating brief\u2026 this takes a few seconds the first time, instant after that.</p>',t.classList.remove("hidden");try{const a=await api("GET",`/api/campaigns/${currentCampaign.id}/threads/${e}/brainstorm`);_renderBrainstormConversation(a.messages||[])}catch(a){document.getElementById("brainstorm-conversation").innerHTML=`<p class="placeholder placeholder--padded error">Failed to open brainstorm: ${escHtml(a.message||"Unknown")}</p>`}}function closeBrainstormModal(){document.getElementById("brainstorm-modal")?.classList.add("hidden")}function _renderBrainstormConversation(e){const t=document.getElementById("brainstorm-conversation");if(t){if(!e.length){t.innerHTML='<p class="placeholder placeholder--padded">No brief generated. Ask a Surface-mode question below to get started.</p>';return}t.innerHTML=e.map(_renderBrainstormMessage).join(""),t.scrollTop=t.scrollHeight}}function _renderBrainstormMessage(e){const t=e.role==="user",n=e.mode?` brainstorm-msg--${e.mode}`:"",a=t?"":`<span class="brainstorm-msg-meta">${escHtml((e.mode||"assistant").toUpperCase())}</span>`;let s=e.content||"",i="";if(!t){const o=_parseBrainstormFollowups(s);s=o.body;const r=o.followups.map((d,u)=>`<button class="brainstorm-followup-chip" data-followup="${escHtml(d)}" title="Click to ask this"><span class="brainstorm-followup-num">${u+1}</span><span class="brainstorm-followup-text">${escHtml(d)}</span></button>`).join(""),l=`<button class="brainstorm-followup-chip brainstorm-followup-chip--custom" data-followup-custom="1" title="Type your own question"><span class="brainstorm-followup-num">${o.followups.length+1}</span><span class="brainstorm-followup-text">Write your own\u2026</span></button>`;i='<div class="brainstorm-followups"><span class="brainstorm-followups-label">Set the hook:</span>'+r+l+"</div>"}return`<div class="brainstorm-msg brainstorm-msg--${t?"user":"assistant"}${n}">`+a+`<div class="brainstorm-msg-body">${escHtml(s)}</div>`+i+"</div>"}function _parseBrainstormFollowups(e){const t=(e||"").trimEnd(),n=t.match(/\n\s*---\s*\n/);if(!n)return{body:t,followups:[]};const a=t.slice(0,n.index).trim(),s=t.slice(n.index+n[0].length),i=[];for(const o of s.split(/\r?\n/)){const r=o.match(/^\s*\d+\s*[.)\-]\s+(.+?)\s*$/);r&&r[1]&&i.push(r[1])}return i.length<2?{body:t,followups:[]}:{body:a,followups:i}}async function _sendBrainstormMessage(e,t){if(_brainstormSending||!currentCampaign||!_brainstormThreadI
3097d)return;const n=(e||"").trim();if(!n)return;_brainstormSending=!0;const a=document.getElementById("brainstorm-conversation");a&&a.querySelector(".placeholder")&&(a.innerHTML="");const s=document.createElement("div");s.className=`brainstorm-msg brainstorm-msg--user brainstorm-msg--${t}`,s.textContent=n,a?.appendChild(s);const i=document.createElement("div");i.className=`brainstorm-msg brainstorm-msg--assistant brainstorm-msg--${t}`,i.innerHTML=`<span class="brainstorm-msg-meta">${escHtml(t.toUpperCase())}</span><span class="placeholder">Thinking\u2026</span>`,a?.appendChild(i),a&&(a.scrollTop=a.scrollHeight);try{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/threads/${_brainstormThreadId}/brainstorm/messages`,{content:n,mode:t});s.replaceWith(_brainstormMessageEl(o.user_message)),i.replaceWith(_brainstormMessageEl(o.assistant_message)),a.scrollTop=a.scrollHeight}catch(o){i.innerHTML='<span class="brainstorm-msg-meta error">ERROR</span>'+escHtml(o.message||"Send failed")}finally{_brainstormSending=!1}}function _brainstormMessageEl(e){const t=document.createElement("div");return t.innerHTML=_renderBrainstormMessage(e),t.firstElementChild}document.getElementById("brainstorm-modal-close")?.addEventListener("click",closeBrainstormModal),document.getElementById("brainstorm-modal")?.addEventListener("click",e=>{e.target.id==="brainstorm-modal"&&closeBrainstormModal()}),document.getElementById("brainstorm-refresh-btn")?.addEventListener("click",async()=>{if(!currentCampaign||!_brainstormThreadId)return;const e=document.getElementById("brainstorm-refresh-btn"),t=e?e.textContent:"";e&&(e.disabled=!0,e.textContent="\u21BB \u2026"),document.getElementById("brainstorm-conversation").innerHTML='<p class="placeholder placeholder--padded">Regenerating brief against latest campaign data\u2026</p>';try{const n=await api("POST",`/api/campaigns/${currentCampaign.id}/threads/${_brainstormThreadId}/brainstorm/refresh`,{});_renderBrainstormConversation(n.messages||[])}catch(n){document.getElementById("brainstorm-conversation").innerHTML=`<p class="placeholder placeholder--padded error">Refresh failed: ${escHtml(n.message||"Unknown")}</p>`}finally{e&&(e.disabled=!1,e.textContent=t)}}),document.getElementById("brainstorm-conversation")?.addEventListener("click",e=>{const t=e.target.closest(".brainstorm-custom-send");if(t){const o=(t.closest(".brainstorm-followup-chip--expanded")?.querySelector(".brainstorm-custom-input")?.value||"").trim();o&&_sendBrainstormMessage(o,"surface");return}const n=e.target.closest(".brainstorm-followup-chip");if(!n||n.classList.contains("brainstorm-followup-chip--expanded"))return;if(n.dataset.followupCustom==="1"){_expandCustomChip(n);return}const a=n.dataset.followup||"";a&&_sendBrainstormMessage(a,"surface")});function _expandCustomChip(e){const t=e.querySelector(".brainstorm-followup-num")?.textContent||"4";e.classList.add("brainstorm-followup-chip--expanded"),e.innerHTML=`<span class="brainstorm-followup-num">${escHtml(t)}</span><input type="text" class="brainstorm-custom-input" placeholder="Type your question, Enter to send" maxlength="4000" /><button class="brainstorm-custom-send" title="Send">Send</button>`;const n=e.querySelector(".brainstorm-custom-input");n.focus(),n.addEventListener("keydown",a=>{if(a.key==="Enter"){a.preventDefault();const s=(n.value||"").trim();s&&_sendBrainstormMessage(s,"surface")}else a.key==="Escape"&&(a.preventDefault(),_collapseCustomChip(e))})}function _collapseCustomChip(e){const t=e.querySelector(".brainstorm-followup-num")?.textContent||"4";e.classList.remove("brainstorm-followup-chip--expanded"),e.innerHTML=`<span class="brainstorm-followup-num">${escHtml(t)}</span><span class="brainstorm-followup-text">Write your own\u2026</span>`}let _party2Spec=null,_party2LoadedCid=null;function _party2Panel(){let e=document.getElementById("tab-party2");if(!e){e=document.createElement("div"),e.id="tab-party2",e.className="tab-panel hidden cool-paper-surface",e.setAttribute("role","tabpanel");const t=document.getElementById("tab-compendium");t.parentNode.insertBefore(e,t.nextSibling)}return e}function _party2Show(e,t){document.querySelectorAll(".tab-panel").forEach(n=>n.classList.add("hidden")),document.querySelectorAll(".tab-btn").forEach(n=>n.classList.remove("active")),document.querySelector('[data-tab="party"]')?.classList.add("active"),e.classList.remove("hidden"),_tfStampCampaignHash(t)}function _p2Esc(e){return escHtml(e==null?"":String(e))}function _p2PipeName(e){const t=(e||"").trim(),n=t.indexOf(" ");return n<0?_p2Esc(t):_p2Esc(t.slice(0,n))+'<span class="p2-pipe">|</span>'+_p2Esc(t.slice(n+1))}const _P2_FACET_LABEL={melee:"Melee",ranged:"Ranged",control:"Control",healing:"Healing",party_face:"Party Face",perception:"Perception",mobility:"Mobility",stealth:"Stealth",concentration:"Conc.",con:"CON",dex:"DEX",int:"INT",wis:"WIS",cha:"CHA",dh_frontline:"Frontline",dh_reach:"Reach",dh_control:"Control",dh_soak:"Soak",dh_support:"Support",dh_face:"Presence",dh_awareness:"Awareness",dh_mobility:"Mobility",dh_agility:"Agility",dh_strength:"Strength",dh_finesse:"Finesse",dh_instinct:"Instinct",dh_presence:"Presence",dh_knowledge:"Knowledge"};function _p2FacetLabel(e){return _P2_FACET_LABEL[e]||e}const _P2_FACET_ICON={melee:'<polyline points="14.5 17.5 3 6 3 3 6 3 17.5 14.5"/><line x1="13" y1="19" x2="19" y2="13"/><line x1="16" y1="16" x2="20" y2="20"/><line x1="19" y1="21" x2="21" y2="19"/>',ranged:'<line x1="5" y1="19" x2="19" y2="5"/><polyline points="9 5 19 5 19 15"/><line x1="5" y1="19" x2="2" y2="14"/><line x1="5" y1="19" x2="10" y2="22"/>',control:'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',healing:'<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/><path d="M3.22 12H9.5l1.5-3 2 6 1.5-4 1 2h4.28"/>',party_face:'<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',perception:'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>
3097',mobility:'<path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/>',stealth:'<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',concentration:'<path d="M4 8 L4 4 L8 4"/><path d="M16 4 L20 4 L20 8"/><path d="M20 16 L20 20 L16 20"/><path d="M8 20 L4 20 L4 16"/><circle cx="12" cy="12" r="1.5"/><line x1="12" y1="8" x2="12" y2="10.5"/><line x1="12" y1="13.5" x2="12" y2="16"/><line x1="8" y1="12" x2="10.5" y2="12"/><line x1="13.5" y1="12" x2="16" y2="12"/>',con:'<line x1="8.5" y1="12" x2="15.5" y2="12"/><rect x="3" y="9" width="2.5" height="6" rx="1"/><rect x="5.5" y="10" width="2" height="4" rx="0.8"/><rect x="16.5" y="10" width="2" height="4" rx="0.8"/><rect x="18.5" y="9" width="2.5" height="6" rx="1"/>',dex:'<path d="M3 14.3 C3 12.3 4.4 11.3 6.3 11.3 L8.7 7.5 C9.3 6.6 10.4 6.8 10.6 7.9 L11.2 14.3"/><path d="M11.2 14.3 L16.6 14.3 C16.7 12.4 17.5 11.3 19 11.3 C20.3 11.3 21.2 12.6 21.2 14.6 C21.2 16.8 19.8 18.2 17.6 18.2 L7 18.2 C4.6 18.2 3 16.7 3 14.3 Z"/><line x1="3.5" y1="16.5" x2="20.5" y2="16.5"/><line x1="11.5" y1="9.4" x2="12.7" y2="11.5"/><line x1="13.4" y1="8.7" x2="14.7" y2="11.5"/><line x1="15.4" y1="8.9" x2="16.6" y2="11.5"/>',int:'<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/>',wis:'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="4"/><line x1="17.5" y1="3.5" x2="16.2" y2="5.2"/><line x1="21" y1="7.5" x2="19.3" y2="8.5"/><line x1="6.5" y1="3.5" x2="7.8" y2="5.2"/><line x1="3" y1="7.5" x2="4.7" y2="8.5"/>',cha:'<path d="M2 17 L4 9 L8.5 13.5 L12 6 L15.5 13.5 L20 9 L22 17 Z"/><line x1="2" y1="20" x2="22" y2="20"/>',_save:'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1Z"/>'},_DH_FACET_ICON_ALIAS={dh_frontline:"melee",dh_reach:"ranged",dh_control:"control",dh_soak:"_save",dh_support:"healing",dh_face:"party_face",dh_awareness:"perception",dh_mobility:"mobility",dh_agility:"dex",dh_strength:"con",dh_finesse:"control",dh_instinct:"perception",dh_presence:"party_face",dh_knowledge:"int"};function _p2FacetIcon(e){return _P2_FACET_ICON[e]||_P2_FACET_ICON[_DH_FACET_ICON_ALIAS[e]]||_P2_FACET_ICON._save}function _p2FacetAria(e){return`${_p2FacetLabel(e.key)}: ${e.score==null?"no data":e.score+" of "+(e.scale||7)}.`+(e.reasoning?" "+e.reasoning:"")}function _p2Masthead(e){const t=e.titlePenHtml!=null?e.titlePenHtml:e.editable&&_party2Spec&&_party2Spec.can_assess?'<button class="chr2-edit-pen" data-p2-edit-name title="Edit party name" aria-label="Edit party name">\u270E</button>':"",n=e.leadHtml?e.leadId?`<div class="chr2-field-row chr2-field-lead"><p class="index-lead" id="${e.leadId}">${e.leadHtml}</p>${e.leadPenHtml||""}</div>`:`<p class="index-lead">${e.leadHtml}</p>`:"",a=e.titleId?`<div class="chr2-field-row chr2-field-title"><h1 class="index-title" id="${e.titleId}">${e.titleHtml}</h1>${t}</div>`:`<h1 class="index-title">${e.titleHtml}</h1>`;return`<header class="index-head"><div class="index-eyebrow">${_p2Esc(e.eyebrow)}</div>`+a+n+(e.statsHtml?`<div class="index-stats">${e.statsHtml}</div>`:"")+`<div class="ornament index-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div>`+(e.actionsHtml?`<div class="p2-head-actions">${e.actionsHtml}</div>`:"")+"</header>"}function _tf2SectionPlate(e){const t=e.stats&&e.stats.length?`<span class="tf2-plate-stats">${e.stats.join('<span class="dot"></span>')}</span>`:"",n=e.titleId?`<h1 class="tf2-plate-title" id="${e.titleId}">${e.titleHtml}</h1>`:`<h1 class="tf2-plate-title">${e.titleHtml}</h1>`,a=e.leadHtml?`<p class="tf2-plate-lead">
3097${e.leadId?`<span id="${e.leadId}">${e.leadHtml}</span>`:e.leadHtml}${e.leadPen||""}</p>`:"";return`<header class="tf2-plate"><div class="tf2-plate-id"><span class="tf2-plate-eyebrow">${escHtml(e.eyebrow||"")}</span><div class="tf2-plate-titlerow">${n}${e.titlePen||""}${t}</div></div>${e.actionsHtml?`<div class="tf2-plate-actions">${e.actionsHtml}</div>`:""}</header><div class="tf2-plate-rule"></div>`+a}function _p2Colophon(e){const t=String(e||"").replace(/^The\s+/i,"");return`<div class="colophon"><span class="mark">${_CHR2_MARK_SVG} Threadfall ${_p2Esc(t)}</span><span>${_p2Esc(currentCampaign&&currentCampaign.name||"")}</span></div>`}function _p2EditPartyName(){const e=document.getElementById("p2-party-name-el");if(!e||e.dataset.editing==="1")return;e.dataset.editing="1";const t=e.closest(".chr2-field-row");t&&t.classList.add("editing");const n=e.innerHTML,a=()=>{e.innerHTML=n,e.dataset.editing="",t&&t.classList.remove("editing")},s=document.createElement("input");s.className="chr2-inline-input chr2-title-input",s.value=(currentCampaign&&currentCampaign.party_name||"").trim(),s.placeholder="Name your party";const i=document.createElement("div");i.className="chr2-editor-actions",i.innerHTML='<button type="button" class="chr2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="chr2-ed-cancel btn-ghost btn-xs">Cancel</button>';const o=document.createElement("div");o.className="chr2-field-editor",o.appendChild(s),o.appendChild(i),e.innerHTML="",e.appendChild(o),s.focus(),s.select(),i.querySelector(".chr2-ed-cancel").addEventListener("click",a),i.querySelector(".chr2-ed-save").addEventListener("click",async()=>{const r=s.value.trim();await _chr2SaveCampaignField("party_name",r)&&(e.dataset.editing="",t&&t.classList.remove("editing"),_party2Spec&&(_party2Spec.party_name=r||"The Party"),e.textContent=r||"The Party")}),s.addEventListener("keydown",r=>{r.key==="Escape"?(r.preventDefault(),a()):r.key==="Enter"&&(r.preventDefault(),i.querySelector(".chr2-ed-save").click())})}function _p2Poly(e,t,n,a){const s=[];for(let i=0;i<a;i++){const o=(-90+i*360/a)*Math.PI/180;s.push([e+n*Math.cos(o),t+n*Math.sin(o)])}return s}function _p2Band(e,t,n,a,s,i){const o=(-90+s*360/i)*Math.PI/180,r=(-90+(s+1)*360/i)*Math.PI/180,c=(l,d)=>`${(e+l*Math.cos(d)).toFixed(1)},${(t+l*Math.sin(d)).toFixed(1)}`;return`${c(n,o)} ${c(n,r)} ${c(a,r)} ${c(a,o)}`}function _p2DiceSvg(e,t,n){if(!e||!e.length)return"";const a=n.cx,s=n.cy,i=n.maxR,o=e[0].scale||7,r=e[0].par||4,c=i+n.lbl;let l=`<svg class="p2-radar" viewBox="0 0 ${n.w} ${n.h}" role="group" aria-label="${_p2Esc(n.label)}">`;for(let p=1;p<=o;p++){const m=_p2Poly(a,s,i*p/o,t).map(f=>f[0].toFixed(1)+","+f[1].toFixed(1)).join(" ");l+=`<polygon class="p2-ring${p===o?" p2-ring-out":""}" points="${m}"/>`}const d=i*r/o;e.forEach((p,m)=>{const f=p.score==null?0:p.score,g=i*Math.min(f,o)/o,h=i*Math.min(f,r)/o;f<r&&(l+=`<polygon class="p2-shortfall p2-grp-${p.group}" points="${_p2Band(a,s,g,d,m,t)}"/>`),f>r&&(l+=`<polygon class="p2-surplus p2-grp-${p.group}" points="${_p2Band(a,s,d,g,m,t)}"/>`),f>0&&(l+=`<polygon class="p2-wedge p2-grp-${p.group}" points="${_p2Band(a,s,0,h,m,t)}"/>`)});const u=_p2Poly(a,s,d,t).map(p=>p[0].toFixed(1)+","+p[1].toFixed(1)).join(" ");l+=`<polygon class="p2-ring-par" points="${u}"/>`;for(let p=0;p<t;p++){const m=(-90+p*360/t)*Math.PI/180;l+=`<line class="p2-spoke" x1="${a}" y1="${s}" x2="${(a+i*Math.cos(m)).toFixed(1)}" y2="${(s+i*Math.sin(m)).toFixed(1)}"/>`}return e.forEach((p,m)=>{const f=(-90+m*360/t+180/t)*Math.PI/180,g=a+c*Math.cos(f),h=s+c*Math.sin(f);l+=`<g class="p2-ricon p2-grp-${p.group}" transform="translate(${(g-8.4).toFixed(1)},${(h-22).toFixed(1)}) scale(0.7)">${_p2FacetIcon(p.key)}</g>`,l+=`<text class="p2-rlabel p2-grp-${p.group}" x="${g.toFixed(1)}" y="${(h+6).toFixed(1)}" text-anchor="middle">${_p2Esc(_p2FacetLabel(p.key))}</text>`,l+=`<rect class="p2-hit p2-hit--word" data-facet="${_p2Esc(p.key)}" x="${(g-40).toFixed(1)}" y="${(h-24).toFixed(1)}" width="80" height="38" rx="8"/>`;const v=i*r/o*.6,b=a+v*Math.cos(f),y=s+v*Math.sin(f),_=b.toFixed(1),w=y.toFixed(1),E=`transform="rotate(${(f*180/Math.PI-90).toFixed(1)},${_},${w})"`,k=p.score==null?"\u2013":p.score;
3097l+=`<text class="p2-rnum p2-rnum-shadow p2-grp-${p.group}" x="${_}" y="${w}" text-anchor="middle" dominant-baseline="central" ${E} dx="-0.6" dy="-0.6" aria-hidden="true">${k}</text>`,l+=`<text class="p2-rnum p2-rnum-hi p2-grp-${p.group}" x="${_}" y="${w}" text-anchor="middle" dominant-baseline="central" ${E} dx="0.6" dy="0.6" aria-hidden="true">${k}</text>`,l+=`<text class="p2-rnum p2-grp-${p.group}" x="${_}" y="${w}" text-anchor="middle" dominant-baseline="central" ${E}>${k}</text>`;const C=(-90+m*360/t)*Math.PI/180,S=(-90+(m+1)*360/t)*Math.PI/180,q=(I,T)=>`${(a+I*Math.cos(T)).toFixed(1)},${(s+I*Math.sin(T)).toFixed(1)}`;l+=`<polygon class="p2-hit p2-grp-${p.group}" data-facet="${_p2Esc(p.key)}" tabindex="0" role="button" aria-label="${_p2Esc(_p2FacetAria(p))}" points="${a},${s} ${q(i,C)} ${q(i,S)}"/>`}),l+="</svg>",l}const _P2_FRO_GROUP_CLASSES=["p2-fro--combat","p2-fro--noncombat","p2-fro--save","p2-fro--dhbody","p2-fro--dhmind"];function _P2_GROUP_LABEL(e){return e==="dhbody"?"Body":e==="dhmind"?"Mind":e==="combat"?"Combat":e==="save"?"Saves":"Non-combat"}function _p2WhoChips(e){return!e||!e.length?"":'<footer class="p2-fro-who">'+e.slice(0,2).map(t=>`<span class="p2-fro-coin" title="${_p2Esc(t.name)}">`+(t.image_url?`<img class="p2-fro-coin-img" src="${_p2Esc(t.image_url)}" alt="">`:`<span class="p2-fro-coin-init">${_p2Esc(t.initial||(t.name||"?").slice(0,1))}</span>`)+`<span class="p2-fro-coin-name">${_p2Esc(t.name)}</span></span>`).join("")+"</footer>"}function _p2ShowFacet(e){const t=document.getElementById("p2-facet-readout");if(!t)return;const n=_party2Spec&&_party2Spec._facetMap&&_party2Spec._facetMap[e];if(t.classList.remove(..._P2_FRO_GROUP_CLASSES,"p2-fro--active"),!n){delete t.dataset.facet,t.innerHTML=`<div class="p2-fro-default"><p class="p2-facet-hint">${_p2Esc(t.dataset.default||"Hover, focus, or tap a facet for the reasoning.")}</p></div>`;return}t.dataset.facet=e,t.classList.add("p2-fro--"+(n.group||"combat"),"p2-fro--active");const a=n.scoreLabel!=null?n.scoreLabel:n.score==null?"\u2013":n.score,s=n.scale!=null?`<span class="p2-fro-denom">/${n.scale}</span>`:"";t.innerHTML=`<div class="p2-fro-live"><div class="p2-fro-hero"><span class="p2-fro-icon"><svg viewBox="0 0 24 24" class="p2-ricon p2-grp-${n.group}" aria-hidden="true">${_p2FacetIcon(n.key)}</svg></span><span class="p2-fro-name">${_p2Esc(_p2FacetLabel(n.key))}</span><span class="p2-fro-figure"><span class="p2-fro-score">${a}</span>${s}</span></div>`+(n.reasoning?`<p class="p2-fro-why">${_p2Esc(n.reasoning)}</p>`:'<p class="p2-fro-why p2-facet-hint">No sheet data for this facet.</p>')+(n.dc_line&&_party2Spec&&_party2Spec.can_assess?`<p class="p2-fro-dc">${_p2Esc(n.dc_line)}</p>`:"")+_p2WhoChips(n.who)+"</div>"}let _p2FacetEscHandler=null;function _p2CloseFacetModal(){document.getElementById("p2-facet-modal-ov")?.remove(),_p2FacetEscHandler&&(document.removeEventListener("keydown",_p2FacetEscHandler),_p2FacetEscHandler=null)}function _p2OpenFacetModal(e){const t=_party2Spec&&_party2Spec._facetMap&&_party2Spec._facetMap[e];if(!t)return;_p2CloseFacetModal();const n=t.scoreLabel!=null?t.scoreLabel:t.score==null?"\u2013":t.score,a=t.scale!=null?`<span class="p2-fro-denom">/${t.scale}</span>`:"",s=document.createElement("div");s.id="p2-facet-modal-ov",s.className="modal-overlay p2-facet-modal-ov cool-paper-surface p2-fro--"+(t.group||"combat"),s.innerHTML=`<div class="p2-facet-modal" role="dialog" aria-modal="true" aria-label="${_p2Esc(_p2FacetLabel(t.key))} detail"><button class="p2-facet-modal-x" type="button" aria-label="Close">&times;</button><span class="p2-fro-eyebrow">${_p2Esc(_P2_GROUP_LABEL(t.group))}</span><div class="p2-fro-hero"><span class="p2-fro-icon"><svg viewBox="0 0 24 24" class="p2-ricon p2-grp-${t.group}" aria-hidden="true">${_p2FacetIcon(t.key)}</svg></span><span class="p2-fro-name">${_p2Esc(_p2FacetLabel(t.key))}</span><span class="p2-fro-figure"><span class="p2-fro-score">${n}</span>${a}</span></div>`+(t.reasoning?`<p class="p2-facet-modal-why">${_p2Esc(t.reasoning)}</p>`:'<p class="p2-facet-modal-why p2-facet-hint">No sheet data for this facet.</p>
3097')+(t.dc_line&&_party2Spec&&_party2Spec.can_assess?`<p class="p2-fro-dc p2-fro-dc--modal">${_p2Esc(t.dc_line)}</p>`:"")+_p2WhoChips(t.who)+"</div>",s.addEventListener("click",i=>{i.target===s&&_p2CloseFacetModal()}),s.querySelector(".p2-facet-modal-x").addEventListener("click",_p2CloseFacetModal),document.body.appendChild(s),_p2FacetEscHandler=i=>{_tfModalEsc(i,document.getElementById("p2-facet-modal-ov"))&&_p2CloseFacetModal()},document.addEventListener("keydown",_p2FacetEscHandler)}function _p2Glyph(){return'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 3v5c0 4.5-3 8-7 10-4-2-7-5.5-7-10V6z"/><circle cx="12" cy="9.5" r="1.8"/><path d="M8.8 15c.6-1.6 1.9-2.4 3.2-2.4s2.6.8 3.2 2.4"/></svg>'}function _p2Portrait(e,t){const n=e.portrait_offset_x!=null||e.portrait_offset_y!=null?` style="object-position:${e.portrait_offset_x||50}% ${e.portrait_offset_y||50}%"`:"";return e.image_url?`<div class="p2-portrait ${t||""}"><img src="${_p2Esc(e.image_url)}" alt="${_p2Esc(e.name)}"${n}></div>`:`<div class="p2-portrait p2-portrait--glyph ${t||""}">${_p2Glyph()}</div>`}function _p2ClassLine(e){const n=[e.classes&&e.classes.length?e.classes.join(" / "):null,e.species].filter(Boolean);return n.length?n.join(" \xB7 "):null}function _p2QuoteInner(e){return`<span class="p2-quote-txt">${_p2Esc(e.quote)}</span>`+(e.context?`<cite>${_p2Esc(e.context)}</cite>`:"")+(e&&e.id!=null?_p2ReactionBar(e):"")}const _P2_REACTIONS=[["like","\u{1F44D}","liked",!0],["love","\u2764\uFE0F","loved",!0],["laugh","\u{1F602}","laughed at",!0],["celebrate","\u{1F389}","celebrated",!0],["fire","\u{1F525}","hyped",!1],["gasp","\u{1F62E}","gasped at",!1],["bravo","\u{1F44F}","applauded",!1],["moved","\u{1F972}","teared up at",!1],["nat20","\u{1F3B2}","rolled a 20 on",!1],["respect","\u{1F64F}","saluted",!1],["hundred","\u{1F4AF}","co-signed",!1],["eyes","\u{1F440}","eyed",!1]],_P2_REACT_META=Object.fromEntries(_P2_REACTIONS.map(([e,t,n])=>[e,{e:t,verb:n}]));function _p2HasReactions(e){const t=e&&e.reactions||{};return _P2_REACTIONS.some(([n])=>(t[n]||0)>0)}function _p2NameList(e){const t=(e||[]).filter(Boolean);return t.length?t.length===1?t[0]:t.length===2?`${t[0]} & ${t[1]}`:t.length===3?`${t[0]}, ${t[1]} & ${t[2]}`:`${t[0]}, ${t[1]} & ${t.length-2} others`:""}function _p2ReactBtn1(e,t){const n=t&&t.reactions||{},a=t&&t.mine||[],s=t&&t.reactors||{},i=_P2_REACT_META[e];if(!i)return"";const o=n[e]||0,r=_p2NameList(s[e]),c=r?` title="${escHtml(`${r} ${i.verb} this`)}"`:"";return`<button class="p2-react-btn${a.includes(e)?" is-mine":""}" type="button" data-react="${e}"${c} aria-label="${e}" aria-pressed="${a.includes(e)}"><span class="p2-react-emo">${i.e}</span>${o>0?`<span class="p2-react-n">${o}</span>`:""}</button>`}function _p2ReactBtns(e){const t=e&&e.reactions||{};return _P2_REACTIONS.filter(([a,,,s])=>s||(t[a]||0)>0).map(([a])=>a).map(a=>_p2ReactBtn1(a,e)).join("")+'<button class="p2-react-more" type="button" data-react-more aria-label="More reactions" title="More reactions">+</button>'}function _p2ReactionBar(e){return!e||!e.id?"":`<div class="p2-react${_p2HasReactions(e)?" has-reactions":""}" data-quote-id="${e.id}">${_p2ReactBtns(e)}</div>`}function _p2ReactBarSet(e,t){!e||!t||!t.id||(e.dataset.quoteId=t.id,e.classList.toggle("has-reactions",_p2HasReactions(t)),e.innerHTML=_p2ReactBtns(t))}function _p2Quote(e){const t=e.quotes&&e.quotes.length?e.quotes:e.quote?[e.quote]:[];return!t.length||!t[0].quote?"":`<blockquote class="p2-quote" data-quote-pc="${e.id}" title="Make a shareable quote card">${_p2QuoteInner(t[0])}</blockquote>`}let _p2QuoteTimers=[];function _p2StopQuoteCycle(){_p2QuoteTimers.forEach(e=>clearInterval(e)),_p2QuoteTimers=[]}function _p2StartQuoteCycle(e){if(_p2StopQuoteCycle(),!e||!_party2Spec)return;const t={};["active","departed","fallen"].forEach(n=>(_party2Spec[n]||[]).forEach(a=>{t[a.id]=a})),e.querySelectorAll(".p2-quote[data-quote-pc]").forEach(n=>{const a=t[+n.dataset.quotePc],s=a&&a.quotes||[];if(!s.length)return;const i=n.querySelector(".p2-quote-txt");let o=s;if(i){const l=getComputedStyle(i),d=parseFloat(l.lineHeight)||parseFloat(l.fontSize)*1.5||21,u=document.createElement("span");u.style.cssText="position:absolute;
3097left:-9999px;top:0;visibility:hidden;display:block;white-space:normal;",u.style.width=(i.clientWidth||210)+"px",u.style.fontFamily=l.fontFamily,u.style.fontSize=l.fontSize,u.style.fontStyle=l.fontStyle,u.style.fontWeight=l.fontWeight,u.style.lineHeight=l.lineHeight,u.style.letterSpacing=l.letterSpacing,document.body.appendChild(u);const p=s.filter(m=>(u.textContent=m.quote||"",u.offsetHeight<=d*2+2));document.body.removeChild(u),o=p.length?p:[s.slice().sort((m,f)=>(m.quote||"").length-(f.quote||"").length)[0]]}let r=0;if(n.innerHTML=_p2QuoteInner(o[0]),n._p2q=o[0],o.length<2)return;const c=()=>n.matches(":hover")||n.contains(document.activeElement)||_p2PickerBar&&n.contains(_p2PickerBar);_p2QuoteTimers.push(setInterval(()=>{c()||(n.style.transition="opacity 0.45s ease",n.style.opacity="0",setTimeout(()=>{if(c()){n.style.opacity="1";return}r=(r+1)%o.length,n.innerHTML=_p2QuoteInner(o[r]),n._p2q=o[r],n.style.opacity="1"},450))},7e3))})}function _p2Weapons(e){const t=e&&e.dh_weapons||[];return t.length?'<div class="p2-wpns">'+t.map(n=>`<span class="p2-wpn" title="${_p2Esc([n.name,n.trait,n.range,n.damage].filter(Boolean).join(" \xB7 "))}"><b>${_p2Esc(n.name)}</b>`+(n.range?`<i class="p2-wpn-rng">${_p2Esc(n.range)}</i>`:"")+"</span>").join("")+"</div>":""}function _p2Cards(e){const t=e&&e.dh_cards||[];if(!t.length)return"";const n=a=>[a.domain,a.level?"Level "+a.level:null,a.range,a.damage_type==="mag"?"Magic damage":a.damage_type==="phy"?"Physical damage":a.damage_type==="both"?"Physical & magic damage":null,a.hope_cost?"Costs "+a.hope_cost+" Hope":null,a.in_loadout===!1&&a.recall_cost?"Recall Cost: "+a.recall_cost+" Stress":null,(a.touches||[]).indexOf("stress")>=0?"Marks Stress to use":null,(a.touches||[]).indexOf("clr_stress")>=0?"Clears Stress":null,(a.touches||[]).indexOf("clr_hp")>=0?"Clears Hit Points":null,(a.touches||[]).indexOf("ally")>=0?"Affects an ally":null,(a.touches||[]).indexOf("armor")>=0?"Uses an Armor slot":null,a.in_loadout===!1?"In the vault":null].filter(Boolean).join(" \xB7 ");return'<div class="p2-exps p2-cards"><span class="form-section-label">Domain cards</span><div class="p2-exp-row">'+t.map(a=>`<span class="p2-exp p2-card${a.in_loadout===!1?" is-vault":""}" title="${_p2Esc(a.name+(n(a)?" \u2014 "+n(a):""))}">${_p2Esc(a.name)}</span>`).join("")+"</div></div>"}function _p2Exps(e){return!e.dh_experiences||!e.dh_experiences.length?"":'<div class="p2-exps"><span class="form-section-label">Experiences</span><div class="p2-exp-row">'+e.dh_experiences.map(t=>{const n=String(t.name||""),a=n.split(/\s[-–—]\s/)[0].trim()||n;return`<span class="p2-exp"${a!==n?` title="${_p2Esc(n)}"`:""}>${_p2Esc(a)}</span>`}).join("")+"</div></div>"}function _p2Leads(e){return!e.leads||!e.leads.length?"":'<div class="p2-leads">'+e.leads.map(t=>`<span class="p2-lead">${_p2Esc(t.label)} <b>${_p2Esc(t.value)}</b></span>`).join("")+"</div>"}function _p2Companions(e){return!e.companions||!e.companions.length?"":'<div class="p2-comps"><span class="p2-comps-lbl">Bonded</span>'+e.companions.map(t=>{const n=(t.image_url||"").trim(),a=n?`<img src="${escHtml(n)}" alt="" loading="lazy">`:'<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#tf-glyph-companion"/></svg>';return`<span class="entity-link p2-comp" data-id="${t.id}" tabindex="0" role="button" title="${_p2Esc(t.name)}" aria-label="${_p2Esc(t.name)}">${a}</span>`}).join("")+"</div>"}function _p2FlankCard(e,t){const n=_p2ClassLine(e);let a="";if(e.dh_vitals){const f=e.dh_vitals,g=f.gm||{},h=(y,_,w)=>y==null||y===""?"":`<span${w?` title="${_p2Esc(w)}"`:""}><b>${_p2Esc(String(y))}</b>${_}</span>`,v=g.major!=null||g.severe!=null?h((g.major!=null?g.major:"\u2014")+"/"+(g.severe!=null?g.severe:"\u2014"),"THR","Damage thresholds, Major / Severe"):"",b=_party2Spec&&_party2Spec.can_assess?v+h(g.hp_max,"HP","Maximum HP")+h(g.stress_max,"STR","Maximum Stress"):"";a='<div class="p2-flank-vit">'+h(f.evasion,"EVA","Evasion \u2014 what an attack has to beat")+h(f.armor_score,"ARM","Armor Score")+b+"</div>"}else if(e.vitals){const f=e.vitals,g=f.extra_speeds||{},h=["fly","swim","climb","burrow"].filter(b=>g[b]).map(b=>b.charAt(0).toUpperCase()+b.slice(1)+" "+g[b]),v=h.length?` title="Walking ${f.speed} ft \xB7 ${h.join(" \xB7 ")} ft"`:"";a='<div class="p2-flank-vit">'+(f.ac!=null?`<span><b>${f.ac}</b>AC</span>`:"")+(f.hp!=null?`<span><b>${f.hp}</b>HP</span>`:"")+(f.init!=null?`<span><b>${f.init>=0?"+"+f.init:f.init}</b>
3097INIT</span>`:"")+(f.pp!=null?`<span><b>${f.pp}</b>PP</span>`:"")+(f.speed!=null?`<span${v}><b>${f.speed}</b>SPD${h.length?' <span class="p2-flank-spd-x">\u2726</span>':""}</span>`:"")+"</div>"}else{const f=e.tenure;a=`<div class="p2-flank-tenure">${f?_p2Esc(f.sessions+(f.sessions===1?" session":" sessions")+(f.first&&f.first.n?" \xB7 since Ch. "+f.first.n:"")):"No sheet imported"}</div>`}const s=e.role_summary||[e.party_role,e.noncombat_role].filter(Boolean).join(" \xB7 "),i=s?`<div class="p2-flank-role"${e.role_assessment?` title="${_p2Esc(e.role_assessment)}"`:""}>${_p2Esc(s)}</div>`:"",o=e.image_url||"",r=e.focal_x!=null&&e.focal_x>=0?Math.max(30,Math.min(70,e.focal_x)):50,c=!!(_party2Spec&&_party2Spec.can_assess),l=o?`<div class="p2-cardart"><img class="p2-cardart-img" src="${escHtml(o)}" alt="" draggable="false" style="--p2-focal-x:${r}%"></div>`:"",d=c||!!e.owned_by_me,u=!d&&!!e.assignable&&!!(currentUser&&currentUser.id),p=!!(e.vitals||e.dh_vitals||e.stat_source),m=_pcSheetControlHtml(e,p,{canImport:d,canClaim:u,editAttr:"data-p2-edit-pc"});return`<article class="p2-flank-card ${t||""}">`+l+`<span class="entity-link p2-flank-name pipe" data-id="${e.id}" tabindex="0" role="button">${_p2PipeName(e.name)}</span>`+(n?`<div class="p2-flank-cls">${_p2Esc(n)}</div>`:"")+_p2Domains(e)+(e.player_name?`<div class="p2-flank-player">played by ${_p2Esc(e.player_name)}</div>`:"")+_p2Quote(e)+a+_p2Weapons(e)+_p2Leads(e)+i+_p2Exps(e)+_p2Features(e)+_p2Cards(e)+_p2Companions(e)+m+"</article>"}function _p2Features(e){const t=e&&e.dh_features||[],n=e&&e.dh_hope_feature;if(!t.length&&!n)return"";const a=t.map(i=>`<span class="p2-feat">${_p2Esc(i)}</span>`).join(""),s=n?`<span class="p2-feat p2-feat--hope" title="Hope feature, costs ${n.cost} Hope">${_p2Esc(n.name)}<span class="p2-feat-cost">${n.cost}</span></span>`:"";return`<div class="p2-feats"><span class="form-section-label">Features</span><div class="p2-feat-row">${a}${s}</div></div>`}function _p2Domains(e){const t=e&&e.dh_domains||[];return t.length?'<div class="p2-flank-doms" title="Domain card pools this class draws from">'+t.map(n=>`<span class="p2-dom">${_p2Esc(n)}</span>`).join("")+"</div>":""}function _p2FullAssessment(e){const t=(a,s,i)=>!s||!s.length?"":`<div class="p2-rk-group p2-rk-group--${i}"><div class="p2-rk-eyebrow">${a}</div>`+s.map(o=>!(o.detail&&String(o.detail).trim())?`<div class="p2-rk-item"><span class="p2-rk-label">${_p2Esc(o.label||"")}</span></div>`:`<div class="p2-rk-item p2-rk-item--toggle" tabindex="0" role="button" aria-expanded="false"><span class="p2-rk-label">${_p2Esc(o.label||"")}<span class="p2-rk-caret" aria-hidden="true">\u25B8</span></span><span class="p2-rk-detail">${_p2Esc(o.detail)}</span></div>`).join("")+"</div>";let n="";return e.summary&&(n+=`<p class="p2-rk-summary">${_p2Esc(e.summary)}</p>`),n+='<div class="p2-rk-grid">'+t("Strengths",e.strengths,"str")+t("Gaps",e.gaps,"gap")+t("Synergies",e.synergies,"syn")+t("Watch-outs",e.watch_outs,"watch")+"</div>",e.tip&&(n+=`<div class="p2-rk-tip"><span class="p2-rk-tip-lbl">Key Tip</span><span class="p2-rk-tip-txt">${_p2Esc(e.tip)}</span></div>`),n}let _p2CampSeq=0;function _p2CampfireSvg(){const e=++_p2CampSeq;return`<svg class="p2-camp-fire" viewBox="0 0 120 120" aria-hidden="true">
3098    <defs><radialGradient id="p2glow${e}">
3099      <stop offset="0%" stop-color="#f59d3d" stop-opacity="0.5"/>
3100      <stop offset="55%" stop-color="#e0762e" stop-opacity="0.16"/>
3101      <stop offset="100%" stop-color="#e0762e" stop-opacity="0"/>
3102    </radialGradient></defs>
3103    <circle cx="60" cy="60" r="56" fill="url(#p2glow${e})"/>
3104    <ellipse cx="89.5" cy="65.2" rx="6.5" ry="4.5" transform="rotate(100 89.5 65.2)" fill="#8d8779"/>
3105    <ellipse cx="79.3" cy="83" rx="6" ry="4.2" transform="rotate(140 79.3 83)" fill="#7c7668"/>
3106    <ellipse cx="60" cy="90" rx="7" ry="4.6" fill="#96907f"/>
3107    <ellipse cx="40.7" cy="83" rx="6.2" ry="4.3" transform="rotate(40 40.7 83)" fill="#8d8779"/>
3108    <ellipse cx="30.5" cy="65.2" rx="6.4" ry="4.4" transform="rotate(80 30.5 65.2)" fill="#7c7668"/>
3109    <ellipse cx="34" cy="45" rx="6" ry="4.2" transform="rotate(120 34 45)" fill="#96907f"/>
3110    <ellipse cx="49.7" cy="31.8" rx="6.6" ry="4.5" transform="rotate(160 49.7 31.8)" fill="#8d8779"/>
3111    <ellipse cx="70.3" cy="31.8" rx="6.2" ry="4.3" transform="rotate(20 70.3 31.8)" fill="#7c7668"/>
3112    <ellipse cx="86" cy="45" rx="6.4" ry="4.4" transform="rotate(60 86 45)" fill="#96907f"/>
3113    <g transform="rotate(20 60 60)"><rect x="56.5" y="33" width="7" height="21" rx="3.5" fill="#6b4a32"/></g>
3114    <g transform="rotate(110 60 60)"><rect x="56.5" y="33" width="7" height="21" rx="3.5" fill="#5d4029"/></g>
3115    <g transform="rotate(200 60 60)"><rect x="56.5" y="33" width="7" height="21" rx="3.5" fill="#6b4a32"/></g>
3116    <g transform="rotate(290 60 60)"><rect x="56.5" y="33" width="7" height="21" rx="3.5" fill="#5d4029"/></g>
3117    <g class="p2-fl"><circle cx="60" cy="60" r="15" fill="#e0762e" opacity="0.95"/><circle cx="60" cy="60" r="10.5" fill="#f59d3d"/></g>
3118    <g class="p2-fl p2-fl--b"><circle cx="60" cy="60" r="6.5" fill="#ffd166"/><circle cx="60" cy="60" r="3" fill="#fff3c4"/></g>
3119  </svg>`}function _p2CampRng(e){let t=e>>>0;return()=>{t|=0,t=t+1831565813|0;let n=Math.imul(t^t>>>15,1|t);return n=n+Math.imul(n^n>>>7,61|n)^n,((n^n>>>14)>>>0)/4294967296}}function _p2CampfireArt(e){const t=e.active||[];if(!t.length||!currentCampaign||currentCampaign.id!==e.campaign_id||!Array.isArray(allEntities)||!allEntities.length)return"";const n=[];if(t.forEach(d=>{const u=allEntities.find(m=>m.id===d.id),p=u&&typeof _tfLedgerTokList=="function"?_tfLedgerTokList(u):[];p.length&&n.push({id:d.id,name:d.name||"PC",classes:(d.classes||[]).join(" "),url:p[0].raw.replace("/upload/","/upload/c_scale,w_200/"),px:p[0].px})}),!n.length)return"";const a=n.length;let s=(e.campaign_id||0)*2654435761>>>0;n.forEach(d=>{s=(s^Math.imul(d.id,2246822519))>>>0});const i=_p2CampRng(s),o=(d,u)=>d+i()*(u-d),r=[];if(a===1)r.push({t:n[0],ang:o(0,2*Math.PI),rad:o(.5,.62),out:!1,jit:o(-20,20),sm:1});else if(a===2){const d=o(0,2*Math.PI),u=o(2.4,2.9);n.forEach((p,m)=>r.push({t:p,ang:d+m*u,rad:o(.55,.7),out:!1,jit:o(-15,15),sm:o(.94,1.06)}))}else{let d=n.findIndex(y=>/ranger|rogue|scout/i.test(y.classes));d<0&&(d=Math.floor(i()*a));const u=n[d],p=n.filter((y,_)=>_!==d),m=p.length,f=o(0,2*Math.PI),g=o(.55,.75),h=(2*Math.PI-g*2)/m,v=p.map((y,_)=>f+g+h*(_+.5)+o(-.25,.25)*h);if(m>=4){const y=Math.floor(i()*(m-1)),_=(v[y]+v[y+1])/2;v[y]+=(_-v[y])*.45,v[y+1]+=(_-v[y+1])*.45}const b=m>=3&&i()<.6?Math.floor(i()*m):-1;p.forEach((y,_)=>{const w=_===b;r.push({t:y,ang:v[_],rad:w?o(.48,.6):o(.72,.95),out:!1,jit:w?o(-6,6):o(-18,18),sm:(w?1.06:1)*o(.94,1.06)})}),r.push({t:u,ang:f+o(-.15,.15),rad:o(1.05,1.18),out:!0,jit:o(-15,15),sm:o(1,1.05)})}const c=Math.min(1,6.5/a),l=r.map(d=>{const u=95*d.rad*Math.cos(d.ang),p=64*d.rad*Math.sin(d.ang),m=Math.max(9,Math.min(91,50+u/280*100)),f=Math.max(13,Math.min(87,50+p/200*100)),h=((Math.atan2(p,u)*180/Math.PI+(d.out?270:90)+d.jit)%360+360)%360,v=
3119Math.max(.5,Math.min(2,d.t.px/280))*c*d.sm;return`<button type="button" class="p2-camp-tok" data-p2-camp-pc="${d.t.id}" title="${_p2Esc(d.t.name)}" aria-label="${_p2Esc(d.t.name)}" style="--seat-x:${m.toFixed(1)}%;--seat-y:${f.toFixed(1)}%;--seat-s:${v.toFixed(2)};--seat-r:${h.toFixed(0)}deg"><img src="${d.t.url}" alt="" loading="lazy"></button>`}).join("");return`<div class="p2-camp">${_p2CampfireSvg()}${l}</div>`}function _p2Assessment(e){let t=(e.tldr||"").trim();if(!t&&e.summary){const g=e.summary.trim().match(/^[\s\S]*?[.!?](\s|$)/);t=(g?g[0]:e.summary).trim()}if(!t)return"";const n=e.party_portrait||{},a=!!e.can_assess,s=(n.image_url||"").trim(),i=n.entity_id,o=a&&i?`<button class="p2-reck-edit" type="button" data-p2-reck-edit="${i}" title="Change the party image" aria-label="Change the party image"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l4-4 3 3 4-5 3 4"/><circle cx="8.5" cy="9.5" r="1.1"/></svg></button>`:"";let r="";if(s)r=`<div class="ft-art"><div class="art art--dissolve-r scene-img" role="img" aria-label="The party" style="background-image:url('${encodeURI(s)}')"></div>${o}</div>`;else{const g=_p2CampfireArt(e);g?r=`<div class="ft-art">${g}${o}</div>`:a&&i&&(r=`<div class="ft-art ft-art--empty"><button class="p2-reck-addimg" type="button" data-p2-reck-edit="${i}">+ Add a party image</button></div>`)}const c=r?"p2-reckoning-card":"p2-reckoning-card p2-reckoning-card--noart",l=e.dice||{},d=l.with_data,u=l.active_total,m=typeof d=="number"&&typeof u=="number"&&u>0&&d<u?`${d} of ${u} party sheets imported, Party Sync \u2192`:"Party Sync extension \u2192",f=a&&!(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart())?`<div class="ft-meta p2-reck-meta"><a class="open" href="https://chromewebstore.google.com/detail/lbkabfefehjgfoeackpiomkikhbnbndo?utm_source=threadfall-party2-reckoning" target="_blank" rel="noopener noreferrer">${m}</a></div>`:"";return`<section class="p2-reckoning-sec"><div class="${c}">`+r+`<div class="ft-body"><span class="ft-tag">Assessment</span><h2 class="ft-title">An Honest Reckoning</h2><p class="ft-teaser">${_p2Esc(t)}</p>${f}</div></div></section>`}function _p2DiceAssessing(){const e=document.getElementById("p2-facet-readout");e&&(e.classList.remove(..._P2_FRO_GROUP_CLASSES,"p2-fro--active"),e.innerHTML="");const t=document.querySelector(".p2-dice-sec .p2-dice-hint");t&&(t.textContent="Reading the party; this refreshes automatically in 1\u20132 minutes.",t.classList.add("p2-dice-hint--working")),document.querySelectorAll(".p2-dice-wrap .p2-die").forEach(n=>{if(n.querySelector(".p2-die-spin"))return;const a=document.createElement("div");a.className="p2-die-spin",a.innerHTML=typeof _tfSpinnerSvg=="function"?_tfSpinnerSvg():"",n.classList.add("p2-die--assessing"),n.appendChild(a)})}async function _p2Reassess(e){const t=e.currentTarget,n=_party2Spec&&_party2Spec.campaign_id||currentCampaign&&currentCampaign.id;if(n){t.disabled=!0,t.innerHTML="Assessing\u2026",_p2DiceAssessing();try{await api("POST",`/api/campaigns/${n}/party-assessment`),await renderParty2({cid:n})}catch(a){typeof showToast=="function"&&showToast("Re-assess failed: "+(a.message||a),5e3),await renderParty2({cid:n})}}}function _p2ReassessBtn(e,t){return e.can_assess?e.can_reassess!==!1?`<button class="p2-reassess btn-ghost btn-xs" data-p2-reassess title="${t==="Re-assess"?"Regenerate the AI party analysis (the party has changed since the last run)":"Generate the AI party analysis"}">\u2726 ${t}</button>`:`<button class="p2-reassess btn-ghost btn-xs" aria-disabled="true" title="No party changes since the last assessment. Level up, add, or remove a member to re-assess.">\u2726 ${t}</button>`:""}function _p2Trunc(e,t){if(e=(e||"").trim(),e.length<=t)return e;let n=e.slice(0,t);const a=n.lastIndexOf(" ");return a>t*.6&&(n=n.slice(0,a)),n.replace(/[\s,;:.–—-]+$/,"")+"\u2026"}function _p2StandsLine(e){const t=e.last_session,n=e.campaign_id;let a="";if(t&&t.id){const o=t.no!=null?`Chapter ${_chr2Roman(t.no)}`:"Last chapter",r=t.date?new Date(String(t.date).length===10?t.date+"T12:00:00":t.date).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"}):"",c=r?`${o} \xB7 ${r}`:o,l=_p2Trunc(t.blurb||"",116);a=`<a class="p2-resume-last" href="#chronicle/${n}/${t.id}" data-p2-resume="${t.id}" title="Open the recap for ${_p2Esc(o)}"><span class="p2-resume-lbl">Last we left off</span><span class="p2-resume-snip">${_p2Esc(l)}</span><span class="p2-resume-meta">${_p2Esc(c)}<span class="p2-resume-go" aria-hidden="true">Read recap \u2192</span></span></a>`}let s="";const i=e.next_session_at?new Date(e.next_session_at):null;if(i&&!isNaN(i.getTime())){const o=i.toLocaleDateString(void 0,{weekday:"short",month:"short",day:"numeric"}),r=e.current_chapter!=null?` \xB7 Chapter ${_chr2Roman(e.current_chapter+1)}`:"";s=`<div class="p2-resume-next"><span class="p2-resume-lbl p2-resume-lbl--next">Next session</span><span class="p2-resume-when">${_p2Esc(o)}${_p2Esc(r)}
3119</span></div>`}else a&&(s='<div class="p2-resume-next"><span class="p2-resume-lbl p2-resume-lbl--next">Next session</span><span class="p2-resume-when p2-resume-when--none">not yet scheduled</span></div>');return!a&&!s?"":`<div class="p2-resume">${a}${a&&s?'<span class="p2-resume-div" aria-hidden="true"></span>':""}${s}</div>`}function _p2SoftSpots(e){const t=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart(),n=e.dice;if(!t&&(!n||!n.available))return"";const s=(t?(e._dhFacets||[]).filter(i=>i.score!=null&&i.reasoning):(n.d8||[]).concat(n.d6||[]).filter(i=>i.score!=null&&i.reasoning)).slice().sort((i,o)=>i.score-o.score).filter(i=>i.score<=3).slice(0,3);return s.length?'<section class="p2-softspots"><div class="p2-section-eyebrow">Soft Spots <span class="p2-eyebrow-note">where the company has no answer</span></div><div class="p2-ss-list">'+s.map(i=>`<div class="p2-ss p2-grp-${i.group}"><div class="p2-ss-hd"><span class="p2-ss-icon"><svg viewBox="0 0 24 24" class="p2-ricon p2-grp-${i.group}" aria-hidden="true">${_p2FacetIcon(i.key)}</svg></span><span class="p2-ss-name">${_p2Esc(_p2FacetLabel(i.key))}</span><span class="p2-ss-score">${i.score}<i>/${i.scale||7}</i></span></div><p class="p2-ss-why">${_p2Esc(i.reasoning)}</p></div>`).join("")+"</div></section>":""}let _p2CenterView="dice";function _p2DiceSection(e){if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart())return'<section class="p2-dice-sec"><div class="p2-dice-hdr"><div class="p2-section-eyebrow">The Party, Measured</div>'+_p2ReassessBtn(e,e.tldr?"Re-assess":"Assess party")+"</div>"+_dhPartyPanelHtml()+"</section>";const t=e.dice;if(!t||!t.available){const u=!!e.can_assess&&t&&(t.with_data||0)>=1,p=u?"Assess your party to unlock, use the \u201CAssess Party\u201D prompt above.":t&&t.active_total?`Import character sheets or run an assessment to measure the party: ${t.with_data} of ${t.active_total} active members have a sheet.`:"Add active party members (with sheets, or run an assessment) to measure the party.";return`<section class="p2-dice-sec p2-dice-locked"><div class="p2-dice-hdr"><div class="p2-section-eyebrow">The Party, Measured</div>${u?"":_p2ReassessBtn(e,"Assess party")}</div><p class="placeholder--padded">${p}</p></section>`}const n=!!e.can_assess,a=n&&!!(e.summary||e.strengths&&e.strengths.length||e.gaps&&e.gaps.length||e.synergies&&e.synergies.length||e.watch_outs&&e.watch_outs.length||e.tip),s=a&&_p2CenterView==="swot"?"swot":"dice",i=a?`<div class="p2-view-toggle" role="tablist" aria-label="Center panel view"><button class="p2-vt-btn${s==="dice"?" is-active":""}" type="button" data-p2-view="dice" role="tab" aria-selected="${s==="dice"}">Dice</button><button class="p2-vt-btn${s==="swot"?" is-active":""}" type="button" data-p2-view="swot" role="tab" aria-selected="${s==="swot"}">SWOT</button></div>`:"",o={concentration:"Conc",con:"CON",dex:"DEX",int:"INT",wis:"WIS",cha:"CHA"},r=n?(e.dice.d6||[]).filter(u=>u.tip_dc!=null):[],c=r.length?`<div class="p2-d6-dcs gm-surface"><div class="gm-surface-head"><span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.screen}</span><span class="gm-surface-label">Behind the Screen</span><span class="gm-surface-seal">GM&nbsp;only</span></div><p class="gm-surface-lead">DCs above which &gt;&#189; the party is statistically likely to fail the save</p><div class="p2-d6-dcgrid">`+r.map(u=>`<span class="p2-d6-dc"><b>${o[u.key]||_p2Esc(u.key)}</b> ${u.tip_dc}+</span>`).join("")+"</div></div>":"",l=`<div class="p2-view-pane p2-view-dice${s==="dice"?"":" hidden"}"><p class="p2-dice-hint">Hover, focus, or tap any facet for key insights.</p><div class="p2-legend"><span class="p2-lg p2-lg-combat">\u2694 Combat</span><span class="p2-lg p2-lg-noncombat">\u25CE Non-combat</span><span class="p2-lg p2-lg-save">\u25C6 Saves</span><span class="p2-lg p2-lg-par">Par ${t.par} - Well Rounded</span></div><div class="p2-dice-wrap"><div class="p2-die p2-d8"><div class="p2-die-cap">The D8 \xB7 Capabilities</div>${_p2DiceSvg(t.d8,8,{cx:150,cy:150,maxR:96,lbl:30,w:300,h:300,label:"Party capability octagon: eight scored facets"})}</div><div class="p2-facet-readout" id="p2-facet-readout" role="status" aria-l
3119ive="polite" data-default="${_p2Esc(e.tldr||"Hover, focus, or tap any facet for the reasoning.")}"><div class="p2-fro-default"><p class="p2-facet-hint">${_p2Esc(e.tldr||"Hover, focus, or tap any facet for the reasoning.")}</p></div></div><div class="p2-die p2-d6"><div class="p2-die-cap">The D6 \xB7 Saves</div>${_p2DiceSvg(t.d6,6,{cx:136,cy:120,maxR:74,lbl:26,w:272,h:244,gm:n,label:"Party resilience hexagon: saving throws and concentration"})}${c}</div></div></div>`,d=a?`<div class="p2-view-pane p2-view-swot${s==="swot"?"":" hidden"}">${_p2FullAssessment(e)}</div>`:"";return`<section class="p2-dice-sec"><div class="p2-dice-hdr"><div class="p2-section-eyebrow">The Party, Measured</div>${i}${_p2ReassessBtn(e,"Re-assess")}</div>`+l+d+"</section>"}function _p2DeedHtml(e){let t="";if(e.honorific){const a=e.party?"the whole party":(e.honorees||[]).map(s=>((allEntities||[]).find(i=>i.id===s)||{}).name).filter(Boolean).join(", ");t=`<span class="p2-deed-bestow">Title bestowed: <b>${_p2Esc(e.honorific)}</b>${a?" \u2014 "+_p2Esc(a):""}</span>`}const n=/^\d+$/.test(String(e.chapter))?"Ch. "+_chr2Roman(+e.chapter):e.chapter||(e.session&&e.session.n?"Ch. "+e.session.n:"");return`<div class="p2-deed"><span class="p2-deed-t">${_p2Esc(e.title||"")}</span>`+(e.detail?`<span class="p2-deed-d">${_p2Esc(e.detail)}</span>`:"")+t+(n?`<span class="p2-deed-ch">${_p2Esc(n)}</span>`:"")+"</div>"}function _p2HonorsRoll(e){const t=e.annals&&e.annals.deeds||[],n=!!e.can_assess;if(!t.length&&!n)return"";const a='<div class="p2-section-eyebrow">Honors Roll'+(n?`<button class="p2-honors-edit btn-ghost btn-xs" data-p2-honors-edit title="Add or edit the company's honors">\u270E Edit</button>`:"")+"</div>",s=t.length?'<div class="p2-deeds">'+t.map(_p2DeedHtml).join("")+"</div>":`<p class="p2-empty-note">No honors recorded yet, set down the company's legendary deeds.</p>`,i=n?'<div class="p2-hon-noms" data-p2-hon-noms></div>':"";return'<section class="p2-annal" data-p2-honors>'+a+s+i+"</section>"}async function _p2LoadHonorNoms(e,t){t=t||{};const n=currentCampaign&&currentCampaign.id,a=e.querySelector("[data-p2-hon-noms]");if(!n||!a)return;let s=null;try{s=await api("GET",`/api/campaigns/${n}/honors/nominations`)}catch{return}if(_campaignStale(n)||!a.isConnected)return;const i=s&&s.nominations||[];if(!i.length){a.innerHTML=t.empty?`<p class="placeholder placeholder--inline">${escHtml(t.empty)}</p>`:"";return}const o={};(allSessions||[]).filter(r=>!r.is_session_zero).forEach(r=>{o[r.id]=_chr2SessionNum(r)}),a.innerHTML=(t.after?"":`<div class="p2-hn-hdr">Awaiting the GM <span class="p2-hn-count">(${i.length})</span></div>`)+i.map(r=>{const c=o[r.session_id]||null,l=c?"Ch. "+_chr2Roman(c):"";return`<div class="p2-hn-row" data-mid="${r.id}" data-sid="${r.session_id||""}" data-chnum="${c||""}" data-kind="${escHtml(r.kind||"")}">
3120        <span class="p2-hn-star" title="${r.honor==="candidate"?"Auto-flagged candidate":"Player-nominated"}">\u2606</span>
3121        <span class="p2-hn-pc">${escHtml(r.pc_name)}</span>
3122        <span class="p2-hn-beat" data-beat="${escHtml(r.beat)}">${escHtml(r.beat)}</span>
3123        ${l?`<span class="p2-hn-ch">${escHtml(l)}</span>`:""}
3124        <span class="p2-hn-acts"><button type="button" class="btn-new btn-xs p2-hn-go">Enshrine</button><button type="button" class="btn-ghost btn-xs p2-hn-dismiss" title="Clear the nomination (the moment stays on the timeline)">\u2715</button></span>
3125      </div>`}).join(""),a.querySelectorAll(".p2-hn-dismiss").forEach(r=>r.addEventListener("click",async()=>{const c=r.closest(".p2-hn-row");r.disabled=!0;try{await api("POST",`/api/campaigns/${n}/moments/${c.dataset.mid}`,{honor:"clear"}),c.remove()}catch(l){r.disabled=!1,showToast("Failed: "+(l.message||"try again"))}})),a.querySelectorAll(".p2-hn-go").forEach(r=>r.addEventListener("click",()=>{const c=r.closest(".p2-hn-row");if(c.querySelector(".p2-hn-form"))return;const l=c.querySelector(".p2-hn-beat").dataset.beat||"",d=c.querySelector(".p2-hn-pc").textContent,u=l.startsWith(d)?l:`${d}: ${l}`,p=c.dataset.kind==="kill"&&/^Felled ([^:]+)/.test(l)?l.match(/^Felled ([^:]+)/)[1].trim():"",m=+c.dataset.chnum||null,f=document.createElement("div");f.className="p2-hn-form hm-promote-form",f.innerHTML=`
3126      <input type="text" class="hm-pf-text" maxlength="120" value="${esc
3126Html(u.slice(0,120))}" title="The honor line, as the roll will read it">
3127      <input type="text" class="hm-pf-title" maxlength="60" placeholder="Grant a title (optional)" value="${p?escHtml("Bane of "+p):""}">
3128      <span class="hm-pf-acts"><button type="button" class="btn-new btn-xs p2-hn-confirm">Enshrine</button><button type="button" class="btn-ghost btn-xs p2-hn-cancel">Cancel</button></span>`,c.appendChild(f),f.querySelector(".p2-hn-cancel").addEventListener("click",()=>f.remove()),f.querySelector(".p2-hn-confirm").addEventListener("click",async()=>{const g=f.querySelector(".p2-hn-confirm");g.disabled=!0;try{await api("POST",`/api/campaigns/${n}/moments/${c.dataset.mid}`,{honor:"promote",honor_text:f.querySelector(".hm-pf-text").value.trim(),honorific:f.querySelector(".hm-pf-title").value.trim()||void 0,chapter:m}),_campaignStale(n)||(showToast("Enshrined on the Honors Roll.",3500),t.after?t.after():renderParty2({cid:n}))}catch(h){g.disabled=!1,showToast("Failed: "+(h.message||"try again"))}})}))}function _p2OpenHonorsEditor(e,t,n){n=n||{};const a=t.annals&&t.annals.deeds||[],s=t.campaign_id,i=(allEntities||[]).filter(l=>l.type==="pc"&&!l.is_dead&&!l.is_inactive);(n.npcs||(typeof _p2RnParty<"u"&&_p2RnParty&&_p2RnParty.npc_members||[]).filter(l=>l.status==="confirmed"&&!l.is_dead).map(l=>({id:l.entity_id,name:l.name}))).forEach(l=>{i.some(d=>d.id===l.id)||i.push(l)});const r=l=>{const d=new Set(l.honorees||[]),u=`<button type="button" class="p2-de-chip p2-de-chip--party${l.party?" sel":""}" data-party="1">Whole party</button>`+i.map(m=>`<button type="button" class="p2-de-chip${!l.party&&d.has(m.id)?" sel":""}" data-pc="${m.id}">${_p2Esc(m.name)}</button>`).join(""),p=(()=>{const m=/^\d+$/.test(String(l.chapter))?+l.chapter:_tfRomanToNum(l.chapter),f=(allSessions||[]).filter(g=>!g.is_session_zero).map(g=>({n:_chr2SessionNum(g),t:g.title||""})).filter(g=>g.n).sort((g,h)=>g.n-h.n).map(g=>`<option value="${g.n}"${g.n===m?" selected":""}>Ch. ${_chr2Roman(g.n)}${g.t?" \xB7 "+_p2Esc(String(g.t).slice(0,34)):""}</option>`);return`<select class="p2-de-ch"><option value=""${m?"":" selected"}>Chapter\u2026</option>${f.join("")}</select>`})();return`<div class="p2-deed-edit"${l._i!=null?` data-i="${l._i}"`:""}><input class="p2-de-title" placeholder="The deed (e.g. Broke the Siege of Hollowmere)" maxlength="120" value="${_p2Esc(l.title||"")}"><input class="p2-de-detail" placeholder="One line of detail (optional)" maxlength="400" value="${_p2Esc(l.detail||"")}">`+p+`<button type="button" class="p2-de-del btn-ghost btn-xs" title="Remove">\u2715</button><div class="p2-de-bestow-row"><input class="p2-de-hon" placeholder="Bestow a title (optional)" maxlength="60" value="${_p2Esc(l.honorific||"")}"><span class="p2-de-chips">${u}</span></div></div>`};e.innerHTML=(n.bare?"":'<div class="p2-section-eyebrow">Honors Roll <span class="p2-eyebrow-note">editing</span></div>')+'<div class="p2-honors-editor" data-rows>'+(a.length?a.map((l,d)=>r(Object.assign({},l,{_i:d}))).join(""):r({}))+'</div><div class="p2-honors-bar"><button type="button" class="p2-de-add btn-ghost btn-xs">+ Add deed</button><span class="p2-honors-actions"><button type="button" class="p2-de-cancel btn-ghost btn-xs">Cancel</button><button type="button" class="p2-de-save btn-ghost btn-xs p2-reassess">Save</button></span></div>';const c=e.querySelector("[data-rows]");e.querySelector(".p2-de-add").onclick=()=>{c.insertAdjacentHTML("beforeend",r({})),c.lastElementChild.querySelector(".p2-de-title").focus()},e.addEventListener("click",l=>{if(l.target.classList.contains("p2-de-del")){l.target.closest(".p2-deed-edit")?.remove();return}if(l.target.classList.contains("p2-de-chip")){const d=l.target.closest(".p2-de-chips");l.target.dataset.party?l.target.classList.toggle("sel")&&d.querySelectorAll("[data-pc].sel").forEach(p=>p.classList.remove("sel")):(l.target.classList.toggle("sel"),d.querySelector("[data-party]")?.classList.remove("sel"))}}),e.querySelector(".p2-de-cancel").onclick=()=>n.onCancel?n.onCancel():renderParty2({cid:s}),e.querySelector(".p2-de-save").onclick=async l=>{const d=l.currentTarget;d.disabled=!0,d.textContent="Saving\u2026";let u=!1;const p=[...c.querySelectorAll(".p2-deed-edit")].map(m=>{const f=m.querySelector(".p2-de-ch"),g=m.querySelector(".p2-de-title").value.trim(),h=parseInt(f.value)||null;return f.classList.toggle("p2-de-ch--missing",!!g&&!h),g&&!h&&(u=!0),{...m.dataset.i!=null?a[+m.dataset.i]||{}:{}
3128,title:g,detail:m.querySelector(".p2-de-detail").value.trim(),chapter:h,honorific:m.querySelector(".p2-de-hon").value.trim()||void 0,party:!!m.querySelector(".p2-de-chip--party.sel")||void 0,honorees:[...m.querySelectorAll(".p2-de-chip[data-pc].sel")].map(b=>parseInt(b.dataset.pc)).filter(Boolean)}}).filter(m=>m.title);if(u){showToast("Every deed needs its chapter.",4e3),d.disabled=!1,d.textContent="Save";return}try{await api("PUT",`/api/campaigns/${s}/company-deeds`,{deeds:p});try{await loadEntities()}catch{}n.after?await n.after():await renderParty2({cid:s})}catch(m){typeof showToast=="function"&&showToast("Save failed: "+(m.message||m),5e3),d.disabled=!1,d.textContent="Save"}}}function _p2WallSection(e){const t=e._wall||[],n=!!e.can_assess;return t.length?`<section class="p2-annal p2-wall"><div class="p2-section-eyebrow">Words We Won't Forget<span class="p2-eyebrow-note">the moments the table won't let die</span></div><div class="p2-wall-list">`+t.map(a=>{const s=[a.speaker,a.chapter?`Ch. ${_chr2Roman(a.chapter)}`:null].filter(Boolean).join(" \xB7 "),i=n?`<span class="p2-wall-gm"><button class="p2-wall-feat${a.featured?" is-on":""}" type="button" data-wall-feature="${a.id}" data-on="${a.featured?"1":"0"}" title="${a.featured?"Unfeature":"Feature on the wall"}" aria-label="Feature">\u2605</button><button class="p2-wall-hide" type="button" data-wall-hide="${a.id}" title="Hide from the wall" aria-label="Hide">\u2715</button></span>`:"",o=a.id?_qcShareBtn(`"${(a.quote||"").trim()}" \u2014 ${a.speaker||""}${a.context?" ("+a.context+")":""}`):"",r=a.card_spec&&typeof _qcRenderInline=="function",c=r?`<div class="p2-wall-cardart" data-qc-open="${a.id!=null?a.id:""}" title="Tweak or re-share this card">${_qcRenderInline(a.card_spec,.52)}</div>`:`<blockquote class="p2-wall-q">\u201C${_p2Esc(a.quote)}\u201D</blockquote><figcaption class="p2-wall-attr">${_p2Esc(s)}${a.context?` \xB7 <span class="p2-wall-ctx">${_p2Esc(a.context)}</span>`:""}</figcaption>`;return`<figure class="p2-wall-card${a.featured?" is-featured":""}${r?" p2-wall-card--art":""}" data-quote-id="${a.id!=null?a.id:""}">`+c+_p2ReactionBar(a)+o+i+"</figure>"}).join("")+"</div></section>":n?`<section class="p2-annal p2-wall"><div class="p2-section-eyebrow">Words We Won't Forget</div><p class="p2-empty-note">The moments your table won't let die. React to quotes (in a PC's Codex or a recap), or \u2605 a favorite, and they gather here.</p></section>`:""}function _p2Annals(e){const t=e.annals||{},n=[],a=_p2HonorsRoll(e);a&&n.push(a);const s=_p2WallSection(e);if(s&&n.push(s),t.foes_felled){const o=t.kill_groups||[];n.push(`<section class="p2-annal"><div class="p2-section-eyebrow">Foes Felled</div><div class="p2-annal-figure"><span class="p2-annal-num">${t.foes_felled}</span><span class="p2-annal-num-lbl">put to the sword, all told</span></div>`+(o.length?'<div class="p2-killroll">'+o.map(r=>`<span class="p2-kill"><span class="p2-kill-n">${_p2Esc(r.name)}</span><span class="p2-kill-x">\xD7${r.count}</span></span>`).join("")+(t.kill_groups_more?`<span class="p2-kill p2-kill--more">\u2026and ${t.kill_groups_more} more</span>`:"")+"</div>":"")+"</section>")}if(t.quests_complete){const o=t.quests_recent||[];n.push(`<section class="p2-annal"><div class="p2-section-eyebrow">Quests Won</div><div class="p2-annal-figure"><span class="p2-annal-num">${t.quests_complete}</span><span class="p2-annal-num-lbl">${t.quests_complete===1?"quest":"quests"} seen through</span></div>`+(o.length?'<ul class="p2-questroll">'+o.map(r=>`<li>${_p2Esc(r)}</li>`).join("")+"</ul>":"")+"</section>")}const i=t.near_wipes||[];return i.length&&n.push(`<section class="p2-annal p2-annal--peril"><div class="p2-section-eyebrow">The Hour We Nearly Fell <span class="p2-eyebrow-note">${i.length} time${i.length===1?"":"s"} the dark reached, and missed</span></div><div class="p2-perils">`+i.map(o=>'<div class="p2-peril">'+(o.chapter?`<span class="p2-peril-ch">Ch. ${_p2Esc(_chr2Roman(o.chapter))}</span>`:"")+`<div class="p2-peril-body"><span class="p2-peril-name">${_p2Esc(o.name)}</span>`+(o.note?`<span class="p2-peril-note">${_p2Esc(o.note)}</span>`:"")+"</div></div>").join("")+"</div></section>"),n.length?'<div class="p2-annals">'+n.join("")+"</div>":`<p class="placeholder--padded">The company's annals are still being written.</p>`}function _p2Departed(e){const t=e.departed||[];return t.length?'<div class="party2-page"><div class="index-inner">'+_p2Masthead({eyebrow:"Party",titleHtml:"The Departed",leadHtml:"Alive, as far as the table knows, but no longer marching. Longest-served first."})+'<div class="p2-departed">'+t.map(n=>{const a=n.tenure,s=a?`${a.sessions} ${a.sessions===1?"session":"sessions"}`:null,i=n.departed_session&&n.departed_session.n?`Ch. ${n.departed_session.n}`:n.tenure&&n.tenure.last&&n.tenure.last.n?`last seen Ch. ${n.tenure.last.n}`:null,o=n.quotes&&n.quotes.length||n.quote&&n.quote.quote;return'<article class="p2-dcard">'+_p2Portrait(n,"p2-dcard-portrait p2-sepia")+`<div class="p2-dcard-body"><span class="entity-link p2-dcard-name pipe" data-id="${n.id}" tabindex="0" role="button">${_p2PipeName(n.name)}</span>`+(_p2ClassLine(n)?`<div class="p2-dcard-cls">${_p2Esc(_p2ClassLine(n))}</div>`:"")+(n.snippet&&!o?`<p class="p2-dcard-epithet">${_p2Esc(n.snippet)}</p>`:"")+`<div class="p2-dcard-meta">${i?`<span class="p2-parted">Parted \xB7 ${_p2Esc(i)}</span>`:""}${n.departed_temporary?'<span class="p2-may-return">may yet return</span>':""}${s?`<span class="p2-served">${_p2Esc(s)} under the banner</span>`:""}</div>`+_p2Quote(n)+_p2MemNote(n,"parting")+"</div></article>"}).join("")+"</div>"+_p2Colophon("The Departed")+"</div></div>":""}function _p2MemorialTie(e,t){const n=e.memorial_linked_entity_id;if(!n)return"";const s=[].concat(t.active||[],t.departed||[],t.fallen||[]).find(i=>i.id===n);return!s||s.id===e.id?"":`<div class="p2-mem-tie">Bound to <span class="entity-link p2-mem-tie-name pipe" data-id="${s.id}" tabindex="0" role="button">${_p2PipeName(s.name)}</span>, who marches still.</div>`}function _p2MemEpitaph(e){if(e.memorial_epitaph)return`<p class="p2-mem-epitaph">\u201C${_p2Esc(e.memorial_epitaph)}\u201D</p>`;if(e.bardic_flavor&&String(e.bardic_flavor).trim())return`<div class="p2-mem-verse">${_bardicFlavorHtml(e.bardic_flavor)}</div><div class="p2-mem-byline">\u2014 ${_p2Esc(_bardicByline("the Bard"))}</div>`;const t=e.quotes&&e.quotes[0]||e.quote;return t&&t.quote?`<p class="p2-mem-epitaph">\u201C${_p2Esc(t.quote)}\u201D</p>`:'<p class="p2-mem-epitaph"><span class="p2-mem-todo">Their epitaph is still being written.</span></p>'}const _P2_MEMNOTE={lastwords:{label:"Last Words",placeholder:"What they s
3128aid at the end: to the party, to the dark, to no one.",add:"Leave their last words for the company\u2026"},parting:{label:"Parting Ways",placeholder:"What called them away: the title reclaimed, the vow taken, the lead the party wouldn\u2019t follow.",add:"Note what called them from the company\u2026"}};function _p2MemNote(e,t){const n=_P2_MEMNOTE[t];if(!n)return"";const a=(e.memorial_text||"").trim(),s=!!e.can_edit_last_words;if(!a&&!s)return"";const i=`<div class="p2-lw-eyebrow">${_p2Esc(n.label)}`+(s?`<button class="p2-lw-edit" type="button" data-p2-memnote-edit title="${a?"Edit":"Add"}" aria-label="${a?"Edit":"Add"} ${_p2Esc(n.label)}">\u270E</button>`:"")+"</div>",o=a?`<blockquote class="p2-lw-text">${_p2Esc(a).replace(/\n/g,"<br>")}</blockquote>`:`<button class="p2-lw-add" type="button" data-p2-memnote-edit>${_p2Esc(n.add)}</button>`;return`<div class="p2-lastwords" data-p2-memnote data-pc-id="${e.id}" data-memnote-kind="${t}">${i}${o}</div>`}function _p2OpenMemNoteEditor(e,t,n){const a=_P2_MEMNOTE[n]||_P2_MEMNOTE.lastwords,s=_party2Spec&&_party2Spec.campaign_id||currentCampaign&&currentCampaign.id;e.innerHTML=`<div class="p2-lw-eyebrow">${_p2Esc(a.label)} <span class="p2-eyebrow-note">editing</span></div><textarea class="p2-lw-ta" rows="3" maxlength="1000" placeholder="${_p2Esc(a.placeholder)}">${_p2Esc(t.memorial_text||"")}</textarea><div class="p2-lw-bar"><button type="button" class="p2-lw-cancel btn-ghost btn-xs">Cancel</button><button type="button" class="p2-lw-save btn-ghost btn-xs p2-reassess">Save</button></div>`;const i=e.querySelector(".p2-lw-ta");i.focus(),i.setSelectionRange(i.value.length,i.value.length),e.querySelector(".p2-lw-cancel").onclick=()=>renderParty2({cid:s}),e.querySelector(".p2-lw-save").onclick=async o=>{const r=o.currentTarget;r.disabled=!0,r.textContent="Saving\u2026";try{await api("PUT",`/api/campaigns/${s}/entities/${t.id}/last-words`,{last_words:i.value}),await renderParty2({cid:s})}catch(c){typeof showToast=="function"&&showToast("Save failed: "+(c.message||c),5e3),r.disabled=!1,r.textContent="Save"}}}const _P2_RENOWN_TONE={"-3":"hostile","-2":"hostile","-1":"cool",0:"neutral",1:"warm",2:"warm",3:"ally"},_P2_RN_LABELS={"-3":"Hunted","-2":"Opposed","-1":"Wary",0:"Tolerated",1:"Welcomed",2:"Favored",3:"Championed"},_P2_RN_TIPS={"-3":"Kill-on-sight. Every door in their territory is shut, and someone's already collecting on the bounty.","-2":"They move against you in the open: contacts dry up, prices double, and rivals hear about you first.","-1":"Business still gets done, but never without a chaperone, a markup, or a second thought.",0:"They'll deal with you like anyone off the street. No favor owed, none extended.",1:"They remember your name and extend a small courtesy or two, but nobody's vouching for you yet.",2:"Doors open early, prices soften, and their people vouch for you before you ask.",3:"They'll spend blood or coin on your name, and tell the story afterward."};function _p2RnScaleHtml(e,t){const n=e.standing==null?null:e.standing,a=(e.proposals||[])[0],s=a?Math.max(-3,Math.min(3,(n??0)+(a.delta||0))):null,i=d=>((d+3)/6*100).toFixed(1);let o="";if(s!==null&&s!==(n??0)){const d=n??0,u=Math.min(d,s),p=Math.max(d,s);o=`<span class="p2-rn-shift p2-rn-shift--${s>d?"up":"down"}" style="left:${i(u)}%;width:${((p-u)/6*100).toFixed(1)}%" aria-hidden="true"></span>`}const r=[-3,-2,-1,0,1,2,3].map(d=>{const u=["p2-rn-tick"];let p="",m=_P2_RN_LABELS[String(d)]+": "+(_P2_RN_TIPS[String(d)]||""),f="Set standing to "+_P2_RN_LABELS[String(d)];return n!=null&&d===n?u.push("p2-rn-tick--current","p2-rn-tone--"+(_P2_RENOWN_TONE[String(d)]||"neutral")):s!==null&&d===s&&(u.push("p2-rn-tick--ghost"),p=' data-ghost="1"',m="Proposed: "+_P2_RN_LABELS[String(d)]+" \u2014 tap to review",f="Review the proposed shift to "+_P2_RN_LABELS[String(d)]),`<button type="button" class="${u.join(" ")}" style="left:${i(d)}%" role="radio" aria-checked="${n!=null&&d===n}" data-set="${d}"${p} data-fid="${e.entity_id}" title="${escHtml(m)}" aria-label="${escHtml(f)}"></button>`}).join(""),c=n===-3?" p2-rn-cur-lbl--lo":n===3?" p2-rn-cur-lbl--hi":"",l=t||n==null?"":`<span class="p2-rn-cur-lbl p2-rn-tone--${_P2_RENOWN_TONE[String(n)]||"neutral"}${c}" style="left:${i(n)}%">${escHtml(_P2_RN_LABELS[String(n)]||"")}</span>`;return`<div class="p2-rn-scale${t?" p2-rn-scale--compact":""}${n===-3?" p2-rn-scale--hostile":""}" role="radiogroup" aria-label="Standing with ${escHtml(e.name)}
3128">
3129    <div class="p2-rn-track"><span class="p2-rn-heat" aria-hidden="true"></span>${o}${l}${r}</div>
3130    ${t?"":`<div class="p2-rn-caps" aria-hidden="true"><span${n===-3?' class="is-cur"':""}>Hunted</span><span${n===3?' class="is-cur"':""}>Championed</span></div>`}
3131  </div>`}function _p2RnWireScales(e,t,n,a){e.querySelectorAll(".p2-rn-tick[data-set]").forEach(s=>s.addEventListener("click",()=>{if(!s.classList.contains("p2-rn-tick--current")){if(s.dataset.ghost){n&&n(s);return}t(s.dataset.fid,parseInt(s.dataset.set,10),s)}})),e.querySelectorAll(".p2-rn-scale").forEach(s=>{const i=s.querySelector(".p2-rn-track"),o=i&&i.querySelector(".p2-rn-tick--current");if(!o)return;const r=parseInt(o.dataset.set,10),c=g=>((g+3)/6*100).toFixed(1)+"%",l=i.querySelector(".p2-rn-cur-lbl");let d=!1,u=null;const p=g=>{const h=i.getBoundingClientRect();return Math.round(Math.min(1,Math.max(0,(g-h.left)/h.width))*6)-3},m=g=>{o.style.left=c(g),l&&(l.style.left=c(g),l.textContent=_P2_RN_LABELS[String(g)]||""),a&&a(s,g)};o.addEventListener("pointerdown",g=>{d=!0,u=r;try{o.setPointerCapture(g.pointerId)}catch{}s.classList.add("is-dragging"),g.preventDefault()}),o.addEventListener("pointermove",g=>{if(!d)return;const h=p(g.clientX);h!==u&&(u=h,m(h))});const f=()=>{if(d){if(d=!1,s.classList.remove("is-dragging"),u===null||u===r){m(r);return}
3131t(o.dataset.fid,u,o)}};o.addEventListener("pointerup",f),o.addEventListener("pointercancel",f)})}const _FMAP_BOOK_SVG='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 4h6a4 4 0 0 1 4 4v13a3 3 0 0 0-3-3H2z"/><path d="M22 4h-6a4 4 0 0 0-4 4v13a3 3 0 0 1 3-3h7z"/></svg>',_FMAP_IMG_SVG='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l4-4 3 3 4-5 3 4"/><circle cx="8.5" cy="9.5" r="1.1"/></svg>',_FMAP_TIER_OPTS={hero:{emblemSize:96,sizeOther:42,sizeLeader:56,maxRadius:130,addToken:!0},major:{emblemSize:78,sizeOther:36,sizeLeader:48,maxRadius:106,addToken:!0},standard:{emblemSize:64,sizeOther:30,sizeLeader:40,maxRadius:88,addToken:!0},minor:{emblemSize:52,sizeOther:26,sizeLeader:34,maxRadius:72,addToken:!0},far:{emblemSize:44,sizeOther:22,sizeLeader:30,maxRadius:60,addToken:!0}};function _fmapTierFor(e,t){return e===0?"hero":e<=3?"major":e===4?"standard":"minor"}function _fmapClockSvg(e,t,n){e=Math.max(1,e|0),n=n||18;const a=n/2,s=a-2,i=e>1?.14:0,o=2*Math.PI/e,r=o-i;let c="";for(let l=0;l<e;l++){const d=-Math.PI/2+l*o+i/2,u=d+r;c+=`<path class="${l<t?"is-filled":""}" d="M ${(a+s*Math.cos(d)).toFixed(1)} ${(a+s*Math.sin(d)).toFixed(1)} A ${s} ${s} 0 ${r>Math.PI?1:0} 1 ${(a+s*Math.cos(u)).toFixed(1)} ${(a+s*Math.sin(u)).toFixed(1)}"/>`}return`<svg class="fmap-clock" width="${n}" height="${n}" viewBox="0 0 ${n} ${n}" aria-hidden="true">${c}</svg>`}function _p2RenownCard(e,t){t=t===!0?"far":t||"standard";const n=t!=="hero"&&t!=="major",a=(e.proposals||[]).length,s=!_isGm(),i=(()=>{const r=s?_fmapPlayerProject(e.members||[]):e.members||[],c=r.filter(d=>d.type!=="pc"),l=r.filter(d=>d.type==="pc").map(d=>({...d,member_pill:"pc"}));return c.concat(l)})(),o=window.FactionMap?FactionMap.renderClusterExpanded(e,i,{..._FMAP_TIER_OPTS[t]||_FMAP_TIER_OPTS.standard,pendingDot:!!a&&!s,readonly:s}):"";return`<article class="p2-rn-card fmap-card fmap-card--${t}" data-fid="${e.entity_id}"
3132    role="group" aria-label="${escHtml(e.name)} \u2014 standing and members">
3133    ${o}
3134    <button type="button" class="p2-rn-name p2-rn-name--link" data-fid="${e.entity_id}" title="Open ${escHtml(e.name)} in the Codex">${escHtml(e.name)}</button>
3135    <div class="fmap-scale-row">${_p2RnScaleHtml(e,n)}</div>
3136    ${e.agenda&&!s?(()=>{const r=e.agenda_clock_size||6,c=e.agenda_clock_filled||0,l=c>=r,d=l?"complete":`${c} of ${r} steps`;return`<button type="button" class="fmap-agenda${l?" fmap-agenda--done":""}" data-fid="${e.entity_id}"
3137      title="${escHtml(e.agenda)} \xB7 ${d}"
3138      aria-label="Agenda for ${escHtml(e.name)}: ${escHtml(e.agenda)}, ${d}">
3139      ${_fmapClockSvg(r,c,18)}
3140      <span class="fmap-agenda-count">${l?"Complete":`${c}/${r}`}</span>
3141      <span class="fmap-agenda-goal">${escHtml(e.agenda)}</span>
3142    </button>`})():""}
3143    ${s?"":_fmapAddPanelHtml(e.entity_id)}
3144  </article>`}function _fmapAddPanelHtml(e){return`<div class="fmap-add-panel hidden" data-fid="${e}">
3145      <input type="text" class="form-input fmap-add-q" placeholder="Add by name\u2026" aria-label="Search entities to add" />
3146      <div class="fmap-add-list" role="listbox"></div>
3147    </div>`}let _p2RnData=null,_p2RnParty=null;async function _p2RenownLoad(){const e=currentCampaign?.id;if(e==null)return;let t;try{t=await api("GET",`/api/campaigns/${e}/renown`)}catch{return}_campaignStale(e)||(_p2RnData=t.factions||[],_p2RnParty=t.party||null,_p2RenownPaint(e))}function _p2RenownPaint(e){const t=document.getElementById("p2-renown-mount");if(!t)return;const n=document.querySelector("#fmap-pop textarea");if(n&&(document.activeElement===n||n.value.trim()))return;_fmapPopClose(),_fmapLogClose();const a=_p2RnData||[],s=new Set(a.map(l=>l.entity_id)),i=(allEntities||[]).filter(l=>l.type==="faction"&&!s.has(l.id)&&!l.pending_reveal&&!(l.aliases||[]).some(d=>(d||"").toLowerCase()==="the party")),o=`<div class="p2-rn-add">
3148      <button type="button" id="p2-rn-add-btn" class="btn-new btn-xs">\uFF0B Track a faction</button>
3149      <div class="fmap-add-panel hidden" id="p2-rn-track-panel">
3150        <input type="text" class="form-input fmap-add-q" placeholder="Track a faction\u2026" aria-label="Search factions to track" />
3151        <div class="fmap-add-list" role="listbox"></div>
3152      </div>
3153    </div>`,r=a.reduce((l,d)=>l+(d.proposals||[]).length,0),c=!_isGm();t.innerHTML=`<div class="party2-page"><div class="index-inner fmap-page${c?" fmap-readonly":""}">`+_p2Masthead({eyebrow:"Party",titleHtml:"Standing",leadHtml:"The world in relation to the company: every soul placed where the story put them."+(r&&!c?` <b>${r} proposed shift${r===1?"":"s"} await${r===1?"s":""} your call \u2014 tap the pulsing amber shift on a slider to rule on it.</b>`:"")})+(c?"":o)+_p2RnFanHtml(a)+_p2Colophon("Standing")+"</div></div>",c?_p2RenownWireReadonly(t):_p2RenownWire(t,e),_fmapOrbit(t),_p2Reland(document.getElementById("tab-party2")),_tfRailDot(document.getElementById("p2-rail"),"p2-sec-factions",r&&!c?"wat
3153ch":null,r&&!c?`${r} proposed shift${r===1?"":"s"} waiting`:""),_p2NewsPaint(document.getElementById("tab-party2"),e)}function _p2RenownWireReadonly(e){const t=n=>{n&&typeof navigateToEntity=="function"&&navigateToEntity(n)};e.querySelectorAll(".p2-rn-name--link[data-fid]").forEach(n=>n.addEventListener("click",()=>t(parseInt(n.dataset.fid,10)))),e.querySelectorAll(".fmap-token[data-mid]:not(.fmap-token--add):not(.fmap-token--more)").forEach(n=>n.addEventListener("click",()=>t(parseInt(n.dataset.mid,10))))}function _p2RnPartyHtml(){if(!_p2RnParty||!window.FactionMap)return"";const e=(allEntities||[]).filter(s=>s&&s.type==="pc").map(s=>({entity_id:s.id,name:s.name,state:"met",killed:!1,is_dead:!!s.is_dead,missing:!!s.is_inactive&&!s.is_dead,image_url:s.image_crop_url||s.image_url,matte_url:null,disposition_tone:null,activity:1,leader:_p2RnParty.leader_entity_id!=null&&s.id===_p2RnParty.leader_entity_id}));(_isGm()?_p2RnParty.npc_members||[]:_fmapPlayerProject(_p2RnParty.npc_members||[])).forEach(s=>e.push({entity_id:s.entity_id,name:s.name,state:"met",killed:!1,is_dead:!!s.is_dead,missing:!1,departed_right:s.status==="departed"&&!s.is_dead,image_url:s.image_url,matte_url:null,disposition_tone:s.disposition_tone||null,activity:0,leader:!1,npc_arc:!0}));const t=new Set(e.filter(s=>!s.is_dead&&!s.missing).map(s=>s.entity_id));(allEntities||[]).filter(s=>s&&s.type==="companion"&&t.has(s.parent_entity_id)).forEach(s=>e.push({entity_id:s.id,name:s.name,state:"met",killed:!1,is_dead:!!s.is_dead,missing:!1,image_url:s.image_crop_url||s.image_url,matte_url:null,disposition_tone:null,activity:0,leader:!1,satellite_of:s.parent_entity_id}));const a=_p2RnParty.entity_id!=null&&_isGm();return`<div class="fmap-party">
3154    ${FactionMap.renderClusterExpanded({entity_id:_p2RnParty.entity_id,name:_p2RnParty.name,image_url:_p2RnParty.image_url,matte_url:_p2RnParty.matte_url},e,{emblemSize:104,sizeOther:46,maxRadius:132,recessLifecycle:!0,addToken:a,readonly:!a,partyWheel:!0})}
3155    <span class="fmap-party-cap">${escHtml(_p2RnParty.name||"The Party")}</span>
3156    ${a?_fmapAddPanelHtml(_p2RnParty.entity_id):""}
3157  </div>`}function _p2RnRecencySorted(e){const t=l=>[l.last_seen_rank??-1,...(l.members||[]).map(d=>d.last_seen_rank??-1)].filter(d=>d>=0),n=Math.max(-1,...e.flatMap(t)),a=l=>t(l).reduce((d,u)=>d+Math.pow(.5,n-u),0),s=e.map(l=>({f:l,s:a(l)})).sort((l,d)=>d.s-l.s||(d.f.standing??-4)-(l.f.standing??-4)||l.f.name.localeCompare(d.f.name)),i=.15,o=l=>l.s>=i||(l.f.proposals||[]).length>0;let r=s.filter(o).map(l=>l.f),c=s.filter(l=>!o(l)).map(l=>l.f);return r.length||(r=s.map(l=>l.f),c=[]),{active:r,far:c,all:r.concat(c)}}function _p2RnFanHtml(e){const t=_p2RnPartyHtml();if(!e.length)return t+'<p class="placeholder placeholder--padded">No factions tracked yet. Organizations your codex knows join here on their own once their Kind is known, or add one below.</p>';const{active:n,far:a}=_p2RnRecencySorted(e);return`<div class="fmap-orbit">${t}`+n.map((s,i)=>_p2RenownCard(s,_fmapTierFor(i,n.length))).join("")+a.map(s=>_p2RenownCard(s,"far")).join("")+"</div>"}function _fmapOrbit(e){const t=e.querySelector(".fmap-orbit");if(!t)return;const n=t.querySelector(".fmap-party"),a=[...t.querySelectorAll(".fmap-card")],s=t.clientWidth;if(!s)return;if(a.forEach(f=>{f.style.left="",f.style.top=""}),n&&(n.style.left="",n.style.top=""),t.style.height="",!n||s<1e3){t.classList.add("fmap-orbit--stack");return}t.classList.remove("fmap-orbit--stack");const i=[.091,.305,.5,.686,.914].map(f=>s*f),o=f=>{const g=f.querySelector(".fmap-cluster");return g?g.offsetTop+g.offsetHeight/2:f.offsetHeight/2},r=(f,g,h)=>(f.style.left=`${Math.round(g-f.offsetWidth/2)}px`,f.style.top=`${Math.max(0,Math.round(h-o(f)))}px`,(parseInt(f.style.top)||0)+f.offsetHeight);n.style.left=`${Math.round(s/2-n.offsetWidth/2)}px`,n.style.top="0px";const c=o(n);let l=n.offsetHeight;const d=a.slice();d.length&&(l=Math.max(l,r(d.shift(),s*.128,c))),d.length&&(l=Math.max(l,r(d.shift(),s*.838,c)));const u=n.offsetHeight+85;d.length&&(l=Math.max(l,r(d.shift(),s*.381,u))),d.length&&(l=Math.max(l,r(d.shift(),s*.648,u)));const p=u+105;d.length&&(l=Math.max(l,r(d.shift(),i[0],p))),d.length&&(l=Math.max(l,r(d.shift(),i[4],p)));let m=p+178;for(;d.length;){const f=d[0].offsetHeight,g=Math.round(f*.27),h=Math.round(f*.54);d.length&&(l=Math.max(l,r(d.shift(),i[2],m))),d.length&&(l=Math.max(l,r(d.shift(),i[1],m+g))),d.length&&(l=Math.max(l,r(d.shift(),i[3],m+g))),d.length&&(l=Math.max(l,r(d.shift(),i[0],m+h))),d.length&&(l=Math.max(l,r(d.shift(),i[4],m+h))),m+=f+25}t.style.height=`${Math.round(l+16)}px`,_fmapDragMode()&&_fmapDragSetup(t,n,a),document.fonts&&!window._fmapFontsHooked&&(window._fmapFontsHooked=!0,document.fonts.ready.then(()=>{const f=document.getElementById("p2-renown-mount");f&&f.querySelector(".fmap-orbit")&&_fmapOrbit(f)}))}function _fmapDragMode(){if(location.search.includes("fmapdrag"))try{localStorage.setItem("tf_fmap_drag","1")}
3157catch{}try{return localStorage.getItem("tf_fmap_drag")==="1"}catch{return!1}}function _fmapDragKey(){return`tf_fmap_drag_${currentCampaign?.id}`}function _fmapDragSave(e){const t={};[...e.querySelectorAll(".fmap-card, .fmap-party")].forEach(a=>{const s=a.classList.contains("fmap-party")?"party":a.dataset.fid;t[s]={l:parseInt(a.style.left)||0,t:parseInt(a.style.top)||0,w:a.offsetWidth,h:a.offsetHeight,s:parseFloat(a.dataset.dragScale||"1"),name:a.querySelector(".p2-rn-name")?.textContent||"PARTY",tier:([...a.classList].find(i=>i.startsWith("fmap-card--"))||"party").replace("fmap-card--","")}});try{localStorage.setItem(_fmapDragKey(),JSON.stringify({w:e.clientWidth,pos:t}))}catch{}const n=Math.max(...Object.values(t).map(a=>a.t+a.h*a.s));e.style.height=`${Math.round(n+40)}px`}function _fmapDragRestore(e){let t=null;try{t=JSON.parse(localStorage.getItem(_fmapDragKey())||"null")}catch{}if(!t||!t.pos)return;[...e.querySelectorAll(".fmap-card, .fmap-party")].forEach(a=>{const s=a.classList.contains("fmap-party")?"party":a.dataset.f
3157id,i=t.pos[s];i&&(a.style.left=`${i.l}px`,a.style.top=`${i.t}px`,i.s&&i.s!==1&&(a.dataset.dragScale=i.s,a.style.transform=`scale(${i.s})`,a.style.transformOrigin="top center"))});const n=Math.max(...[...e.querySelectorAll(".fmap-card, .fmap-party")].map(a=>(parseInt(a.style.top)||0)+a.offsetHeight*parseFloat(a.dataset.dragScale||"1")));e.style.height=`${Math.round(n+40)}px`}function _fmapDragSetup(e,t,n){e.classList.add("fmap-drag"),_fmapDragRestore(e);const a=40,s=8,i=[.7,.85,1,1.2,1.4],o=l=>{const d=l.querySelector(".fmap-cluster"),u=parseFloat(l.dataset.dragScale||"1");return{ax:l.offsetWidth/2*u,ay:(d?d.offsetTop+d.offsetHeight/2:l.offsetHeight/2)*u}},r={v:null,h:null},c=(l,d,u)=>{if(!d){r[l]&&(r[l].remove(),r[l]=null);return}r[l]||(r[l]=document.createElement("div"),r[l].className=`fmap-drag-guide fmap-drag-guide--${l}`,e.appendChild(r[l])),r[l].style[l==="v"?"left":"top"]=`${Math.round(u)}px`};if([t,...n].forEach(l=>{if(!(!l||l._fmapDragWired)){if(l._fmapDragWired=!0,l.classList.contains("fmap-card")&&!l.querySelector(".fmap-drag-size")){const d=document.createElement("div");d.className="fmap-drag-size",d.innerHTML='<button type="button" data-s="-1" title="Demote - smaller">\u2212</button><button type="button" data-s="1" title="Promote - bigger">\uFF0B</button>',l.appendChild(d),d.addEventListener("pointerdown",u=>u.stopPropagation(),!0),d.addEventListener("click",u=>{const p=parseInt((u.target.dataset||{}).s||"0",10);if(!p)return;const m=i.indexOf(parseFloat(l.dataset.dragScale||"1")),f=Math.max(0,Math.min(i.length-1,(m<0?2:m)+p));l.dataset.dragScale=i[f],l.style.transform=i[f]===1?"":`scale(${i[f]})`,l.style.transformOrigin="top center",_fmapDragSave(e)})}l.addEventListener("pointerdown",d=>{if(d.target.closest&&d.target.closest(".fmap-drag-size"))return;d.preventDefault(),d.stopPropagation();const u=d.clientX,p=d.clientY,m=parseInt(l.style.left)||0,f=parseInt(l.style.top)||0,g=o(l),h=[...e.querySelectorAll(".fmap-card, .fmap-party")].filter(y=>y!==l).map(y=>{const _=o(y);return{cx:(parseInt(y.style.left)||0)+_.ax,cy:(parseInt(y.style.top)||0)+_.ay}});try{l.setPointerCapture(d.pointerId)}catch{}l.classList.add("is-dragging");const v=y=>{let _=m+g.ax+(y.clientX-u),w=f+g.ay+(y.clientY-p),$=null,E=null;h.forEach(k=>{$===null&&Math.abs(k.cx-_)<=s&&($=k.cx),E===null&&Math.abs(k.cy-w)<=s&&(E=k.cy)}),_=$!==null?$:Math.round(_/a)*a,w=E!==null?E:Math.round(w/a)*a,c("v",$!==null,_),c("h",E!==null,w),l.style.left=`${Math.round(_-g.ax)}px`,l.style.top=`${Math.max(0,Math.round(w-g.ay))}px`},b=()=>{l.removeEventListener("pointermove",v),l.removeEventListener("pointerup",b),l.classList.remove("is-dragging"),c("v",!1),c("h",!1),_fmapDragSave(e)};l.addEventListener("pointermove",v),l.addEventListener("pointerup",b)},!0)}}),!document.getElementById("fmap-dragbar")){const l=document.createElement("div");l.id="fmap-dragbar",l.className="fmap-dragbar",l.innerHTML=`<span class="fmap-dragbar-lbl">Layout test sheet \u2014 drag to place \xB7 \uFF0B/\u2212 to size \xB7 saves on release</span>
3158      <button type="button" class="btn-ghost btn-xs" data-act="copy">Copy JSON</button>
3159      <button type="button" class="btn-ghost btn-xs" data-act="reset">Reset</button>
3160      <button type="button" class="btn-ghost btn-xs" data-act="exit">Exit</button>`,document.body.appendChild(l),l.addEventListener("click",d=>{const u=d.target.dataset&&d.target.dataset.act;if(u){if(u==="copy"){let p=null;try{p=localStorage.getItem(_fmapDragKey())}catch{}p&&typeof _tfCopyText=="function"&&(_tfCopyText(p),showToast("Layout copied."))}else if(u==="reset"){try{localStorage.removeItem(_fmapDragKey())}catch{}_p2RenownLoad()}else if(u==="exit"){try{localStorage.removeItem("tf_fmap_drag")}catch{}l.remove(),location.reload()}}})}}window.addEventListener("resize",()=>{clearTimeout(window._fmapOrbitRz),window._fmapOrbitRz=setTimeout(()=>{const e=document.getElementById("p2-renown-mount");e&&e.querySelector(".fmap-orbit")&&_fmapOrbit(e)},150)});function _p2RenownWire(e,t){const n=async(r,c,l)=>{l.disabled=!0;try{await api("POST",`/api/campaigns/${t}/renown/events/${r}`,{action:c}),_campaignStale(t)||_p2RenownLoad()}catch(d){l.disabled=!1,showToast("Failed: "+(d.message||"try again"))}};e.querySelectorAll(".p2-rn-confirm").forEach(r=>r.addEventListener("click",()=>n(parseInt(r.dataset.ev,10),"confirm",r))),e.querySelectorAll(".p2-rn-dismiss").forEach(r=>r.addEventListener("click",()=>n(parseInt(r.dataset.ev,10),"dismiss",r))),_p2RnWireScales(e,async(r,c,l)=>{l&&(l.disabled=!0);try{const d=await api("POST",`/api/campaigns/${t}/renown/${r}/standing`,{standing:c});if(_campaignStale(t))return;_cx2RnCid=null,showToast("Standing set to "+(d&&d.label||_P2_RN_LABELS[String(c)])+".",4e3),_p2RenownLoad()}catch(d){l&&(l.disabled=!1),showToast("Failed: "+(d.message||"try again"))}},r=>{const c=(_p2RnData||[]).find(l=>
3160String(l.entity_id)===String(r.dataset.fid));c&&_fmapLogShow(c,r,n)});const s=e.querySelector("#p2-rn-add-btn"),i=e.querySelector("#p2-rn-track-panel");if(s&&i){const r=i.querySelector(".fmap-add-q"),c=i.querySelector(".fmap-add-list"),l=new Set((_p2RnData||[]).map(p=>p.entity_id)),d=(allEntities||[]).filter(p=>p&&p.type==="faction"&&!l.has(p.id)&&!p.pending_reveal&&!(p.aliases||[]).some(m=>(m||"").toLowerCase()==="the party")),u=()=>{const p=(r.value||"").trim().toLowerCase(),m=d.filter(g=>!p||g.name.toLowerCase().includes(p)||(g.aliases||[]).some(h=>(h||"").toLowerCase().includes(p))).sort((g,h)=>g.name.localeCompare(h.name)).slice(0,8),f=p?"Nothing by that name.":(allEntities||[]).length?"Every organization the codex knows is already tracked.":"Still loading the codex\u2026";c.innerHTML=m.length?m.map(g=>`<button type="button" class="fmap-add-row" data-fid="${g.id}" role="option">
3161            <span class="fmap-add-row-name">${escHtml(g.name)}</span>
3162            <span class="fmap-add-row-type">${escHtml((_codex2EntityLedger(g)||{}).kind||"Faction")}</span>
3163          </button>`).join(""):`<p class="form-helper-text fmap-add-empty">${f}</p>`,c.querySelectorAll(".fmap-add-row").forEach(g=>g.addEventListener("click",async()=>{g.disabled=!0;try{await api("POST",`/api/campaigns/${t}/renown/${parseInt(g.dataset.fid,10)}/track`,{tracked:!0}),_campaignStale(t)||_p2RenownLoad()}catch(h){g.disabled=!1,showToast("Failed: "+(h.message||"try again"))}}))};s.addEventListener("click",()=>{i.classList.toggle("hidden"),i.classList.contains("hidden")||(u(),r.focus())}),r.addEventListener("input",u)}e.querySelectorAll(".p2-rn-name--link").forEach(r=>r.addEventListener("click",()=>_fmapOpenCodex(t,parseInt(r.dataset.fid,10)))),e.querySelectorAll(".p2-rn-card").forEach(r=>{const c=parseInt(r.dataset.fid,10),l=(_p2RnData||[]).find(d=>d.entity_id===c);l&&_fmapWireCluster(r,t,l,l.members||[],()=>{_campaignStale(t)||_p2RenownLoad()},{emblemMenu:n})}),e.querySelectorAll(".fmap-agenda").forEach(r=>r.addEventListener("click",()=>{const c=(_p2RnData||[]).find(l=>l.entity_id===parseInt(r.dataset.fid,10));c&&_fmapEmblemMenu(t,c,r,()=>{_campaignStale(t)||_p2RenownLoad()},n)}));const o=e.querySelector(".fmap-party");if(o&&_p2RnParty){const r={entity_id:_p2RnParty.entity_id,name:_p2RnParty.name||"The Party",_party:!0},c={};(_p2RnParty.npc_members||[]).forEach(p=>{c[p.entity_id]={entity_id:p.entity_id,name:p.name,state:"met",killed:!1,is_dead:!!p.is_dead,met_override:null,pending_reveal:!1,image_url:p.image_url,matte_url:null,leader:!1,receipts:[],disposition:p.disposition,disposition_tone:p.disposition_tone,_partyStatus:p.status}});const l=()=>{_campaignStale(t)||_p2RenownLoad()};o.querySelectorAll(".fmap-token:not(.fmap-token--add):not(.fmap-token--more)").forEach(p=>p.addEventListener("click",()=>{const m=parseInt(p.dataset.mid,10);c[m]?_fmapPopShow(t,r,c[m],p,l):_fmapPartyPopShow(t,m,p,l)})),r.entity_id!=null&&o.querySelectorAll(".fmap-emblem[data-fid]").forEach(p=>p.addEventListener("click",async()=>{typeof openCx2ImageModal=="function"&&openCx2ImageModal(r.entity_id,l)}));const d=o.querySelector(".fmap-token--add"),u=o.querySelector(".fmap-add-panel");if(d&&u){const p=u.querySelector(".fmap-add-q"),m=u.querySelector(".fmap-add-list"),f=new Set(Object.keys(c).map(Number)),g=(allEntities||[]).filter(v=>v&&v.type==="npc"&&!f.has(v.id)&&!v.pending_reveal),h=()=>{const v=(p.value||"").trim().toLowerCase(),b=g.filter(y=>!v||y.name.toLowerCase().includes(v)||(y.aliases||[]).some(_=>(_||"").toLowerCase().includes(v))).sort((y,_)=>y.name.localeCompare(_.name)).slice(0,8);m.innerHTML=b.length?b.map(y=>`<button type="button" class="fmap-add-row" data-mid="${y.id}" role="option">
3164              <span class="fmap-add-row-name">${escHtml(y.name)}</span>
3165              <span class="fmap-add-row-type">NPC</span>
3166            </button>`).join(""):`<p class="form-helper-text fmap-add-empty">${v?"No one by that name.":"Still loading the codex\u2026"}</p>`,m.querySelectorAll(".fmap-add-row").forEach(y=>y.addEventListener("click",async()=>{y.disabled=!0;try{await api("POST",`/api/campaigns/${t}/renown/${r.entity_id}/members`,{member_entity_id:parseInt(y.dataset.mid,10)}),l()}catch(_){y.disabled=!1,showToast("Failed: "+(_.message||"try again"))}}))};d.addEventListener("click",()=>{u.classList.toggle("hidden"),u.classList.contains("hidden")||(h(),p.focus())}),p.addEventListener("input",h)}}}function _fmapOpenCodex(e,t){if(t&&(location.hash=`#codex/${e}/${t}`,typeof renderCodexEntry=="function"))try{renderCodexEntry(t)}catch{}}function _fmapLogClose(){const e=document.getElementById("fmap-log-pop");e&&e.remove(),document.removeEventListener("pointerdown",_fmapLogOutside,!0),document.removeEventListener("keydown",_fmapLogEsc,!0)}function _fmapLogOutside(e){const t=document.getElementById("fmap-log-pop");t&&!t.contains(e.target)&&!e.target.closest(".fmap-logbtn")&&_fmapLogClose()}function _fmapLogEsc(e){e.key==="Escape"&&_fmapLogClose()}function _fmapLogShow(e,t,n){const a=document.querySelector(`#fmap-log-pop[data-fid="${e.entity_id}"]`);if(_fmapLogClose(),a)return;const s=document.createElement("div");
3166s.id="fmap-log-pop",s.dataset.fid=e.entity_id,s.setAttribute("role","dialog"),s.setAttribute("aria-label",`Interaction log \u2014 ${e.name}`);const i=m=>m.kind==="agenda"?"Agenda ":"",o=(e.proposals||[]).map(m=>`<div class="p2-rn-prop" data-ev="${m.id}">
3167      <span class="p2-rn-prop-delta">${i(m)}${m.delta>0?"+"+m.delta:m.delta}</span>
3168      <span class="p2-rn-prop-note">${escHtml(m.note)}</span>
3169      <span class="p2-rn-prop-acts">
3170        <button type="button" class="btn-ghost btn-xs p2-rn-confirm" data-ev="${m.id}" title="${m.kind==="agenda"?"Apply this tick":"Apply this shift"}">\u2713</button>
3171        <button type="button" class="btn-ghost btn-xs p2-rn-dismiss" data-ev="${m.id}" title="Dismiss \u2014 never resurfaces">\u2715</button>
3172      </span>
3173    </div>`).join(""),r=(e.recent||[]).map(m=>`<span class="p2-rn-trail-row">${i(m)}${m.delta>0?"+"+m.delta:m.delta} \xB7 ${escHtml(m.note)}</span>`).join("");s.innerHTML=`
3174    <div class="fmap-pop-hdr">
3175      <span class="fmap-pop-name">${escHtml(e.name)}</span>
3176      <span class="fmap-pop-state-line">Interaction log</span>
3177    </div>
3178    ${o?`<div class="p2-rn-props">${o}</div>`:""}
3179    ${r?`<div class="p2-rn-trail">${r}</div>`:""}
3180    ${!o&&!r?'<p class="form-helper-text">Nothing recorded yet.</p>':""}`,document.body.appendChild(s);const c=t.getBoundingClientRect(),l=s.offsetWidth||300,d=s.offsetHeight||160;let u=Math.min(Math.max(8,c.left+c.width/2-l/2),window.innerWidth-l-8),p=c.bottom+8;p+d>window.innerHeight-8&&(p=Math.max(8,c.top-d-8)),s.style.left=`${Math.round(u)}px`,s.style.top=`${Math.round(p)}px`,document.addEventListener("pointerdown",_fmapLogOutside,!0),document.addEventListener("keydown",_fmapLogEsc,!0),s.querySelectorAll(".p2-rn-confirm").forEach(m=>m.addEventListener("click",()=>{n(parseInt(m.dataset.ev,10),"confirm",m),_fmapLogClose()})),s.querySelectorAll(".p2-rn-dismiss").forEach(m=>m.addEventListener("click",()=>{n(parseInt(m.dataset.ev,10),"dismiss",m),_fmapLogClose()}))}function _fmapWireCluster(e,t,n,a,s,i){i=i||{},e.querySelectorAll(".fmap-token:not(.fmap-token--add):not(.fmap-token--more)").forEach(f=>f.addEventListener("click",()=>{const g=(a||[]).find(h=>h.entity_id===parseInt(f.dataset.mid,10));g&&_fmapPopShow(t,n,g,f,s)})),e.querySelectorAll(".fmap-emblem[data-fid]").forEach(f=>f.addEventListener("click",async()=>{if(i.emblemMenu){_fmapEmblemMenu(t,n,f,s,i.emblemMenu);return}if(typeof openCx2ImageModal!="function")return;const g=+f.dataset.fid;!(allEntities||[]).some(h=>h.id===g)&&typeof loadEntities=="function"&&(await loadEntities(),_campaignStale(t))||openCx2ImageModal(g,()=>s&&s())}));const o=e.querySelector(".fmap-token--add"),r=e.querySelector(".fmap-add-panel");if(!o||!r)return;const c=r.querySelector(".fmap-add-q"),l=r.querySelector(".fmap-add-list"),d=new Set((a||[]).map(f=>f.entity_id)),u=(allEntities||[]).filter(f=>f&&["npc","pc","deity"].includes(f.type)&&!d.has(f.id)),p={npc:"NPC",pc:"PC",deity:"Deity"},m=()=>{const f=(c.value||"").trim().toLowerCase();_fmapAddQuery=c.value||"";const g=u.filter(v=>!f||v.name.toLowerCase().includes(f)||(v.aliases||[]).some(b=>(b||"").toLowerCase().includes(f))).sort((v,b)=>v.name.localeCompare(b.name)).slice(0,8),h=f?"No one by that name.":(allEntities||[]).length?"Everyone the codex knows is already here.":"Still loading the codex\u2026";l.innerHTML=g.length?g.map(v=>`<button type="button" class="fmap-add-row" data-mid="${v.id}" role="option">
3181          <span class="fmap-add-row-name">${escHtml(v.name)}</span>
3182          <span class="fmap-add-row-type">${p[v.type]||v.type}${v.pending_reveal?" \xB7 hidden":""}</span>
3183        </button>`).join(""):`<p class="form-helper-text fmap-add-empty">${h}</p>`,l.querySelectorAll(".fmap-add-row").forEach(v=>v.addEventListener("click",async()=>{v.disabled=!0;try{await api("POST",`/api/campaigns/${t}/renown/${n.entity_id}/members`,{member_entity_id:parseInt(v.dataset.mid,10)}),s&&s()}catch(b){v.disabled=!1,showToast("Failed: "+(b.message||"try again"))}}))};o.addEventListener("click",()=>{r.classList.toggle("hidden");const f=!r.classList.contains("hidden");_fmapAddOpenFid=f?n.entity_id:null,f||(_fmapAddQuery=""),f&&(m(),c.focus())}),c.addEventListener("input",m),_fmapAddOpenFid===n.entity_id&&(r.classList.remove("hidden"),c.value=_fmapAddQuery,m(),c.focus())}let _fmapAddOpenFid=null,_fmapAddQuery="";document.addEventListener("pointerdown",e=>{document.querySelectorAll(".fmap-add-panel:not(.hidden)").forEach(t=>{if(t.contains(e.target)||e.target.closest&&(e.target.closest(".fmap-token--add")||e.target.closest("#p2-rn-add-btn")))return;const n=t.querySelector(".fmap-add-q");
3183n&&n.value.trim()||(t.classList.add("hidden"),t.dataset.fid&&String(_fmapAddOpenFid)===String(t.dataset.fid)&&(_fmapAddOpenFid=null,_fmapAddQuery=""))})},!0);let _cx2FmapSeq=0;async function _cx2FmapLoad(e,t){if(!window.FactionMap)return;const n=++_cx2FmapSeq;let a;try{a=await api("GET",`/api/campaigns/${e}/factions/${t}/members`)}catch{const r=document.querySelector(`.cx2-fmap-host[data-fid="${t}"]`);r&&n===_cx2FmapSeq&&(r.innerHTML='<p class="form-helper-text">Couldn\u2019t load members.</p>');return}if(_campaignStale(e)||n!==_cx2FmapSeq)return;const s=document.querySelector(`.cx2-fmap-host[data-fid="${t}"]`);if(!s)return;let i=a.members||[];if(!_isGm()){if(i=_fmapPlayerProject(i),!i.length){const o=s.closest(".cx2-fmap-panel");o&&o.remove();return}s.innerHTML=FactionMap.renderClusterExpanded(a.faction,i,{emblemSize:72,sizeOther:38,maxRadius:100,readonly:!0}),s.querySelectorAll(".fmap-token[data-mid]").forEach(o=>o.addEventListener("click",()=>{const r=parseInt(o.dataset.mid,10);r&&typeof navigateToEntity=="function"&&navigateToEntity(r)}));return}s.innerHTML=FactionMap.renderClusterExpanded(a.faction,i,{emblemSize:72,sizeOther:38,maxRadius:100,addToken:!0})+_fmapAddPanelHtml(t),_fmapWireCluster(s,e,a.faction,i,()=>_cx2FmapLoad(e,t))}function _fmapPlayerProject(e){return(e||[]).map(t=>{if(!t||!t.pending_reveal)return t;if(!t.tease_position)return null;const n={...t,reveal_masked:!0,receipts:[],met_override:null};return delete n.tease_alias,t.tease_name||(n.name=_teaseAlias(t)),t.tease_photo||(n.image_url=null,n.matte_url=null),n}).filter(Boolean)}function _fmapPopClose(){const e=document.getElementById("fmap-pop");if(e){try{typeof e._flushPhrase=="function"&&e._flushPhrase()}catch{}e.remove()}document.removeEventListener("pointerdown",_fmapPopOutside,!0),document.removeEventListener("keydown",_fmapPopEsc,!0)}function _fmapPopOutside(e){const t=document.getElementById("fmap-pop");t&&!t.contains(e.target)&&!e.target.closest(".fmap-token")&&_fmapPopClose()}function _fmapPopEsc(e){e.key==="Escape"&&_fmapPopClose()}const _FMAP_TONE_ORDER=["enemy","unfriendly","neutral","friendly","ally"],_FMAP_TONE_LBL={enemy:"Enemy",unfriendly:"Unfriendly",neutral:"Neutral",friendly:"Friendly",ally:"Ally"},_FMAP_TONE_RN={enemy:"hostile",unfriendly:"cool",neutral:"neutral",friendly:"warm",ally:"ally"};function _fmapToneScaleHtml(e){const t=_FMAP_TONE_ORDER.map((s,i)=>{const o=e===s,r=["p2-rn-tick","fmap-pop-tone"];return o&&r.push("p2-rn-tick--current","p2-rn-tone--"+_FMAP_TONE_RN[s]),`<button type="button" class="${r.join(" ")}" style="left:${i*25}%" role="radio" aria-checked="${o}" data-tone="${s}" title="${_FMAP_TONE_LBL[s]}${o?" \u2014 tap again to clear":""}" aria-label="Set disposition to ${_FMAP_TONE_LBL[s]}"></button>`}).join(""),n=e==="enemy"?" p2-rn-cur-lbl--lo":e==="ally"?" p2-rn-cur-lbl--hi":"",a=e?`<span class="p2-rn-cur-lbl p2-rn-tone--${_FMAP_TONE_RN[e]}${n}" style="left:${_FMAP_TONE_ORDER.indexOf(e)*25}%">${_FMAP_TONE_LBL[e]}</span>`:"";return`<div class="p2-rn-scale fmap-tone-scale${e==="enemy"?" p2-rn-scale--hostile":""}" role="radiogroup" aria-label="Disposition toward the party">
3184    <div class="p2-rn-track"><span class="p2-rn-heat" aria-hidden="true"></span>${a}${t}</div>
3185    <div class="p2-rn-caps" aria-hidden="true"><span${e==="enemy"?' class="is-cur"':""}>Enemy</span><span${e==="ally"?' class="is-cur"':""}>Ally</span></div>
3186  </div>`}function _fmapToneWire(e,t,n){if(!e||e.dataset.toneWired==="1")return;e.dataset.toneWired="1";let a=t||null;const s=e.querySelector(".p2-rn-track"),i=[...e.querySelectorAll(".p2-rn-tick")],o=f=>{const g=s.getBoundingClientRect();return Math.round(Math.min(1,Math.max(0,(f-g.left)/g.width))*4)},r=f=>{i.forEach(v=>{const b=v.dataset.tone===f;_FMAP_TONE_ORDER.forEach(y=>v.classList.remove("p2-rn-tone--"+_FMAP_TONE_RN[y])),v.classList.toggle("p2-rn-tick--current",b),b&&v.classList.add("p2-rn-tone--"+_FMAP_TONE_RN[f]),v.setAttribute("aria-checked",String(b))});let g=s.querySelector(".p2-rn-cur-lbl");f?(g||(g=document.createElement("span"),s.appendChild(g)),g.className="p2-rn-cur-lbl p2-rn-tone--"+_FMAP_TONE_RN[f]+(f==="enemy"?" p2-rn-cur-lbl--lo":f==="ally"?" p2-rn-cur-lbl--hi":""),g.style.left=_FMAP_TONE_ORDER.indexOf(f)*25+"%",g.textContent=_FMAP_TONE_LBL[f]):g&&g.remove(),e.classList.toggle("p2-rn-scale--hostile",f==="enemy");const h=e.querySelectorAll(".p2-rn-caps span");h.length===2&&(h[0].classList.toggle("is-cur",f==="enemy"),h[1].classList.toggle("is-cur",f==="ally"))},c=f=>{a=f,r(f),n(f)};let l=!1,d=!1,u=0,p=null;s.addEventListener("pointerdown",f=>{l=!0,d=!1,u=f.clientX,p=_FMAP_TONE_ORDER[o(f.clientX)];try{s.setPointerCapture(f.pointerId)}catch{}e.classList.add("is-dragging"),r(p),f.preventDefault()}),s.addEventListener("pointermove",f=>{if(!l)return;Math.abs(f.clientX-u)>4&&(d=!0);const g=_FMAP_TONE_ORDER[o(f.clientX)];g!==p&&(p=g,d=!0,r(g))});const m=()=>{l&&(l=!1,e.classList.remove("is-dragging"),d?p!==a?c(p):r(a):c(p===a?null:p)
3186)};s.addEventListener("pointerup",m),s.addEventListener("pointercancel",()=>{l=!1,e.classList.remove("is-dragging"),r(a)}),i.forEach(f=>f.addEventListener("click",g=>{g.detail===0&&c(a===f.dataset.tone?null:f.dataset.tone)}))}let _npcStand=null,_npcStandCid=null,_npcStandP=null;function _npcStandLoad(e,t){if(!e)return Promise.resolve(null);if(!t&&_npcStandCid===e&&_npcStand)return Promise.resolve(_npcStand);if(!t&&_npcStandP&&_npcStandP.cid===e)return _npcStandP.p;const n=api("GET",`/api/campaigns/${e}/npc-standing`).then(a=>{if(_campaignStale(e))return null;const s={};return(a&&a.npcs||[]).forEach(i=>{s[i.entity_id]=i}),_npcStand=s,_npcStandCid=e,s}).catch(()=>null).finally(()=>{_npcStandP&&_npcStandP.p===n&&(_npcStandP=null)});return _npcStandP={cid:e,p:n},n}function _npcStandOf(e){const t=currentCampaign&&currentCampaign.id;return _npcStandCid===t&&_npcStand&&_npcStand[e]||null}async function _npcStandSave(e,t,n){const a=await api("POST",`/api/campaigns/${e}/npc-standing/${t}`,n);return _campaignStale(e)?null:(_npcStandCid===e&&_npcStand&&(_npcStand[t]={entity_id:t,tone:a.tone,phrase:a.phrase,agenda:a.agenda}),typeof _p2RnData<"u"&&(_p2RnData=null),a)}function _npcToneLabel(e){return e?_FMAP_TONE_LBL[e]||e:"Unknown"}let _cx2Rn=null,_cx2RnCid=null;async function _cx2RenownEnsure(e){if(_cx2RnCid===e&&_cx2Rn)return _cx2Rn;let t;try{t=await api("GET",`/api/campaigns/${e}/renown`)}catch{return null}return _campaignStale(e)?null:(_cx2Rn=t&&t.factions||[],_cx2RnCid=e,_cx2Rn)}
3186function _cx2StandRowHtml(e,t){if(!e||e.type!=="npc"&&e.type!=="faction")return"";const n=e.type==="npc"?"Disposition":"Standing";return`<div class="cx2-ledger-row cx2-stand-row hidden" data-cx2-stand="${e.id}" data-edit="${t?"1":"0"}"><span class="cx2-ledger-key">${n}</span><span class="cx2-ledger-val"></span></div>`}function _cx2StandKnown(e){if(e.type==="npc"){const a=_npcStandOf(e.id);return!!(a&&(a.tone||a.phrase))}const t=currentCampaign&&currentCampaign.id,n=_cx2RnCid===t&&_cx2Rn?_cx2Rn.find(a=>a.entity_id===e.id):null;return!!(n&&n.standing!=null)}async function _cx2FillStanding(e){const t=currentCampaign&&currentCampaign.id,n=(allEntities||[]).find(o=>o.id===e),a=()=>[...document.querySelectorAll(`.cx2-stand-row[data-cx2-stand="${e}"]`)];if(!t||!n)return;const s=n.type==="npc"?!(_npcStandCid===t&&_npcStand):!(_cx2RnCid===t&&_cx2Rn);if(n.type==="npc"?await _npcStandLoad(t):await _cx2RenownEnsure(t),_campaignStale(t))return;if(!a().length){if(s&&_cx2StandKnown(n)){const o=(location.hash||"").match(/^#codex\/(\d+)\/(\d+)$/),r=document.getElementById("tab-mychar");(o&&+o[2]===e||r&&!r.classList.contains("hidden")&&typeof _myCharSelId<"u"&&_myCharSelId===e)&&_cx2RerenderEntry(e)}return}a().forEach(o=>{const r=o.dataset.edit==="1",c=o.querySelector(".cx2-ledger-val");if(n.type==="npc"){const l=_npcStandOf(e)||{};if(!r&&!l.tone&&!l.phrase){o.remove();return}c.innerHTML=(r?_fmapToneScaleHtml(l.tone||null):escHtml(_npcToneLabel(l.tone)))+(l.phrase?`<span class="cx2-stand-phrase">${escHtml(l.phrase)}</span>`:""),o.classList.remove("hidden"),r&&_fmapToneWire(c.querySelector(".fmap-tone-scale"),l.tone||null,d=>_npcStandSave(t,e,{tone:d}).catch(u=>showToast("Could not save: "+(u&&u.message||"try again"))))}else{const l=(_cx2Rn||[]).find(d=>d.entity_id===e);if(!l||!r&&l.standing==null){o.remove();return}c.innerHTML=r?_p2RnScaleHtml(l,!0):escHtml(_P2_RN_LABELS[String(l.standing)]||"Unknown"),o.classList.remove("hidden"),r&&_p2RnWireScales(c,(d,u)=>_cx2FactionSet(t,+d,u),()=>showToast("A change is proposed for this faction. Review it on Party.",4e3))}});const i=_npcStandOf(e);document.querySelectorAll(`.cx2-wants-input[data-cx2-wants="${e}"]`).forEach(o=>{document.activeElement!==o&&!o.value&&(o.value=i&&i.agenda||"")})}async function _cx2FactionSet(e,t,n){try{if(await api("POST",`/api/campaigns/${e}/renown/${t}/standing`,{standing:n}),_campaignStale(e))return;const a=(_cx2Rn||[]).find(s=>s.entity_id===t);a&&(a.standing=n),typeof _p2RnData<"u"&&(_p2RnData=null),typeof _btsFactions<"u"&&(_btsFactions=null,_btsFactionsCid=null),_cx2FillStanding(t)}catch(a){showToast("Could not save: "+(a&&a.message||"try again"))}}function _cx2WantsHtml(e){const t=_npcStandOf(e.id);return`<div class="cx2-wants"><span class="cx2-wants-lbl">Wants</span><input type="text" class="cx2-wants-input" data-cx2-wants="${e.id}" maxlength="300" value="${escHtml(t&&t.agenda||"")}" placeholder="What ${escHtml(e.name)} wants" aria-label="What ${escHtml(e.name)} wants, GM only"></div>`}document.addEventListener("change",e=>{const t=e.target&&e.target.closest&&e.target.closest(".cx2-wants-input");if(!t||!_isGm())return;const n=currentCampaign&&currentCampaign.id,a=+t.dataset.cx2Wants;n&&a&&_npcStandSave(n,a,{agenda:t.value.trim()}).catch(s=>showToast("Could not save: "+(s&&s.message||"try again")))});function _fmapEmblemMenu(e,t,n,a,s){const i=document.querySelector(`#fmap-pop[data-fid="${t.entity_id}"][data-menu="1"]`);if(_fmapPopClose(),i)return;const o=(t.proposals||[]).length,r=document.createElement("div");r.id="fmap-pop",r.dataset.fid=t.entity_id,r.dataset.menu="1",r.setAttribute("role","menu"),r.setAttribute("aria-label",`${t.name} options`),r.innerHTML=`
3187    <div class="fmap-pop-hdr fmap-pop-hdr--menu">
3188      <div class="fmap-pop-id">
3189        <span class="fmap-pop-name">${escHtml(t.name)}</span>
3190        ${t.kind?`<span class="fmap-pop-state-line">${escHtml(t.kind)}</span>`:""}
3191      </div>
3192      <div class="fmap-pop-icons">
3193        <button type="button" class="fmap-pop-iconbtn" data-act="image" role="menuitem" title="Update image" aria-label="Update image for ${escHtml(t.name)}">${_FMAP_IMG_SVG}</button>
3194        <button type="button" class="fmap-pop-iconbtn" data-act="log" role="menuitem" title="Standing log${o?` \xB7 ${o} pending`:""}" aria-label="Standing log for ${escHtml(t.name)}">${_FMAP_BOOK_SVG}${o?'<span class="fmap-logdot" aria-hidden="true"></span>':""}</button>
3195        <button type="button" class="fmap-pop-iconbtn fmap-pop-iconbtn--danger" data-act="untrack" role="menuitem" title="Remove from tracker" aria-label="Remove ${escHtml(t.name)} from tracker">\u2715</button>
3196      </div>
3197    </div>
3198    ${t._party?"":`<div class="fmap-agenda-block gm-surface">
3199      <div class="gm-surface-head">
3200        <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>
3201        <span class="gm-surface-label">Agenda</span>
3202        <span class="gm-surface-seal">GM&nbsp;only</span>
3203      </div>
3204      <div class="fmap-agenda-body"></div>
3205    </div>`}`,document.body.appendChild(r),_fmapAgendaBlock(r,e,t,a);const c=n.getBoundingClientRect(),l=r.offsetWidth||240,d=r.offsetHeight||140;let u=Math.min(Math.max(8,c.left+c.width/2-l/2),window.innerWidth-l-8),p=c.bottom+8;p+d>window.innerHeight-8&&(p=Math.max(8,c.top-d-8)),r.style.left=`${Math.round(u)}px`,r.style.top=`${Math.round(p)}px`,document.addEventListener("pointerdown",_fmapPopOutside,!0),document.addEventListener("keydown",_fmapPopEsc,!0),r.querySelector('[data-act="image"]').addEventListener("click",async()=>{_fmapPopClose(),typeof openCx2ImageModal=="function"&&(!(allEntities||[]).some(m=>m.id===t.entity_id)&&typeof loadEntities=="function"&&(await loadEntities(),_campaignStale(e))||openCx2ImageModal(t.entity_id,()=>a&&a()))}),r.querySelector('[data-act="log"]').addEventListener("click",()=>{_fmapPopClose(),_fmapLogShow(t,n,s)}),r.querySelector('[data-act="untrack"]').addEventListener("click",async m=>{m.currentTarget.disabled=!0;try{await api("POST",`/api/campaigns/${e}/renown/${t.entity_id}/track`,{tracked:!1}),_fmapPopClose(),a&&a()}catch(f){m.currentTarget.disabled=!1,showToast("Failed: "+(f.message||"try again"))}})}function _fmapAgendaBlock(e,t,n,a){const s=e.querySelector(".fmap-agenda-body");if(!s)return;const i=async(c,l,d)=>{if(_campaignStale(t)){_fmapPopClose();return}d&&(d.disabled=!0);try{await api("POST",`/api/campaigns/${t}/renown/${n.entity_id}${c}`,l),_fmapPopClose(),a&&a()}catch(u){d&&(d.disabled=!1),showToast("Failed: "+(u.message||"try again"))}},o=()=>{const c=n.agenda_clock_size||6;s.innerHTML=`
3206      <input type="text" class="form-input fmap-agenda-input" maxlength="200"
3207        placeholder="What are they working toward?" value="${escHtml(n.agenda||"")}">
3208      <div class="fmap-agenda-sizerow">
3209        <span class="form-section-label">Steps to complete</span>
3210        <select class="form-input fmap-agenda-size">
3211          ${[[4,"quick"],[6,"typical"],[8,"a stretch"],[10,"long game"],[12,"epic"]].map(([d,u])=>`<option value="${d}"${d===c?" selected":""}>${d} steps \xB7 ${u}</option>`).join("")}
3212        </select>
3213      </div>
3214      ${n.agenda?"":'<p class="form-helper-text fmap-agenda-helper">Fill a step whenever the story moves them closer: nudge it yourself, or confirm the recap\u2019s suggestion.</p>'}
3215      <div class="fmap-pop-foot">
3216        <button type="button" class="btn-ghost btn-xs" data-act="save-agenda">Save</button>
3217        <button type="button" class="btn-ghost btn-xs" data-act="cancel-agenda">Cancel</button>
3218        ${n.agenda?'<button type="button" class="btn-ghost btn-xs btn-ghost--danger" data-act="clear-agenda">Clear</button>':""}
3219      </div>`,s.querySelector('[data-act="save-agenda"]').addEventListener("click",d=>{const u=s.querySelector(".fmap-agenda-input").value.trim();if(!u){showToast("Name the goal first.");return}i("/agenda",{agenda:u,clock_size:parseInt(s.querySelector(".fmap-agenda-size").value,10)},d.currentTarget)}),s.querySelector('[data-act="cancel-agenda"]').addEventListener("click",r);const l=s.querySelector('[data-act="clear-agenda"]');l&&l.addEventListener("click",d=>i("/agenda",{agenda:null},d.currentTarget)),s.querySelector(".fmap-agenda-input").focus()},r=()=>{if(!n.agenda){s.innerHTML='<button type="button" class="fmap-agenda-empty">+ Give them a goal</button>',s.querySelector(".fmap-agenda-empty").addEventListener("click",o);return}const c=n.agenda_clock_size||6,l=n.agenda_clock_filled||0,d=l>=c;s.innerHTML=`
3220      <div class="fmap-agenda-view${d?" fmap-agenda--done":""}">
3221        <button type="button" class="fmap-agenda-open" title="Edit the agenda">
3222          ${_fmapClockSvg(c,l,22)}
3223          <span class="fmap-agenda-view-text">
3224            <span class="fmap-agenda-view-goal">${escHtml(n.agenda)}</span>
3225            <span class="fmap-agenda-view-progress">${d?"Complete \xB7 wants a scene":`${l} of ${c} steps`}</span>
3226          </span>
3227        </button>
3228        <span class="fmap-agenda-ticks">
3229          <button type="button" class="btn-ghost btn-xs" data-tick="-1"${l<=0?" disabled":""} title="A setback: one step back">\u22121</button>
3230          <button type="button" class="btn-ghost btn-xs" data-tick="1"${l>=c?" disabled":""} title="They moved closer: one step forward">\uFF0B1</button>
3231        </span>
3232      </div>`,s.querySelector(".fmap-agenda-open").addEventListener("click",o),s.querySelectorAll("[data-tick]").forEach(u=>u.addEventListener("click",p=>i("/agenda/tick",{delta:parseInt(u.dataset.tick,10)},p.currentTarget)))};r()}function _fmapPartyPopShow(e,t,n,a){const s=document.querySelector(`#fmap-pop[data-mid="${t}"][data-fid="party"]`);if(_fmapPopClose(),s)return;const i=(allEntities||[]).find(g=>g&&g.id===t);if(!i||!_p2RnParty)return;const o=i.type==="pc",r=_p2RnParty.leader_entity_id!=null&&t===_p2RnParty.leader_entity_id,c=document.createElement("div");c.id="fmap-pop",c.dataset.mid=t,c.dataset.fid="party",c.setAttribute("role","dialog"),c.setAttribute("aria-label",`${i.name} \u2014 ${_p2RnParty.name||"The Party"}`),c.innerHTML=`
3233    <div class="fmap-pop-hdr">
3234      <span class="fmap-pop-name">${escHtml(i.name)}</span>
3235      <span class="fmap-pop-state-line">${i.is_dead?"Fallen":i.is_inactive?"Departed":o?"Of the company":"Companion"}</span>
3236    </div>
3237    ${o?`<div class="fmap-pop-life" role="radiogroup" aria-label="Lifecycle">
3238      ${(()=>{const g=i.is_dead?"dead":i.is_inactive?"departed":"active";return[["active","Active"],["dead","Fallen"],["departed","Departed"]].map(([h,v])=>`<button type="button" class="btn-ghost btn-xs fmap-pop-lifebtn${h===g?" is-on":""}" data-life="${h}" aria-pressed="${h===g}">${v}</button>`).join("")})()}
3239    </div>`:""}
3240    <div class="fmap-pop-foot">
3241      ${o&&!i.is_dead&&!i.is_inactive&&_p2RnParty.entity_id!=null?`<button type="button" class="btn-ghost btn-xs fmap-pop-leader">${r?"Unmark leader":"Mark as party leader"}</button>`:""}
3242      <button type="button" class="btn-ghost btn-xs fmap-pop-codex">Open in Codex</button>
3243    </div>`,document.body.appendChild(c);const l=n.getBoundingClientRect(),d=c.offsetWidth||280,u=c.offsetHeight||100;let p=Math.min(Math.max(8,l.left+l.width/2-d/2),window.innerWidth-d-8),m=l.bottom+8;m+u>window.innerHeight-8&&(m=Math.max(8,l.top-u-8)),c.style.left=`${Math.round(p)}px`,c.style.top=`${Math.round(m)}px`,document.addEventListener("pointerdown",_fmapPopOutside,!0),document.addEventListener("keydown",_fmapPopEsc,!0);const f=c.querySelector(".fmap-pop-leader");f&&f.addEventListener("click",async g=>{if(_campaignStale(e)){_fmapPopClose();return}g.currentTarget.disabled=!0;try{await api("POST",`/api/campaigns/${e}/renown/${_p2RnParty.entity_id}/members/${t}/leader`,{leader:!r}),_fmapPopClose(),a&&a()}catch(h){g.currentTarget.disabled=!1,showToast("Failed: "+(h.message||"try again"))}}),c.querySelectorAll(".fmap-pop-lifebtn").forEach(g=>g.addEventListener("click",async h=>{if(_campaignStale(e)){_fmapPopClose();return}const v=g.dataset.life;h.currentTarget.disabled=!0;try{await api("PUT",`/api/campaigns/${e}/entities/${t}`,{is_dead:v==="dead",is_inactive:v==="departed"}),typeof loadEntities=="function"&&await loadEntities(),_fmapPopClose(),a&&a()}catch(b){h.currentTarget.disabled=!1,showToast("Failed: "+(b.message||"try again"))}})),c.querySelector(".fmap-pop-codex").addEventListener("click",()=>{_fmapPopClose(),_fmapOpenCodex(e,t)})}const _FMAP_STATE_LINE=e=>e.killed?"Killed by the party":e.is_dead?"Dead":e.state==="met"?"Met":e.state==="mentioned"?"Heard of \u2014 not yet met":"Not yet in the story";function _fmapPopShow(e,t,n,a,s){const i=t&&t.entity_id,o=n&&n.entity_id,r=document.querySelector(`#fmap-pop[data-mid="${o}"][data-fid="${i}"]`);if(_fmapPopClose(),r||!t||!n)return;const c=document.createElement("div");
3243c.id="fmap-pop",c.dataset.mid=o,c.dataset.fid=i,c.setAttribute("role","dialog"),c.setAttribute("aria-label",`${n.name} \u2014 ${t.name}`);const l=t._party||n.killed?"":(n.state==="met"?'<button type="button" class="btn-ghost btn-xs fmap-pop-state" data-set="mentioned">Mark not yet met</button>':'<button type="button" class="btn-ghost btn-xs fmap-pop-state" data-set="met">Mark as met</button>')+(n.met_override?'<button type="button" class="btn-ghost btn-xs fmap-pop-state" data-set="" title="Return to the derived state">Reset to automatic</button>':""),d=t._party?`<button type="button" class="btn-ghost btn-xs fmap-pop-member">${n._partyStatus==="departed"?"Mark active":"Mark as left the party"}</button>`:"",u=(n.receipts||[]).length?`<div class="p2-rn-trail">${n.receipts.map(k=>`<span class="p2-rn-trail-row">${escHtml(k.note)}</span>`).join("")}</div>`:"",p=n.state==="unknown"?'<p class="form-helper-text">Not yet in the story \u2014 nothing to record until the party hears of them.</p>':`<div class="fmap-pop-disp">
3244      <span class="form-section-label">Disposition</span>
3245      <textarea class="form-input fmap-pop-ta" rows="2" maxlength="120"
3246        placeholder="The party's known relationship \u2014 \u201Cowes them a life-debt\u201D, never secret intent.">${escHtml(n.disposition||"")}</textarea>
3247      ${_fmapToneScaleHtml(n.disposition_tone)}
3248      <p class="form-helper-text">Write it as the party knows it \u2014 this goes player-visible when the player view ships.</p>
3249    </div>`;c.innerHTML=`
3250    <div class="fmap-pop-hdr">
3251      <span class="fmap-pop-name">${escHtml(n.name)}</span>
3252      <span class="fmap-pop-state-line">${t._party?n._partyStatus==="departed"?"Left the party":"Active":_FMAP_STATE_LINE(n)}${n.pending_reveal?" \xB7 Hidden from players"+(_teaseSummary(n)?" \xB7 "+escHtml(_teaseSummary(n)):""):""}</span>
3253    </div>
3254    ${p}
3255    ${u}
3256    <div class="fmap-pop-foot">
3257      ${l}${d}
3258      <button type="button" class="btn-ghost btn-xs fmap-pop-leader">${n.leader?"Unmark leader":"Mark as leader"}</button>
3259      <button type="button" class="btn-ghost btn-xs btn-ghost--danger fmap-pop-remove">Remove</button>
3260      <button type="button" class="btn-ghost btn-xs fmap-pop-codex">Open in Codex</button>
3261    </div>`,document.body.appendChild(c);const m=a.getBoundingClientRect(),f=c.offsetWidth||280,g=c.offsetHeight||200;let h=Math.min(Math.max(8,m.left+m.width/2-f/2),window.innerWidth-f-8),v=m.bottom+8;v+g>window.innerHeight-8&&(v=Math.max(8,m.top-g-8)),c.style.left=`${Math.round(h)}px`,c.style.top=`${Math.round(v)}px`,document.addEventListener("pointerdown",_fmapPopOutside,!0),document.addEventListener("keydown",_fmapPopEsc,!0);let b=n.disposition_tone||null;const y=async(k,C,S,q)=>{if(_campaignStale(e)){_fmapPopClose();return}S&&(S.disabled=!0);try{await api("POST",`/api/campaigns/${e}/renown/${i}/members/${o}${k}`,C||{}),q&&q.keepOpen?S&&(S.disabled=!1):document.querySelector(`#fmap-pop[data-mid="${o}"][data-fid="${i}"]`)&&_fmapPopClose(),s&&s()}catch(I){S&&(S.disabled=!1),showToast("Failed: "+(I.message||"try again"))}},_=c.querySelector(".fmap-tone-scale"),w=()=>{_npcStandCid=null;const k=(c.querySelector(".fmap-pop-ta")?.value||"").trim();y("/disposition",{disposition:k||null,disposition_tone:b},null,{keepOpen:!0})};_&&_fmapToneWire(_,b,k=>{b=k,w()});const $=c.querySelector(".fmap-pop-ta");if($){let k=(n.disposition||"").trim(),C=null;const S=()=>{clearTimeout(C),C=null;const q=$.value.trim();q!==k&&(k=q,w())};$.addEventListener("input",()=>{clearTimeout(C),C=setTimeout(S,900)}),$.addEventListener("blur",S),c._flushPhrase=S}c.querySelectorAll(".fmap-pop-state").forEach(k=>k.addEventListener("click",C=>y("/state",{met_override:C.currentTarget.dataset.set||null},C.currentTarget))),c.querySelector(".fmap-pop-leader").addEventListener("click",k=>y("/leader",{leader:!n.leader},k.currentTarget));const E=c.querySelector(".fmap-pop-member");E&&E.addEventListener("click",k=>y("/membership",{status:n._partyStatus==="departed"?"confirmed":"departed"},k.currentTarget)),c.querySelector(".fmap-pop-remove").addEventListener("click",k=>y("/remove",{},k.currentTarget)),c.querySelector(".fmap-pop-codex").addEventListener("click",()=>{_fmapPopClose(),_fmapOpenCodex(e,o)})}function _p2Memoriam(e){const t=e.fallen||[];return t.length?'<div class="party2-page"><div class="index-inner">'+_p2Masthead({eyebrow:"Party",titleHtml:"In Memoriam",leadHtml:"Those the Descent Kept. The Company carries them still; the ledger does not forget what was paid."})+'<div class="p2-memoriam">'+t.map(n=>{const a=n.death_session&&n.death_session.n?`Fell \xB7 Ch. ${n.death_session.n}`:n.tenure&&n.tenure.last&&n.tenure.last.n?`Last seen \xB7 Ch. ${n.tenure.last.n}`:null,s=n.tenure,i=s?`${s.sessions} ${s.sessions===1?"session":"sessions"} under the banner`:null,o=_p2ClassLine(n)||n.snippet||null;return`<section class="p2-memorial"><div class="p2-mem-portrait">${_p2Portrait(n,"p2-grayscale").replace("p2-portrait ","p2-portrait p2-mem-img ")}<span class="p2-mem-stone" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 23V11a6 6 0 0 1 12 0v12z"/><rect x="3.4" y="21.6" width="17.2" height="2.4" rx="0.8"/></svg></span></div><div class="p2-mem-body"><span class="entity-link p2-mem-name pipe" data-id="${n.id}" tabindex="0" role="button">${_p2PipeName(n.name)}</span>
3261`+(o?`<div class="p2-mem-title">${_p2Esc(o)}</div>`:"")+(a||i?`<div class="p2-mem-fell">${[a,i].filter(Boolean).map(_p2Esc).join(" \xB7 ")}</div>`:"")+_p2MemEpitaph(n)+_p2MemNote(n,"lastwords")+_p2MemorialTie(n,e)+"</div></section>"}).join("")+`</div><div class="p2-valediction"><div class="ornament index-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div><p class="p2-vale-line">Rest easy in the long dark. The Company marches on, and carries you still.</p></div>`+_p2Colophon("In Memoriam")+"</div></div>":""}function _p2StripCot(e){return e&&String(e).replace(/\s*\([^)]*\b(?:wait|recheck|let me|actually|hold on)\b[^)]*\)/gi,"").replace(/\s*[—–-]\s*(?:wait|recheck|let me|actually|hold on)\b[^.]*\./gi,".").replace(/\s{2,}/g," ").replace(/\s+([.,;])/g,"$1").trim()}const _P2_SYNC_HREF="https://chromewebstore.google.com/detail/lbkabfefehjgfoeackpiomkikhbnbndo";function _p2OnboardBanner(e){if(typeof _isGm=="function"&&!_isGm())return"";let t=!1;try{t=localStorage.getItem("tf_party_intro_dismissed_v1")==="1"}catch{}if(t)return"";const n=e.dice||{};if(n.available)return"";const a=e.counts&&e.counts.active||0,s=(c,l,d,u)=>`<section class="tab-intro-banner cool-paper-surface p2-onboard-banner" data-tf-intro-key="tf_party_intro_dismissed_v1" aria-label="${escHtml(l)}"><button class="tab-intro-dismiss tf-intro-dismiss" title="Dismiss" aria-label="Dismiss intro">\u2715</button><div class="tab-intro-content"><span class="tab-intro-eyebrow">${c}</span><h3 class="tab-intro-title">${l}</h3><p class="tab-intro-body">${d}</p>`+(u?`<div class="p2-empty-actions">${u}</div>`:"")+"</div></section>",i=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart(),o=c=>i?"":`<a class="tf2-onboard-pill p2-empty-sync" href="${_P2_SYNC_HREF}?utm_source=${c}" target="_blank" rel="noopener noreferrer">Get Party Sync for Chrome \u2192</a>`;if(a===0)return s("PARTY",i?"Add your party, then let Threadfall read them.":"Pull in your whole party, then let Threadfall read them.",i?"Add each PC, then bring in their sheets: paste a Demiplane link on the PC, or upload a PDF or photo of the character sheet. Threadfall then measures the company\u2019s coverage and traits.":"The free Party Sync extension imports every PC sheet from your D&amp;D Beyond campaign in one click, then Threadfall measures the company\u2019s strengths, gaps, and watch-outs.",o("threadfall-party-empty")+'<button type="button" class="tf2-onboard-pill p2-empty-newpc">\uFF0B New PC</button><button type="button" class="tf2-onboard-pill p2-empty-import">\u{1F4C4} Import PC(s)</button>');if((n.with_data||0)===0)return s("PARTY","Your party\u2019s in, now import their sheets.",i?"Threadfall needs imported character sheets to measure the company. Paste a Demiplane link or upload a sheet on any PC card, and the measure unlocks.":"Threadfall needs imported character sheets to measure the company. Sync from D&amp;D Beyond, or upload a sheet on any PC card, and the dossier unlocks.",o("threadfall-party-sheets")+'<button type="button" class="tf2-onboard-pill p2-empty-import">\u{1F4C4} Import PC(s)</button>');const r=_p2ReassessBtn(e,"Assess Party");return r?s("Party Dossier","See how your party is built.","Threadfall reads your imported sheets and tells you where your party is strong, where they\u2019re exposed, and what combinations to lean on.",r):""}let _p2RailState={pinned:null,active:null};const _P2_SECTION_SLUGS={assessment:"p2-sec-assessment",quests:"p2-sec-quests",standing:"p2-sec-factions",annals:"p2-sec-annals",departed:"p2-sec-departed",memoriam:"p2-sec-memoriam"};function _p2SlugOf(e){return Object.keys(_P2_SECTION_SLUGS).find(t=>_P2_SECTION_SLUGS[t]===e)||null}async function _partyOpenQuests(e,t){if(e=e||currentCampaign&&currentCampaign.id,!e)return;const n=_party2Panel();typeof _tfRouterPush=="function"&&_tfRouterPush({type:"campaign",cid:e,tab:"party",sid:null}),_party2Show(n,`#party/${e}/quests${t?"/"+t:""}`),typeof _scrollActiveTabIntoView=="function"&&_scrollActiveTabIntoView(),await renderParty2({cid:e,fromCache:!0,focusSection:"p2-sec-quests"}),!_campaignStale(e)&&(t&&await renderQuests2({cid:e,focusId:t}),typeof _presSetTab=="function"&&_presSetTab("quests"))}let _p2QuestsObs=null,_p2PresTimer=0;const _P2_NEWS_INPUTS={"p2-sec-quests:player":"visible quests that are complete or failed (id:status) + revealed step ids on the viewer's OWN quests","p2-sec-quests:gm":"ids of quests that FAILED (the one sync outcome a GM may want to reverse)","p2-sec-factions:gm":"faction.member:state + killed + missing (a member changed with nothing to confirm); never for a player","p2-sec-annals:all":"the close calls (near wipes) only, never the tally or the loot"};function _p2NewsKey(e){const t=typeof currentUser<"u"&&currentUser&&currentUser.id||0;return`tf_p2seen_${e}_${t}_${_isGm()?"gm":"pl"}`}function _p2NewsRead(e){try{return JSON.parse(localStorage.getItem(_p2NewsKey(e))||"{}")||{}}catch{return null}}function _p2NewsWrite(e,t){try{return localStorage.setItem(_p2NewsKey(e),JSON.stringify(t)),!0}catch{return!1}}function _p2NewsSigs(e){const t=_isGm(),n={};if(Array.isArray(allQuests)&&_q2LoadedCid===e){const s=allQuests.filter(_q2QuestVisibleToViewer);n["p2-sec-quests"]=t?s.filter(i=>i.status==="failed").map(i=>i.id).sort((i,o)=>i-o).join(","):s.filter(i=>i.status==="complete"||i.status==="failed").map(i=>`${i.id}:${i.status}`).concat(s.filter(i=>i.pc_entity_id&&_q2ViewerOwnsPc(i.pc_entity_id)).flatMap(i=>_q2VisibleLeads(i).filter(o=>o.revealed_at).map(o=>`L${i.id}.${o.id}`))).sort().join(",")}t&&Array.isArray(_p2RnData)&&_p2RnData.length&&(n["p2-sec-factions"]=_p2RnData.flatMap(s=>(s.members||[]).map(i=>`${s.entity_id}.${i.entity_id}:${i.state||""}${i.killed?"k":""}${i.missing?"m":""}`)).sort().join(","));const a=_party2Spec&&_party2LoadedCid===e?_party2Spec:null;return a&&a.annals&&(n["p2-sec-annals"]=(a.annals.near_wipes||[]).map(s=>String(s&&s.name||"")).sort().join("|")),n}function _p2NewsPaint(e,t){const n=e&&e.querySelector("#p2-rail");if(!n||!t)return;const a=_p2NewsRead(t);if(!a)return;const s=_p2NewsSigs(t);let i=!1;
3261Object.keys(s).forEach(o=>{a[o]===void 0&&(a[o]=s[o],i=!0);const r=n.querySelector(`.manage-rail-chip[data-rail="${o}"]`);if(!r)return;const c=a[o]!==s[o]&&!r.dataset.tone&&_p2RailState.active!==o;let l=r.querySelector(".manage-rail-news");c&&!l?(l=document.createElement("span"),l.className="manage-rail-news",l.setAttribute("aria-hidden","true"),r.appendChild(l)):!c&&l&&l.remove();const d=r.dataset.label||(r.dataset.label=r.textContent.trim());c?r.setAttribute("aria-label",`${d}, changed since you last looked`):r.dataset.tone||r.removeAttribute("aria-label")}),i&&_p2NewsWrite(t,a)}function _p2NewsSeen(e,t,n){const a=_p2NewsRead(t),s=_p2NewsSigs(t);!a||s[n]===void 0||a[n]===s[n]||(a[n]=s[n],_p2NewsWrite(t,a),_p2NewsPaint(e,t))}async function _p2QuestsPrefetch(e,t){if(_q2LoadedCid===t){_p2NewsPaint(e,t);return}try{const n=await api("GET",`/api/campaigns/${t}/quests`);if(_campaignStale(t))return;_q2LoadedCid!==t&&(allQuests=n,_q2LoadedCid=t),e.isConnected&&_p2NewsPaint(e,t)}catch{}}function _p2RailCfg(e){const t=e&&e.querySelector(".party2-stage"),n=e&&e.querySelector("#p2-rail"),a=()=>{clearTimeout(_p2PresTimer),_p2PresTimer=setTimeout(()=>{if(typeof _presSetTab!="function"||!e.isConnected||e.classList.contains("hidden"))return;const s=_p2RailState.pinned||_p2RailState.active,i=s==="p2-sec-quests"?"quests":"party";s&&currentCampaign&&_p2NewsSeen(e,currentCampaign.id,s),!(typeof _presCurrentTab<"u"&&_presCurrentTab===i)&&_presSetTab(i)},1500)};return{rail:n,panel:t,scroller:t,state:_p2RailState,sections:()=>t?[...t.querySelectorAll(".p2-sec[id]")]:[],bandTop:()=>n.getBoundingClientRect().bottom,scrollOffset:()=>n.getBoundingClientRect().height,onSelect:s=>{s.id==="p2-sec-quests"&&_p2QuestsEnsure(e),_p2RailState.reported=s.id,a(s.id),currentCampaign&&_p2NewsSeen(e,currentCampaign.id,s.id);const i=currentCampaign&&currentCampaign.id,o=_p2SlugOf(s.id);i&&_tfStampCampaignHash(o&&o!=="assessment"?`#party/${i}/${o}`:`#party/${i}`)},onActive:a}}function _p2QuestsEnsure(e){const t=e.querySelector("#p2-quests-mount");!t||t.dataset.q2Asked==="1"||(t.dataset.q2Asked="1",_p2QuestsObs&&(_p2QuestsObs.disconnect(),_p2QuestsObs=null),renderQuests2({cid:currentCampaign&&currentCampaign.id}))}function _p2Reland(e){const t=_p2RailState.hold;if(!t||Date.now()>t.until||_p2RailState.pinned!==t.id||!e||!e.isConnected)return;const n=e.querySelector("#"+t.id);n&&_tfRailScrollTo(_p2RailCfg(e),n,"auto")}function _p2RailInit(e,t,n){_p2RailState={pinned:null,active:null,hold:n?{id:n,until:Date.now()+4e3}:null};const a=_p2RailCfg(e);if(!a.rail||!a.scroller)return;_tfRailInit(a),_tfRailMarkActive(a),_p2NewsPaint(e,t),_p2QuestsPrefetch(e,t),_p2QuestsObs&&(_p2QuestsObs.disconnect(),_p2QuestsObs=null);const s=e.querySelector("#p2-quests-mount");if(s&&(_q2LoadedCid===t||n==="p2-sec-quests"||typeof IntersectionObserver!="function"?_p2QuestsEnsure(e):(_p2QuestsObs=new IntersectionObserver(i=>{i.some(o=>o.isIntersecting)&&_p2QuestsEnsure(e)},{root:a.scroller,rootMargin:"800px 0px"}),_p2QuestsObs.observe(s))),n){const i=e.querySelector("#"+n);if(i&&_tfRailScrollTo(a,i,"auto"),i&&typeof ResizeObserver=="function"){const o=new ResizeObserver(()=>_p2Reland(e));a.scroller.querySelectorAll(".p2-sec").forEach(r=>o.observe(r)),setTimeout(()=>o.disconnect(),4200)}}}async function renderParty2(e){e=e||{};const t=_party2Panel(),n=e.cid||currentCampaign&&currentCampaign.id;if(!n)return;const a=t.querySelector(".party2-stage"),s=!!(a&&!t.classList.contains("hidden")&&_party2LoadedCid===n),i=s?t.querySelector('#p2-quests-mount[data-q2-ready="1"]'):null;if((e._noReval||e.background)&&i&&_q2EditorOpen(i))return;const o=s?a.scrollTop:null,r=!!(_party2Spec&&_party2LoadedCid===n&&e.fromCache);let c;if(r)c=_party2Spec;else{t.innerHTML='<div class="party2-stage"><p class="placeholder--padded">Loading the company\u2026</p></div>';
3261try{c=await api("GET",`/api/campaigns/${n}/party-render-spec`)}catch(T){t.innerHTML=`<div class="party2-stage"><p class="placeholder--padded error">Could not load the party. ${_p2Esc(T.message||"")}</p></div>`;return}try{c._wall=(await api("GET",`/api/campaigns/${n}/quotes/wall`)).quotes||[]}catch{c._wall=[]}c._facetMap={},(c.dice&&c.dice.d8||[]).concat(c.dice&&c.dice.d6||[]).forEach(T=>{T.reasoning=_p2StripCot(T.reasoning),c._facetMap[T.key]=T}),_party2Spec=c,_party2LoadedCid=n}c._srvCanAssess===void 0&&(c._srvCanAssess=!!c.can_assess),c.can_assess=c._srvCanAssess&&_isGm();const d=c.active||[],u=Math.ceil(d.length/2),p=d.slice(0,u),m=d.slice(u),f=[`<span><b>${c.counts.active}</b> active</span>`,c.counts.fallen?`<span class="dot"></span><span><b>${c.counts.fallen}</b> fallen</span>`:"",c.counts.departed?`<span class="dot"></span><span><b>${c.counts.departed}</b> departed</span>`:""].filter(Boolean).join(""),g=c.current_chapter?`Chapter ${_chr2Roman(c.current_chapter)}`:null,h=c.avg_level!=null?`Level ${c.avg_level}`:null,v=[g,h].filter(Boolean).join(", at "),b=v?`The company as it stands at ${v}, and an honest measure of what it can and cannot do.`:"An honest measure of what the company can and cannot do.",y='<div class="party2-page"><div class="index-inner">'+_p2OnboardBanner(c)+_p2Masthead({eyebrow:"Party",titleHtml:_p2Esc(c.party_name),titleId:"p2-party-name-el",editable:!0,leadHtml:b,statsHtml:f,actionsHtml:_isGm()?'<button type="button" class="btn-new" id="p2-add-pc-btn">+ Add PC</button><button type="button" class="btn-new" id="p2-import-pc-btn" title="Import one character or your whole party, from Roll20, D&amp;D Beyond, a Foundry VTT export, or a sheet PDF/photo">Import PC(s)</button>':""})+_p2Assessment(c)+`<div class="p2-frame" style="--p2-flank-rows:${u}"><aside class="p2-flank">${p.map(T=>_p2FlankCard(T,"is-left")).join("")}</aside><div class="p2-core">`+_p2DiceSection(c)+`</div><aside class="p2-flank">${m.map(T=>_p2FlankCard(T,"is-right")).join("")}</aside></div>`+_p2SoftSpots(c)+_p2StandsLine(c)+_p2Colophon("Party Assessment")+"</div></div>",_='<div class="party2-page"><div class="index-inner">'+_p2Masthead({eyebrow:"Party",titleHtml:"The Annals",leadHtml:"The deeds, the spoils, and the hours the dark reached for the company, and missed."})+_p2Annals(c)+_p2Colophon("The Annals")+"</div></div>",w=_p2Departed(c),$=_p2Memoriam(c),E=[["p2-sec-assessment","Assessment"],["p2-sec-quests","Quests"],["p2-sec-factions","Standing"],["p2-sec-annals","Annals"]];w&&E.push(["p2-sec-departed","Departed"]),$&&E.push(["p2-sec-memoriam","In Memoriam"]);const k='<nav class="manage-rail p2-rail" id="p2-rail" aria-label="Party sections">'+E.map(([T,P])=>`<button type="button" class="manage-rail-chip" data-rail="${T}">${P}</button>`).join("")+"</nav>",C=(T,P)=>P?`<section class="p2-sec" id="${T}">${P}</section>`:"";if(t.innerHTML='<div class="party2-stage">'+k+C("p2-sec-assessment",y)+C("p2-sec-quests",'<div id="p2-quests-mount"><div class="party2-page"><div class="index-inner"><p class="placeholder--padded">Loading the ledger\u2026</p></div></div></div>')+C("p2-sec-factions",'<div id="p2-renown-mount"></div>')+C("p2-sec-annals",_)+C("p2-sec-departed",w)+C("p2-sec-memoriam",$)+"</div>",i&&i.dataset.cid===String(n)&&i.dataset.viewer===(_isGm()?"gm":"player")){const T=t.querySelector("#p2-quests-mount");T&&T.replaceWith(i)}if(_p2RenownLoad(),_p2RailInit(t,n,e.focusSection),o!=null&&!e.focusSection){const T=t.querySelector(".party2-stage");T&&(T.scrollTop=o,_tfRailMarkActive(_p2RailCfg(t)))}if(t.querySelector("[data-p2-edit-name]")?.addEventListener("click",_p2EditPartyName),t.querySelector(".p2-empty-newpc")?.addEventListener("click",()=>openEntityModal(null,null,"pc")),t.querySelector("#p2-add-pc-btn")?.addEventListener("click",()=>openEntityModal(null,null,"pc")),t.querySelector("#p2-import-pc-btn")?.addEventListener("click",()=>_openImportReview(null)),(c.counts&&c.counts.active||0)===0){const T=t.querySelector("#p2-import-pc-btn");T&&(T.classList.add("btn-new--attn"),T.addEventListener("animationend",()=>T.classList.remove("btn-new--attn"),{once:!0}))}t.querySelector(".p2-empty-import")?.addEventListener("click",()=>_openImportReview(null)),t.querySelectorAll("[data-p2-reassess]").forEach(T=>T.addEventListener("click",_p2Reassess)),t.querySelectorAll("[data-p2-view]").forEach(T=>T.addEventListener("click",()=>{const P=T.dataset.p2View;_p2CenterView=P;const B=T.closest(".p2-dice-sec");B&&(B.querySelectorAll("[data-p2-view]").forEach(H=>{const A=H.dataset.p2View===P;H.classList.toggle("is-active",A),H.setAttribute("aria-selected",String(A))}),B.querySelector(".p2-view-dice")?.classList.toggle("hidden",P!=="dice"),B.querySelector(".p2-view-swot")?.classList.toggle("hidden",P!=="swot"))}));const S=t.querySelector(".dhp-thr-mount");if(S&&typeof _liveRenderThresholdBoard=="function")try{_liveRenderThresholdBoard(S,{bare:!0,seedDmg:!0})}catch{}t.querySelectorAll(".p2-hit").forEach(T=>{const P=T.dataset.facet,B=t.querySelector(".p2-wedge[data-facet]");T.addEventListener("mouseenter",()=>_p2ShowFacet(P)),T.addEventListener("focus",()=>_p2ShowFacet(P)),T.addEventListener("click",()=>_p2ShowFacet(P)),T.addEventListener("keydown",H=>
3261{(H.key==="Enter"||H.key===" ")&&(H.preventDefault(),_p2ShowFacet(P))})}),t.querySelector("[data-p2-honors-edit]")?.addEventListener("click",T=>{const P=T.target.closest("[data-p2-honors]");P&&_party2Spec&&_p2OpenHonorsEditor(P,_party2Spec)}),_p2LoadHonorNoms(t),t.querySelectorAll("[data-p2-reck-edit]").forEach(T=>T.addEventListener("click",P=>{P.preventDefault();const B=parseInt(T.dataset.p2ReckEdit);B&&typeof openCx2ImageModal=="function"&&openCx2ImageModal(B,()=>renderParty2())})),t.querySelectorAll("[data-p2-camp-pc]").forEach(T=>T.addEventListener("click",P=>{P.preventDefault();const B=parseInt(T.dataset.p2CampPc);B&&typeof navigateToEntity=="function"&&navigateToEntity(B)})),t.querySelectorAll("[data-p2-import-pc]").forEach(T=>T.addEventListener("click",P=>{P.stopPropagation(),P.preventDefault();const B=parseInt(T.dataset.p2ImportPc);B&&typeof _pcSheetControlClick=="function"&&_pcSheetControlClick(B)})),t.querySelectorAll("[data-p2-edit-pc]").forEach(T=>T.addEventListener("click",P=>{P.stopPropagation(),P.preventDefault();const B=parseInt(T.dataset.p2EditPc);B&&typeof _openEditStatsModal=="function"&&_openEditStatsModal(B)})),t.querySelectorAll("[data-p2-claim-pc]").forEach(T=>T.addEventListener("click",async P=>{P.stopPropagation(),P.preventDefault();const B=parseInt(T.dataset.p2ClaimPc),H=currentCampaign&&currentCampaign.id;if(!(!B||!H)){T.disabled=!0;try{await api("POST",`/api/campaigns/${H}/entities/${B}/claim`),_afterPcClaimed(B),typeof showToast=="function"&&showToast("\u2713 Claimed: you can now keep this sheet current",4e3),renderParty2()}catch(A){T.disabled=!1,typeof showToast=="function"&&showToast("Claim failed: "+(A.message||A),4e3)}}})),t.querySelectorAll(".p2-rk-item--toggle").forEach(T=>{const P=()=>{const B=T.classList.toggle("is-open");T.setAttribute("aria-expanded",B?"true":"false")};T.addEventListener("click",P),T.addEventListener("keydown",B=>{(B.key==="Enter"||B.key===" ")&&(B.preventDefault(),P())})});const q=async(T,P)=>{if(_party2Spec)try{await api("PUT",`/api/campaigns/${_party2Spec.campaign_id}/quotes/${T}/wall`,P),await renderParty2({cid:_party2Spec.campaign_id})}catch(B){typeof showToast=="function"&&showToast("Failed: "+(B.message||B),3e3)}};t.querySelectorAll("[data-wall-feature]").forEach(T=>T.addEventListener("click",P=>q(P.currentTarget.dataset.wallFeature,{featured:P.currentTarget.dataset.on!=="1"}))),t.querySelectorAll("[data-wall-hide]").forEach(T=>T.addEventListener("click",P=>q(P.currentTarget.dataset.wallHide,{hidden:!0}))),t.querySelectorAll("[data-p2-memnote-edit]").forEach(T=>T.addEventListener("click",P=>{const B=P.target.closest("[data-p2-memnote]");if(!B||!_party2Spec)return;const H=parseInt(B.dataset.pcId),x=[].concat(_party2Spec.fallen||[],_party2Spec.active||[],_party2Spec.departed||[]).find(M=>M.id===H);x&&_p2OpenMemNoteEditor(B,x,B.dataset.memnoteKind||"lastwords")})),t.querySelector("[data-p2-resume]")?.addEventListener("click",T=>{T.preventDefault();const P=T.currentTarget.getAttribute("data-p2-resume");P&&_party2Spec&&(location.hash=`#chronicle/${_party2Spec.campaign_id}/${P}`)}),_p2StartQuoteCycle(t);const I=t.querySelector("#p2-facet-readout");I&&(I.setAttribute("tabindex","0"),I.addEventListener("click",()=>{I.dataset.facet&&_p2OpenFacetModal(I.dataset.facet)}),I.addEventListener("keydown",T=>{(T.key==="Enter"||T.key===" ")&&I.dataset.facet&&(T.preventDefault(),_p2OpenFacetModal(I.dataset.facet))})),r&&!e._noReval&&(async()=>{try{const T=await api("GET",`/api/campaigns/${n}/party-render-spec`);try{T._wall=(await api("GET",`/api/campaigns/${n}/quotes/wall`)).quotes||[]}catch{T._wall=[]}if(T._facetMap={},(T.dice&&T.dice.d8||[]).concat(T.dice&&T.dice.d6||[]).forEach(B=>{B.reasoning=_p2StripCot(B.reasoning),T._facetMap[B.key]=B}),!currentCampaign||currentCampaign.id!==n)return;_party2Spec=T,_party2LoadedCid=n;const P=_party2Panel();P&&!P.classList.contains("hidden")&&renderParty2({cid:n,fromCache:!0,_noReval:!0})}catch{}})()}async function _partyHandleHash(e){const n=(typeof e=="string"?e:location.hash||"").match(/^#party\/(\d+)(?:\/([a-z]+)(?:\/(\d+))?)?$/);if(!n)return!1;const a=+n[1],s=n[2]||null,i=n[3]?+n[3]:null;if(typeof getStoredToken!="function"||!getStoredToken())return!1;const o=++_tfNavSeq;_tfPendingLanding="party";try{if((!currentCampaign||currentCampaign.id!==a)&&await bootWithCampaignId(a),_tfN
3261avStale(o))return!0;if(s==="quests")return await _partyOpenQuests(a,i),!0;const r=_party2Panel(),c=s?_P2_SECTION_SLUGS[s]:null;return _party2Show(r,c?`#party/${a}/${s}`:`#party/${a}`),await renderParty2({cid:a,fromCache:!0,focusSection:c||void 0}),!0}catch(r){return console.warn("[party2] deep-link failed",r),!1}finally{_tfPendingLanding=null}}window.addEventListener("hashchange",_partyHandleHash),window.openParty2=function(e){const t=e||currentCampaign&&currentCampaign.id;t&&(location.hash=`#party/${t}`)};let _q2LoadedCid=null,_q2OpenId=null,_q2OpenCid=null,_q2JustOpened=null;const _q2HiddenTick=new Set;function _questsSurfaceVisible(){const e=_q2Host();return!!e&&e.el.dataset.q2Ready==="1"}function _q2Host(){const e=document.getElementById("tab-party2"),t=e&&!e.classList.contains("hidden")&&e.offsetParent!==null?e.querySelector("#p2-quests-mount"):null;return t?{el:t,embedded:!0}:null}function _q2Stage(e){return e.querySelector(".party2-stage")||e.closest(".party2-stage")}function _q2EditorOpen(e){return e.querySelector(".q2-editing, .chr2-field-editor")?!0:[...e.querySelectorAll(".q2-trail-input")].some(t=>(t.value||"").trim())}function _q2Leads(e){let t=Array.isArray(e.leads)?e.leads:[];if(typeof e.leads=="string")try{t=JSON.parse(e.leads)||[]}catch{t=[]}return t.filter(n=>n&&typeof n=="object")}function _q2LeadVis(e){const t=e&&e.visibility||"all";return t==="gm"||t==="pc"?t:"all"}function _q2ViewerOwnsPc(e){const t=e?(allEntities||[]).find(n=>n.id===e):null;return!!(t&&t.owned_by_me)}function _q2LeadVisibleToViewer(e,t){if(_isGm())return!0;const n=_q2LeadVis(t);return n==="all"||n==="pc"&&_q2ViewerOwnsPc(e.pc_entity_id)}function _q2QuestVisibleToViewer(e){return _isGm()||!e.pending_reveal&&(!e.is_private||_q2ViewerOwnsPc(e.pc_entity_id))}function _q2BoxKind(e){return e.pending_reveal?_isGm()?"unrevealed":null:e.is_private?"private":null}function _q2BoxWrap(e,t,n){const a=_q2BoxKind(e);if(!a)return t;const s=e.pc_entity_id?(allEntities||[]).find(l=>l.id===e.pc_entity_id):null,i=s?s.name:"Party",o=a==="unrevealed",r=_isGm()?o?`<button class="gm-reveal q2-quest-reveal" type="button" data-id="${e.id}" title="Reveal this quest to ${s?escHtml(s.name):"the party"}">${_CX2_ACT_ICO.eyeRays}<span>Reveal</span></button>`:`<button class="gm-reveal q2-quest-public" type="button" data-id="${e.id}" title="Make this quest public: the whole table sees it as ${s?escHtml(s.name)+"\u2019s":"a"} quest">${_CX2_ACT_ICO.eyeRays}<span>Make public</span></button>`:"",c=o?`<p class="gm-surface-lead">Only you can see this quest. Reveal it when the table should know it exists${s?`; ${escHtml(s.name)}\u2019s player is told`:""}.</p>`:"";return`<div class="gm-surface${o?"":" gm-surface--staged"} q2-box q2-box--${a}${n?" q2-box--row":""}" data-id="${e.id}">
3262    <div class="gm-surface-head">
3263      <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.eyeClosed}</span>
3264      <span class="gm-surface-label">${o?"Unrevealed":"Private"}</span>
3265      ${r}
3266      <span class="gm-surface-seal" title="${o?s?`Nobody but you sees this quest yet; when revealed it is ${escHtml(s.name)}\u2019s`:"Nobody but you sees this quest yet":`Only ${escHtml(i)}\u2019s player and the GM can see this quest`}">${escHtml(i)}</span>
3267    </div>${n?"":c}
3268    ${t}
3269  </div>`}function _q2VisibleLeads(e){return _q2Leads(e).filter(t=>_q2LeadVisibleToViewer(e,t))}const _Q2_NEW_MS=7*864e5;function _q2LeadIsNew(e){return!_isGm()&&!!(e&&e.revealed_at)&&Date.now()-new Date(e.revealed_at).getTime()<_Q2_NEW_MS}function _q2SessById(e){return(allSessions||[]).find(t=>t.id===e)||null}function _q2ChapLabel(e,t){const n=getSessionNum(e),a=e.is_session_zero||n===0?"Prologue":`Ch. ${_chr2Roman(n)}`;if(!t)return a;const s=(e.title||"").trim();return s?`${a}: ${s}`:a}function _q2ChLink(e,t){return`<a class="q2-ch-link" href="#chronicle/${currentCampaign&&currentCampaign.id}/${e.id}">${escHtml(_q2ChapLabel(e,t))}</a>`}function _q2KickerHtml(e,t){const n=!!(t&&t.compact),a=e.introduced_session_id?_q2SessById(e.introduced_session_id):null,s=e.resolved_session_id?_q2SessById(e.resolved_session_id):null,i=n?(l,d)=>escHtml(_q2ChapLabel(l,d)):_q2ChLink,o=!n&&e.is_background&&e.pc_entity_id&&!_q2BoxKind(e)?(allEntities||[]).find(l=>l.id===e.pc_entity_id):null,r=o?`<span class="q2-sworn-by"><span class="entity-link" data-id="${o.id}" data-type="pc" tabindex="0">${escHtml(o.name)}</span></span>`:"",c=[];if(e.status==="complete"||e.status==="failed")c.push(e.status==="complete"?"Fulfilled":"Failed"),r&&c.push(r),a&&s?c.push(`${i(a,!1)} \u2013 ${i(s,!1)}`):s?c.push(i(s,!1)):a&&c.push(`taken up ${i(a,!1)}`);else{if(!a&&!r)return"";if(r&&c.push(r),c.push("Taken up"),a){c.push(i(a,!n));const l=getSessionNum(a),u=(allSessions||[]).reduce((p,m)=>Math.max(p,+getSessionNum(m)||0),0)-(+l||0);if(u>0){const p=`${u} chapter${u===1?"":"s"} underway`;n?c[c.length-1]+=`<span class="q2-ref-underway"><span class="q2-kdot">\xB7</span>${p}</span>`:c.push(p)}}}return c.join('<span class="q2-kdot">\xB7</span>')}function _q2SecStore(e){try{return JSON.parse(localStorage.getItem(`tf_q2_secs_${e}`))||{}}catch{return{}}}function _q2SecIsOpen(e,t){return!!_q2SecStore(e)[t]}function _q2SecSet(e,t,n){const a=_q2SecStore(e);a[t]=!!n;try{localStorage.setItem(`tf_q2_secs_${e}`,JSON.stringify(a))}catch{}}function _q2TrailRow(e,t){const n=t.status||"open",a=n==="open",s=n==="dead_end",i=escHtml(t.id||""),o=s?"\u2298":a?"\u25B9":"\u2713",r=a?"Mark this lead run down":"Reopen this lead",c=_q2LeadVis(t),l=c==="gm",d=!_isGm()&&c!=="all",u=c!=="all"&&e.pc_entity_id?(allEntities||[]).find(h=>h.id===e.pc_entity_id):null,p="q2-trail"+(s?" q2-trail--dead":a?"":" q2-trail--resolved")+(l?" q2-trail--hidden":""),m=u?escHtml(u.name):"the party",f=l?"":c==="pc"&&_isGm()&&u&&!e.is_private?`<span class="q2-row-flag q2-trail-flag" title="Revealed to ${m}\u2019s player only">${m}</span>`:_q2LeadIsNew(t)?'<span class="session-new-recap-badge toc-new-recap" title="Your GM revealed this step">\u25CF New</span>':"",g=l?`<button class="q2-trail-reveal" data-qid="${e.id}" data-lead-id="${i}" title="Reveal this step to ${m}" aria-label="Reveal this step to ${m}">${_CX2_ACT_ICO.eyeRays}Reveal</button>`:_isGm()&&c==="all"&&a&&!t.revealed_at&&(e.pc_entity_id||e.is_private)?`<button class="q2-trail-hide" data-qid="${e.id}" data-lead-id="${i}" title="Hide this step until you reveal it" aria-label="Hide this step until you reveal it">${_CX2_ACT_ICO.eyeClosed}Hide</button>`:"";return d?`<div class="${p} q2-trail--locked" data-lead-id="${i}" title="Your GM added this step">
3270      <span class="q2-trail-mark q2-trail-mark--static" aria-hidden="true">${o}</span>
3271      <span class="q2-trail-text">${linkifyText(escHtml(t.text||""),allEntities,null)}</span>${f}
3272    </div>`:`<div class="${p}" data-lead-id="${i}">
3273    <button class="q2-trail-mark" data-qid="${e.id}" data-lead-id="${i}" title="${r}" aria-label="${r}">${o}</button>
3274    <span class="q2-trail-text">${linkifyText(escHtml(t.text||""),allEntities,null)}</span>${f}
3275    <span class="q2-trail-acts">
3276      ${g}
3277      ${a?`<button class="q2-trail-check q2-trail-check--done" data-qid="${e.id}" data-lead-id="${i}" title="Mark run down" aria-label="Mark run down">\u2713</button>
3278           <button class="q2-trail-dead" data-qid="${e.id}" data-lead-id="${i}" title="Call it a dead end" aria-label="Call it a dead end">\u2298</button>`:`<button class="q2-trail-check" data-qid="${e.id}" data-lead-id="${i}" title="Reopen this lead" aria-label="Reopen this lead">\u21A9</button>`}
3279      <button class="q2-trail-edit" data-qid="${e.id}" data-lead-id="${i}" title="Edit lead" aria-label="Edit lead">\u270E</button>
3280      <button class="q2-trail-del" data-qid="${e.id}" data-lead-id="${i}" title="Delete lead" aria-label="Delete lead">\u2715</button>
3281    </span>
3282  </div>`}function _q2MarkFor(e){const t=e.status==="complete"||e.status==="failed";return{color:t?e.status==="complete"?"var(--moss)":"var(--ember-deep)":e.is_background?"var(--violet)":"var(--teal-3)",glyph:t?e.status==="complete"?"\u2713":"\u2715":"\u2756"}}function _q2RowHtml(e){const t=e.status==="complete"||e.status==="failed",{color:n,glyph:a}=_q2MarkFor(e),i=!!!_q2BoxKind(e)&&e.is_background&&e.pc_entity_id?(allEntities||[]).find(h=>h.id===e.pc_entity_id):null,o=i?`<span class="q2-row-flag" title="${escHtml(i.name)}\u2019s personal quest">${escHtml(i.name)}</span>`:"",r=_isGm()&&!t?_q2Leads(e).filter(h=>_q2LeadVis(h)==="gm").length:0,c=r?`<span class="q2-row-unrev" title="${r} step${r===1?"":"s"} only you can see">${r} unrevealed</span>`:"",l=t?[]:_q2VisibleLeads(e).filter(h=>(h.status||"open")==="open"),d=l.length?`<span class="q2-row-trails">${l.length} lead${l.length===1?"":"s"} open</span>`:"",u=e.faction_name?`<span class="q2-row-faction" title="Faction quest \xB7 stakeholder: ${escHtml(e.faction_name)}">${escHtml(e.faction_name)}</span>`:"",p=_isGm()&&(e.gm_notes||"").trim()?'<span class="q2-row-note" title="Has GM notes" aria-hidden="true">\u203B</span>':"";let m=_q2KickerHtml(e,{compact:!0});c&&(m=m?`${m}<span class="q2-kdot">\xB7</span>${c}`:c);const f=!t&&l.length&&(l[0].text||"").trim()?`<span class="q2-row-teaser">\u25B9 ${escHtml(l[0].text)}</span>`:"",g=`<button class="q2-row" id="q2-q-${e.id}" data-id="${e.id}" style="--q2-mark:${n}">
3283    <span class="q2-row-mark" aria-hidden="true">${a}</span>
3284    <span class="q2-row-main">
3285      <span class="q2-row-title">${escHtml(e.title)}</span>${o}${u}${d}${p}
3286      ${m?'<span class="q2-row-lead"></span>':""}
3287    </span>
3288    ${m?`<span class="q2-row-ch">${m}</span>`:""}
3289    ${f}
3290  </button>`;return _q2BoxWrap(e,g,!0)}function _q2SectionBarHtml(e){const t=`<span class="q2-sec-name">${e.name}</span>${e.glyph?`<span class="q2-sec-mark" aria-hidden="true" style="--q2-mark:${e.color}">${e.glyph}</span>`:""}<span class="q2-sec-ro">${e.ro}</span>${e.chev?`<span class="q2-sec-chev" aria-hidden="true">${e.open?"\u25BE":"\u25B8"}</span>`:""}`+(e.ctaHtml||"");return e.chev?`<button class="q2-section-bar q2-section-bar--toggle" data-sec="${e.key}" aria-expanded="${e.open?"true":"false"}">${t}</button>`:`<div class="q2-section-bar">${t}</div>`}function _q2EntryHtml(e){const t=_isGm(),n=e.status==="complete"||e.status==="failed",{color:a,glyph:s}=_q2MarkFor(e);let i="";const o=(e.description||"").split(`
3291`).map($=>$.replace(/^[-*•◆]\s*/,"").trim()).filter(Boolean);o.length?i=`<div class="q2-objs">
3292      <div class="q2-sub-kicker">Objectives<button class="chr2-edit-pen q2-objs-pen" data-id="${e.id}" title="Edit objectives" aria-label="Edit objectives">\u270E</button></div>
3293      ${o.map($=>`<div class="q2-obj"><span class="q2-diamond" aria-hidden="true">\u25C6</span><span class="q2-obj-text">${linkifyText(escHtml($),allEntities,null)}</span></div>`).join("")}
3294    </div>`:n||(i=`<div class="q2-objs">
3294<button class="btn-ghost btn-xs q2-objs-pen" data-id="${e.id}">+ Add objectives</button></div>`);const r=_q2VisibleLeads(e),c=t?r.filter($=>_q2LeadVis($)==="gm"):[],l=r.filter($=>_q2LeadVis($)!=="gm"),d=l.filter($=>($.status||"open")==="open"),u=[...l.filter($=>$.status==="resolved"),...l.filter($=>$.status==="dead_end")],p=l.filter($=>$.status==="resolved").length,m=l.filter($=>$.status==="dead_end").length,f=[p?`${p} lead${p===1?"":"s"} run down`:"",m?`${m} dead end${m===1?"":"s"}`:""].filter(Boolean).join(" \xB7 "),g=u.length?u.length>5?`<details class="q2-trails-resolved" data-qid="${e.id}"><summary class="q2-sub-kicker q2-trails-summary">${f}</summary>${u.map($=>_q2TrailRow(e,$)).join("")}</details>`:u.map($=>_q2TrailRow(e,$)).join(""):"",h=e.pc_entity_id?((allEntities||[]).find($=>$.id===e.pc_entity_id)||{}).name:null,v=c.length?`<div class="gm-surface q2-unrev-box" role="group" aria-label="Unrevealed steps">
3295      <div class="gm-surface-head">
3296        <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.eyeClosed}</span>
3297        <span class="gm-surface-label">Unrevealed</span>
3298        <span class="gm-surface-seal" title="Only you can see these steps; reveal each when ${h?escHtml(h):"the party"} reaches it">GM&nbsp;only</span>
3299      </div>
3300      ${c.map($=>_q2TrailRow(e,$)).join("")}
3301    </div>`:"",b=`<div class="q2-trails">
3302    ${l.length||c.length||!n?`<div class="q2-sub-kicker">Leads${d.length?` \xB7 ${d.length} open`:""}</div>`:""}
3303    ${d.map($=>_q2TrailRow(e,$)).join("")}
3304    ${g}
3305    ${v}
3306    ${n?"":`<div class="q2-trail-add">
3307      <input type="text" class="q2-trail-input" data-id="${e.id}" placeholder="Add a lead, the next thing to chase down\u2026" />
3308      <button class="btn-ghost btn-xs q2-trail-add-btn" data-id="${e.id}">+ Add</button>
3309      ${t?`<label class="checkbox-label gm-portal--gold q2-trail-hidden-opt" title="Only you see this step until you reveal it${e.pc_entity_id?"":" to the party"}. Stays ticked while you plan."><input type="checkbox" class="q2-trail-hidden" data-id="${e.id}"${_q2HiddenTick.has(e.id)?" checked":""} /><span class="gm-portal"><span class="gm-portal-ico" aria-hidden="true">${_CX2_ACT_ICO.eyeClosed}</span><span class="gm-portal-txt">Unrevealed step</span></span></label>`:""}
3310    </div>`}
3311  </div>`,y=t?(e.gm_notes||"").trim()?`<div class="q2-gmnotes gm-surface">
3312        <div class="gm-surface-head">
3313          <span class="gm-surface-ico" aria-hidden="true">${_CX2_ACT_ICO.screen}</span>
3314          <span class="gm-surface-label">Behind the Screen</span>
3315          <button class="chr2-edit-pen q2-gmnotes-pen" data-id="${e.id}" title="Edit GM notes" aria-label="Edit GM notes">\u270E</button>
3316          <span class="gm-surface-seal">GM&nbsp;only</span>
3317        </div>
3318        <div class="q2-gmnotes-text gm-surface-body">${linkifyText(escHtml(e.gm_notes),allEntities,null)}</div>
3319      </div>`:`<div class="q2-gmnotes q2-gmnotes--empty"><button class="btn-ghost btn-xs q2-gmnotes-pen" data-id="${e.id}">+ GM note</button></div>`:"",_=e.status==="active"?`<button class="btn-ghost btn-xs q2-status-btn" data-id="${e.id}" data-next="complete">\u2726 Mark Fulfilled</button>
3320       <button class="btn-ghost btn-xs btn-ghost--danger q2-status-btn" data-id="${e.id}" data-next="failed">Mark Failed</button>`:`<button class="btn-ghost btn-xs q2-status-btn" data-id="${e.id}" data-next="active">\u21A9 Reopen</button>`,w=`<article class="q2-entry${n?" q2-entry--closed":""}${_q2JustOpened===e.id?" tf-entering":""}" id="q2-q-${e.id}" tabindex="-1" style="--q2-mark:${a}">
3321    <div class="q2-head">
3322      <span class="q2-mark" aria-hidden="true">${s}</span>
3323      <div class="chr2-field-row q2-title-row">
3324        <span class="q2-title">${escHtml(e.title)}</span>
3325        <button class="chr2-edit-pen q2-title-pen" data-id="${e.id}" title="Edit title" aria-label="Edit quest title">\u270E</button>
3326      </div>
3327      <div class="quest-detail-meta-actions q2-meta">
3328        <button class="btn-ghost btn-xs q2-collapse-btn" data-id="${e.id}">\u25BE Collapse</button>
3329        <button class="btn-ghost btn-xs q2-edit-btn" data-id="${e.
3329id}">\u270E Edit</button>
3330        ${t?`<button class="btn-ghost btn-xs btn-ghost--danger q2-del-btn" data-id="${e.id}">Delete</button>`:""}
3331      </div>
3332    </div>
3333    <div class="q2-kicker">${_q2KickerHtml(e)}</div>
3334    <div class="q2-body">
3335      ${i}
3336      ${b}
3337      ${y}
3338      <div class="q2-status-actions">${_}</div>
3339    </div>
3340  </article>`;return _q2BoxWrap(e,w,!1)}function _q2Masthead(e,t){const n=currentCampaign||{},a=_isGm(),s=(n.quests_title||"").trim()||"The Quest Log",i=(n.quests_lead||"").trim()||`Every charge ${_partyLabel()} has taken up: held, fulfilled, failed.`,o=a?'<button class="chr2-edit-pen" data-q2-masthead="quests_title" title="Edit ledger title" aria-label="Edit ledger title">\u270E</button>':"",r=a?'<button class="chr2-edit-pen" data-q2-masthead="quests_lead" title="Edit ledger lead" aria-label="Edit ledger lead">\u270E</button>':"",c=[];return e.active&&c.push(`<b>${e.active}</b> active`),e.fulfilled&&c.push(`<b>${e.fulfilled}</b> fulfilled`),e.failed&&c.push(`<b>${e.failed}</b> failed`),t?_p2Masthead({eyebrow:"Party",titleHtml:escHtml(s),titleId:"q2-title-el",titlePenHtml:o,leadHtml:escHtml(i),leadId:"q2-lead-el",leadPenHtml:r,statsHtml:c.map(l=>`<span>${l}</span>`).join('<span class="dot"></span>')}):_tf2SectionPlate({eyebrow:"Quests",titleHtml:escHtml(s),titleId:"q2-title-el",titlePen:o,stats:c,leadHtml:escHtml(i),leadId:"q2-lead-el",leadPen:r})}function _q2CaptureState(e){const t={scrollTop:0,drafts:{},focusAdd:null,editor:null,details:[]},n=_q2Stage(e);t.scrollTop=n?n.scrollTop:0,e.querySelectorAll(".q2-trail-input").forEach(s=>{(s.value||"").trim()&&(t.drafts[s.dataset.id]=s.value),document.activeElement===s&&(t.focusAdd=s.dataset.id)});const a=e.querySelector('.q2-inline-editor:not([data-q2-closing="1"])');if(a&&a.dataset.q2Kind){const s=a.querySelector("input, textarea");t.editor={kind:a.dataset.q2Kind,qid:a.dataset.q2Qid||"",leadId:a.dataset.q2Lead||"",value:s?s.value:""}}return e.querySelectorAll(".q2-trails-resolved[open]").forEach(s=>{s.dataset.qid&&t.details.push(s.dataset.qid)}),t}function _q2RestoreState(e,t){t.details.forEach(a=>{const s=e.querySelector(`.q2-trails-resolved[data-qid="${a}"]`);s&&(s.open=!0)}),Object.entries(t.drafts).forEach(([a,s])=>{const i=e.querySelector(`.q2-trail-input[data-id="${a}"]`);i&&(i.value=s)});const n=_q2Stage(e);if(n&&(n.scrollTop=t.scrollTop),t.focusAdd){const a=e.querySelector(`.q2-trail-input[data-id="${t.focusAdd}"]`);if(a){a.focus({preventScroll:!0});try{a.setSelectionRange(a.value.length,a.value.length)}catch{}}}if(t.editor){const{kind:a,qid:s,leadId:i,value:o}=t.editor;let r=null;if(a==="masthead"?r=e.querySelector(`[data-q2-masthead="${s}"]`):a==="title"?r=e.querySelector(`.q2-title-pen[data-id="${s}"]`):a==="objs"?r=e.querySelector(`.q2-objs-pen[data-id="${s}"]`):a==="gmnotes"?r=e.querySelector(`.q2-gmnotes-pen[data-id="${s}"]`):a==="lead"&&(r=e.querySelector(`.q2-trail-edit[data-qid="${s}"][data-lead-id="${i}"]`)),r){r.click();const c=e.querySelector(".q2-inline-editor"),l=c&&c.querySelector("input, textarea");if(l){l.value=o,l.focus({preventScroll:!0});try{l.setSelectionRange(l.value.length,l.value.length)}catch{}}}}}async function renderQuests2(e){const t=_q2Host();if(!t)return;const{el:n,embedded:a}=t,s=e&&e.cid||currentCampaign&&currentCampaign.id,i=A=>`<div class="party2-page"><div class="index-inner">${A}</div></div>`,o=A=>a?i(A):`<div class="party2-stage">${i(A)}</div>`;if(!s){n.innerHTML=o('<p class="placeholder--padded">Loading the ledger\u2026</p>');return}if(_q2OpenCid!==s&&(_q2OpenId=null,_q2JustOpened=null,_q2OpenCid=s),_q2LoadedCid!==s){n.innerHTML=o('<p class="placeholder--padded">Loading the ledger\u2026</p>');try{allQuests=await api("GET",`/api/campaigns/${s}/quests`),_q2LoadedCid=s}catch(A){n.innerHTML=o(`<p class="placeholder--padded error">${escHtml(A.message)}</p>`);return}if(!currentCampaign||currentCampaign.id!==s||a&&!n.isConnected)return}const r=(allQuests||[]).filter(_q2QuestVisibleToViewer),c=e&&e.focusId;if(c){const A=r.find(x=>x.id===c);A&&(_q2OpenId=c,(A.status==="complete"||A.status==="failed")&&_q2SecSet(s,A.status,!0))}const l=_q2CaptureState(n),d=e&&e.anchorId;let u=null;if(d){const A=n.querySelector(`#q2-q-${d}`);A&&(u=A.getBoundingClientRect().top)}const p=new Set(Object.keys(l.drafts).map(Number));l.editor&&["title","objs","gmnotes","lead"].includes(l.editor.kind)&&l.editor.qid&&p.add(+l.editor.qid);const m=A=>{const x=A.introduced_session_id?_q2SessById(A.introduced_session_id):null;return x?+getSessionNum(x)||0:1/0},f=A=>{const x=A.resolved_session_id&&_q2SessById(A.resolved_session_id)||A.introduced_session_id&&_q2SessById(A.introduced_session_id)||null;return x?+getSessionNum(x)||0:-1},g=A=>{const x=A?_q2SessById(A):null;return x&&+getSessionNum(x)||0},h=A=>Math.max(g(A.introduced_session_id),g(A.resolved_session_id),0,..._q2Leads(A).map(x=>g(x.session_id))),v=(A,x)=>h(x)-h(A)||m(A)-m(x)||A.id-x.id,b=(A,x)=>f(x)-f(A)||x.id-A.id,y=r.filter(A=>A.status==="active"),_=[...y.filter(A=>!A.is_background).sort(v),...y.filter(A=>A.is_background).sort(v)],w=r.filter(A=>A.status==="complete").sort(b),$=r.filter(A=>A.status==="failed").sort(b),E={active:_.length,fulfilled:w.length,failed:$.length},k=A=>A.map(x=>x.id===_q2OpenId||p.has(x.id)?_q2EntryHtml(x):_q2RowHtml(x)).join(""),C='<button class="q2-pill-cta" id="q2-new-btn" title="Add a quest to the ledger">+ New Quest</button>',S=_.length?`<section class="q2-section">
3341    ${_q2SectionBarHtml({name:"Active Quests",ro:`${E.active} active`,ctaHtml:_isGm()?C:""})}
3342    ${k(_)}
3343  </section>`:"",q=(A,x,M,D,O)=>{if(!O.length)return"";const N=_q2SecIsOpen(s,A);let G=null,F=null,W=-1;O.forEach(V=>{const K=V.resolved_session_id?_q2SessById(V.resolved_session_id):null,
3343se=K?+getSessionNum(K)||0:-1;K&&se>=W&&(G=V,F=K,W=se)});const z=N||!G?"":`<p class="q2-sec-teaser">Latest: <i>${escHtml(G.title)}</i> \xB7 ${escHtml(_q2ChapLabel(F,!1))}</p>`;return`<section class="q2-section">
3344      ${_q2SectionBarHtml({key:A,name:x,glyph:M,color:D,chev:!0,open:N,ro:`${O.length} ${O.length===1?"entry":"entries"}`})}
3345      ${N?k(O):z}
3346    </section>`},I=q("complete","Fulfilled","","",w),T=q("failed","Failed","","",$),P=!(y.length||w.length||$.length),B=P?_isGm()?`<div class="q2-empty-first">
3347           <p class="p2-empty-note">Quests appear here after your first recap, or add one now to get started.</p>
3348           <div class="q2-empty-actions">
3349             <button type="button" class="q2-pill-cta q2-empty-session">Start your first session \u2192</button>
3350             ${C}
3351           </div>
3352         </div>`:'<p class="p2-empty-note">Nothing has been entered in the ledger yet.</p>':"",H=_isGm()&&!_.length&&!P?`<div class="q2-new-row">${C}</div>`:"";if(n.innerHTML=o(`
3353    ${a?"":_introBannerHtml("quests")}
3354    ${_q2Masthead(E,a)}
3355    ${S}
3356    ${I}
3357    ${T}
3358    ${B}
3359    ${H}
3360    ${a?_p2Colophon(((currentCampaign||{}).quests_title||"").trim()||"The Quest Log"):`<div class="colophon"><span class="mark">${_CHR2_MARK_SVG} Threadfall Quests</span><span>${escHtml(currentCampaign&&currentCampaign.name||"")}</span></div>`}
3361  `),a&&(n.dataset.q2Ready="1",n.dataset.cid=String(s),n.dataset.viewer=_isGm()?"gm":"player"),_q2Wire(n,s),n.querySelector(".q2-empty-session")?.addEventListener("click",()=>document.getElementById("tab-btn-sessions")?.click()),_q2RestoreState(n,l),d&&u!=null){const A=n.querySelector(`#q2-q-${d}`),x=_q2Stage(n);A&&x&&(x.scrollBy(0,A.getBoundingClientRect().top-u),_q2JustOpened===d&&A.scrollIntoView({block:"nearest"}))}if(_q2JustOpened=null,a&&!c&&_p2Reland(document.getElementById("tab-party2")),c){const A=n.querySelector(`#q2-q-${c}`);A&&(A.scrollIntoView({behavior:"smooth",block:"center"}),A.classList.add("q2-focus"),setTimeout(()=>A.classList.remove("q2-focus"),2400))}}async function _q2SaveQuest(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return!1;try{const a=await api("PUT",`/api/campaigns/${n}/quests/${e}`,t);return allQuests=allQuests.map(s=>s.id===e?a:s),renderQuests2({cid:n}),!0}catch(a){return showToast("Save failed: "+a.message),!1}}function _q2InlineEdit(e,{value:t,multiline:n,textarea:a,placeholder:s,caretEnd:i,ctx:o,onSave:r}){if(!e||e.dataset.q2Editing==="1")return;e.dataset.q2Editing="1";const c=document.createElement("div");c.className="q2-editing q2-inline-editor",o&&(c.dataset.q2Kind=o.kind||"",o.qid!=null&&(c.dataset.q2Qid=String(o.qid)),o.leadId!=null&&(c.dataset.q2Lead=String(o.leadId)));const l=n||a,d=document.createElement(l?"textarea":"input");d.className=n?"q2-edit-ta":"q2-edit-input",d.value=t||"",s&&(d.placeholder=s),n?d.rows=Math.min(8,Math.max(3,(t||"").split(`
3362`).length+1)):l&&(d.rows=Math.min(6,Math.max(2,Math.ceil(((t||"").length+20)/60))));const u=document.createElement("div");u.className="chr2-editor-actions",u.innerHTML=`<button type="button" class="q2-ed-save btn-ghost btn-xs">Save</button><button type="button" class="q2-ed-cancel btn-ghost btn-xs">Cancel</button>${n?'<span class="form-helper-text">Esc cancels \xB7 Ctrl+Enter saves</span>':""}`,c.appendChild(d),c.appendChild(u);const p=e.closest(".chr2-field-row");p&&p.classList.add("editing");const m=e.style.display;if(e.style.display="none",e.insertAdjacentElement("afterend",c),d.focus(),i)try{d.setSelectionRange(d.value.length,d.value.length)}catch{}else l||d.select();const f=()=>{c.remove(),e.style.display=m,e.dataset.q2Editing="",p&&p.classList.remove("editing")};u.querySelector(".q2-ed-cancel").addEventListener("click",f),u.querySelector(".q2-ed-save").addEventListener("click",async()=>{if(c.dataset.q2Closing="1",await r(a&&!n?d.value.replace(/\n+/g," "):d.value)===!1){c.dataset.q2Closing="";return}f()}),d.addEventListener("keydown",g=>{g.key==="Escape"?(g.preventDefault(),f()):g.key==="Enter"&&(!n||g.ctrlKey||g.metaKey)&&(g.preventDefault(),u.querySelector(".q2-ed-save").click())})}let _q2UndoTimer=null;function _q2UndoBar(e,t,n,a){if(e.querySelectorAll(".q2-undo-bar").forEach(i=>
3362i.remove()),_q2UndoTimer&&clearTimeout(_q2UndoTimer),!t)return;const s=document.createElement("div");s.className="q2-undo-bar",s.innerHTML=`<span>${escHtml(n)}</span><span class="q2-kdot">\xB7</span><button type="button" class="q2-undo">Undo</button>`,t.insertAdjacentElement("afterend",s),s.querySelector(".q2-undo").addEventListener("click",()=>{s.remove(),a()}),_q2UndoTimer=setTimeout(()=>s.remove(),6e4)}function _q2Wire(e,t){const n=u=>(allQuests||[]).find(p=>p.id===u),a=u=>{if(document.activeElement&&document.activeElement!==document.body)return;const p=e.querySelector(u);p&&p.offsetParent!==null&&p.focus({preventScroll:!0})};e.querySelectorAll(".q2-row").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.id;_q2OpenId=p,_q2JustOpened=p,await renderQuests2({cid:t,anchorId:p}),a(`#q2-q-${p}`)})),e.querySelectorAll(".q2-section-bar--toggle").forEach(u=>u.addEventListener("click",async()=>{const p=u.dataset.sec;_q2SecSet(t,p,!_q2SecIsOpen(t,p)),await renderQuests2({cid:t}),a(`.q2-section-bar--toggle[data-sec="${p}"]`)}));const s=async u=>{_q2OpenId===u&&(_q2OpenId=null),await renderQuests2({cid:t,anchorId:u}),a(`#q2-q-${u}`)};e.querySelectorAll(".q2-collapse-btn").forEach(u=>u.addEventListener("click",()=>s(+u.dataset.id))),e.querySelectorAll(".q2-entry .q2-head").forEach(u=>u.addEventListener("click",p=>{if(p.target.closest("a, button, .entity-link, input, textarea"))return;const m=u.closest(".q2-entry"),f=+(m&&m.id?m.id.replace("q2-q-",""):0);f&&s(f)})),e.querySelectorAll(".q2-status-btn").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.id,m=u.dataset.next,f=n(p);f&&((m==="complete"||m==="failed")&&!await confirmDialog({title:m==="complete"?"Mark Fulfilled":"Mark Failed",message:m==="complete"?`Mark "${f.title}" fulfilled? It moves to the Fulfilled section of the ledger.`:`Mark "${f.title}" failed? Failure is part of the story; it keeps its place in the ledger.`,confirmLabel:m==="complete"?"Mark Fulfilled":"Mark Failed",danger:!1})||_q2SaveQuest(p,{status:m}))})),e.querySelectorAll(".q2-del-btn").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.id,m=n(p);if(await confirmDialog({title:"Delete Quest",message:`Delete quest "${m?.title}"? This removes it from the ledger entirely, closed quests can stay in the ledger instead.`,confirmLabel:"Delete"}))try{await api("DELETE",`/api/campaigns/${t}/quests/${p}`),allQuests=allQuests.filter(f=>f.id!==p),renderQuests2({cid:t})}catch(f){showToast("Delete failed: "+f.message)}})),e.querySelectorAll(".q2-edit-btn").forEach(u=>u.addEventListener("click",()=>openQuestModal(+u.dataset.id)));const i=e.querySelector("#q2-new-btn");i&&i.addEventListener("click",()=>openQuestModal(null)),e.querySelectorAll(".q2-title-pen").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.id,m=n(p);if(!m)return;const f=u.closest(".q2-title-row");_q2InlineEdit(f.querySelector(".q2-title"),{value:m.title,placeholder:"Quest title",ctx:{kind:"title",qid:p},onSave:g=>{const h=(g||"").trim();return h?_q2SaveQuest(p,{title:h}):!1}})})),e.querySelectorAll(".q2-objs-pen").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.id,m=n(p);m&&_q2InlineEdit(u.closest(".q2-objs"),{value:m.description||"",multiline:!0,placeholder:"One objective per line\u2026",ctx:{kind:"objs",qid:p},onSave:f=>_q2SaveQuest(p,{description:(f||"").trim()||null})})})),e.querySelectorAll(".q2-gmnotes-pen").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.id,m=n(p);m&&_q2InlineEdit(u.closest(".q2-gmnotes"),{value:m.gm_notes||"",multiline:!0,placeholder:"Private prep, players never see this\u2026",ctx:{kind:"gmnotes",qid:p},onSave:f=>_q2SaveQuest(p,{gm_notes:(f||"").trim()||null})})}));const o=(u,p)=>_q2SaveQuest(u,{leads:p}),r=u=>_q2Leads(n(u)||{}).map(p=>({...p})),c=async u=>{const p=+u.dataset.qid,m=u.dataset.leadId,f=r(p),g=f.findIndex(h=>h.id===m);g<0||(f[g].status=(f[g].status||"open")==="open"?"resolved":"open",await o(p,f),a(`.q2-trail-mark[data-qid="${p}"][data-lead-id="${m}"]`))};e.querySelectorAll(".q2-trail-mark").forEach(u=>u.addEventListener("click",()=>c(u))),e.querySelectorAll(".q2-trail-check").forEach(u=>u.addEventListener("click",()=>c(u))),e.querySelectorAll(".q2-trail-dead").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.qid,m=u.dataset.leadId,f=r(p),g=f.findIndex(h=>h.id===m);g<0||(f[g].status="dead_end",await o(p,f),a(`.q2-trail-mark[data-qid="${p}"][data-lead-id="${m}"]`))})),e.querySelectorAll(".q2-trail-edit").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.qid,m=u.dataset.leadId,f=r(p),g=f.findIndex(h=>h.id===m);g<0||_q2InlineEdit(u.closest(".q2-trail"),{value:f[g].text||"",textarea:!0,caretEnd:!0,placeholder:"Lead\u2026",ctx:{kind:"lead",qid:p,leadId:m},onSave:h=>{const v=(h||"").trim();return v?(f[g].text=v,o(p,f)):!1}})})),e.querySelectorAll(".q2-trail-del").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.qid,m=u.dataset.leadId;if(!await confirmDialog({title:"Delete Lead",message:"Delete this lead? If it didn\u2019t pan out, mark it a dead end instead; that keeps the record.",confirmLabel:"Delete"}))return;const f=r(p).filter(g=>g.id!==m);o(p,f)})),e.querySelectorAll(".q2-trail-add-btn").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.id,m=u.closest(".q2-trail-add")?.querySelector(".q2-trail-input"),f=(m?.value||"").trim();if(!f)return;m&&(m.value="");const g=u.closest(".q2-trail-add")?.querySelector(".q2-trail-hidden"),h={text:f,status:"open"};g&&g.checked&&_isGm()&&(h.visibility="gm");const v=r(p);v.push(h),o(p,v)})),e.querySelectorAll(".q2-trail-hidden").forEach(u=>u.addEventListener("change",()=>{const p=+u.dataset.id;u.checked?_q2HiddenTick.add(p):_q2HiddenTick.delete(p)})),e.querySelectorAll(".q2-trail-hide").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.qid,m=u.dataset.leadId,f=r(p),g=f.findIndex(h=>h.id===m);g<0||(f[g].visibility="gm",await o(p,f),a(`.q2-trail-reveal[data-qid="${p}"][data-lead-id="${m}"]`))}));const l=u=>{const p=u.pc_entity_id?(allEntities||[]).find(m=>m.id===u.pc_entity_id):null;return p?p.name:"the party"},d=async(u,p,m,f,g)=>{try{const h=await api("POST",p);allQuests=allQuests.map(v=>v.id===u?h:v),_q2OpenId=u,await renderQuests2({cid:t,anchorId:u}),_q2UndoBar(e,g(e),`Revealed to ${f}`,async()=>{try{const v=await api("POST",m);allQuests=allQuests.map(b=>b.id===u?v:b),renderQuests2({cid:t,anchorId:u})}catch(v){showToast("Undo failed: "+v.message)}})}catch(h){showToast("Reveal failed: "+h.message)}};e.querySelectorAll(".q2-trail-reveal").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.qid,m=u.dataset.leadId,f=n(p);if(!f)return;const g=`/api/campaigns/${t}/quests/${p}/leads/${encodeURIComponent(m)}`;d(p,`${g}/reveal`,`${g}/unreveal`,l(f),h=>h.querySelector(`.q2-trail[data-lead-id="${m}"]`))})),e.querySelectorAll(".q2-quest-public").forEach(u=>u.addEventListener("click",async()=>{const p=+u.dataset.id;await _q2SaveQuest(p,{is_private:!1})&&_q2UndoBar(e,e.querySelector(`#q2-q-${p} .q2-kicker`)||e.querySelector(`#q2-q-${p}`),"Now public to the table",async()=>{await _q2SaveQuest(p,{is_private:!0})})})),e.querySelectorAll(".q2-quest-reveal").forEach(u=>u.addEventListener("click",()=>{const p=+u.dataset.id,m=n(p);if(!m)return;const f=`/api/campaigns/${t}/quests/${p}`;d(p,`${f}/reveal`,`${f}/unreveal`,l(m),g=>g.querySelector(`#q2-q-${p} .q2-kicker`)||g.querySelector(`#q2-q-${p}`))})),e.querySelectorAll(".q2-trail-input").forEach(u=>u.addEventListener("keydown",p=>{p.key==="Enter"&&(p.preventDefault(),u.closest(".q2-trail-add")?.querySelector(".q2-trail-add-btn")?.click())})),e.querySelectorAll("[data-q2-masthead]").forEach(u=>u.addEventListener("click",()=>{const p=u.dataset.q2Masthead,m=p==="quests_title",f=document.getElementById(m?"q2-title-el":"q2-lead-el");_q2InlineEdit(f,{value:((currentCampaign||{})[p]||"").trim(),multiline:!m,ctx:{kind:"masthead",qid:p},placeholder:m?"The Quest Log":"A line introducing the quest log\u2026",onSave:async g=>{const h=await _chr2SaveCampaignField(p,(g||"").trim());return h&&renderQuests2({cid:t}),h}})}))}async function _questsHandleHash(){const e=(location.hash||"").match(/^#quests\/(\d+)(?:\/(\d+))?$/);return e?_partyHandleHash(`#party/${e[1]}/quests${e[2]?"/"+e[2]:""}`):!1}
3362window.addEventListener("hashchange",_questsHandleHash),window.openQuests2=function(e){const t=e||currentCampaign&&currentCampaign.id;t&&_partyOpenQuests(t)};let _at2LoadedCid=null,_at2Maps=[],_at2Specs={},_at2MapId=null,_at2Leaflet=null,_at2InitSeq=0,_at2PlaceMode=null,_at2ViewMode=0,_at2Roads=[],_at2Encounters=[],_at2Portals=[],_at2PendingFocusXY=null,_at2PartyMapId=null,_at2ImgW=0,_at2ImgH=0,_at2SavedView={},_at2DrawMode=null,_at2RouteEdit=null,_at2ScaleMode=null,_at2DiffSecMode=null,_at2PortMode=null,_at2Editing=!1,_at2DragChip=null,_at2PrepLens=!1,_at2InboxCount=0,_at2Coarse=(()=>{try{return matchMedia("(max-width: 900px) and (pointer: coarse)").matches}catch{return!1}})(),_at2Gaz=null,_at2Journey=null,_at2SubGaz={},_at2Placements={},_at2DrillFromMap=null,_at2DrillAnchor=null,_at2MapPrefix={};function _atlas2Panel(){let e=document.getElementById("tab-atlas2");if(!e){e=document.createElement("div"),e.id="tab-atlas2",e.className="tab-panel hidden cool-paper-surface",e.setAttribute("role","tabpanel");const t=document.getElementById("tab-compendium");t.parentNode.insertBefore(e,t.nextSibling)}return e}function _atlas2Show(e,t,n){if(document.querySelectorAll(".tab-panel").forEach(a=>a.classList.add("hidden")),document.querySelectorAll(".tab-btn").forEach(a=>a.classList.remove("active")),document.querySelector('[data-tab="map"]')?.classList.add("active"),e.classList.remove("hidden"),location.hash!==t){if(!/^\/c\/\d+/.test(location.pathname))return;if(n)try{history.pushState(null,"",t)}catch{}else try{history.replaceState(null,"",t)}catch{}}}function _at2IsGm(){try{return _isGm()||!!(currentUser&&currentUser.is_admin)}catch{return!1}}function _at2Roman(e){try{return _chr2Roman(e)}catch{return String(e)}}function _at2Plural(e,t,n){return e===1?t:n}const _AT2_KIND_MAP={city:"settlement",town:"settlement",village:"settlement",settlement:"settlement",district:"settlement",capital:"settlement",hamlet:"settlement",port:"settlement",continent:"realm",region:"realm",nation:"realm",realm:"realm",kingdom:"realm",province:"realm",plane:"sacred",castle:"fort",tower:"fort",fortress:"fort",stronghold:"fort",keep:"fort",citadel:"fort",temple:"sacred",shrine:"sacred",cathedral:"sacred",monastery:"sacred",church:"sacred",building:"haven",tavern:"haven",shop:"haven",inn:"haven",market:"haven",guild:"haven",store:"haven",dungeon:"depths",ruin:"depths",ruins:"depths",cave:"depths",lair:"depths",tomb:"depths",crypt:"depths",forest:"wilds",mountain:"wilds",mountains:"wilds",swamp:"wilds",wilds:"wilds",wilderness:"wilds","body of water":"water",island:"water",ship:"water",lake:"water",river:"water",sea:"water",ocean:"water",road:"road"};function _at2KindSlug(e){return _AT2_KIND_MAP[(e||"").toLowerCase().trim()]||"other"}function _at2KindStyle(e){return`--at2-kc:var(--at2-kind-${_at2KindSlug(e)})`}function _at2SlugStyle(e){return`--at2-kc:var(--at2-kind-${e})`}function _dirKindOf(e){return e&&(e.kind||e.ledger&&e.ledger.kind)||""}const _AT2_OFFROAD_KIND=/(mountain|peak|range|hill|glacier|region|continent|sea|ocean|river|lake|marsh|swamp|body of water|moving ice|\bice\b)/i;function _at2IsOffroadKind(e){return _AT2_OFFROAD_KIND.test(String(e||""))}const _AT2_FAMILY=[{slug:"settlement",label:"Settlements"},{slug:"realm",label:"Regions"},{slug:"fort",label:"Strongholds"},{slug:"sacred",label:"Sacred sites"},{slug:"haven",label:"Shops & lodging"},{slug:"depths",label:"Ruins & dungeons"},{slug:"wilds",label:"Wilderness"},{slug:"water",label:"Waters"}];function _at2NearestToBeacon(e,t,n){if(!t||!e||!e.locations||!e.locations.length)return null;const s=Math.sqrt((n.width||1e3)**2+(n.height||1e3)**2)*.07;let i=null,o=1/0;return e.locations.forEach(r=>{const c=Math.hypot((r.x||0)-t.x,(r.y||0)-t.y);c<o&&(o=c,i=r)}),i&&o<=s?i.entity_id:null}function _at2PartyProse(){try{return(currentCampaign&&(currentCampaign.party_name||"")).trim()||"the party"}catch{return"the party"}}function _at2MapById(e){return(_at2Maps||[]).find(t=>t.id===e)||null}function _at2JoinNames(e){const t=e.filter(Boolean);return t.length<=1?t[0]||"":t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function _at2ParentsOf(e){const t=_at2MapById(e);if(!t||!t.location_entity_id)return[];const n=(allEntities||[]).find(o=>o.id===t.location_entity_id);if(!n)return[];let a=_at2Placements[n.id];(!a||!a.length)&&(a=n.map_id!=null?[n.map_id]:[]);const s=new Set,i=[];return a.forEach(o=>{if(o===e||s.has(o))return;s.add(o);const r=_at2MapById(o);r&&i.push({map:r,entity:n})}),i.sort((o,r)=>o.map.sort_order-r.map.sort_order||o.map.id-r.map.id),i}function _at2ParentOf(e,t){const n=_at2ParentsOf(e);if(!n.length)return null;if(_at2DrillFromMap!=null){const a=n.find(s=>s.map.id===_at2DrillFromMap);if(a)return a}return n[0]}function _at2PlateLabel(e){if(e.plate_label)return e.plate_label;const t=(_at2Maps||[]).findIndex(n=>n.id===e.id);return`Area Map ${_at2Roman(t+1)}`}function _at2LevelGroup(e){const t=_at2MapById(e);if(!t||!t.location_entity_id)return[];const n=(_at2Maps||[]).filter(a=>a.location_entity_id===t.location_entity_id);return n.length<2?[]:(n.sort((a,s)=>(a.level_ordinal==null)-(s.level_ordinal==null)||(a.level_ordinal||0
3362)-(s.level_ordinal||0)||(a.sort_order||0)-(s.sort_order||0)||a.id-s.id),n)}function _at2LevelLabel(e,t){if(!e)return"";if(e.level_label)return e.level_label;if(e.level_ordinal!=null)return`Level ${e.level_ordinal}`;const a=(t||_at2LevelGroup(e.id)).findIndex(s=>s.id===e.id);return`Level ${a>=0?a+1:1}`}function _at2LevelPlaceName(e){const t=e&&e.location_entity_id?(allEntities||[]).find(n=>n.id===e.location_entity_id):null;return t?t.name:e&&(e.title||_at2PlateLabel(e))||""}function _at2AdjacentLevel(e,t){const n=_at2LevelGroup(e),a=n.findIndex(s=>s.id===e);return a<0?null:n[a+(t>0?1:-1)]||null}function _at2PortalTarget(e){return e.target_map_id&&_at2MapById(e.target_map_id)?_at2MapById(e.target_map_id):_at2AdjacentLevel(e.map_id||_at2MapId,e.kind==="exit"?1:-1)}function _at2PortalTitle(e){if(e.label)return e.label;const t=_at2PortalTarget(e);return t?e.kind==="entry"?`Entrance from ${_at2LevelLabel(t)}`:`Exit to ${_at2LevelLabel(t)}`:e.kind==="entry"?"Entrance":"Exit"}function _at2LevelPickerHtml(e){const t=_at2LevelGroup(e.id);if(!t.length)return"";const n=_at2LevelPlaceName(e),a=t.findIndex(p=>p.id===e.id),s=_at2LevelLabel(t[a]||e,t),i=t.findIndex(p=>_at2PartyMapId!=null&&p.id===_at2PartyMapId),o=t[a-1],r=t[a+1],c=o?"Up to "+_at2LevelLabel(o,t):"Top level",l=r?"Down to "+_at2LevelLabel(r,t):"Bottom level",d=i>=0&&i!==a?'<span class="at2-levelnav-dot" aria-hidden="true"></span>':"",u=`${n} \u2014 ${s}`+(d?" \xB7 the party is on another level":"");return`<nav class="at2-levelnav" aria-label="Levels of ${escHtml(n)}">
3363    <div class="at2-levelnav-row">
3364      <button type="button" class="at2-mapctrl at2-levelnav-btn" id="at2-level-up" title="${escHtml(c)}" aria-label="${escHtml(c)}"${o?` data-at2-level="${o.id}"`:" disabled"}>${_MAP_ACT_ICO.chevUp}</button>
3365      <button type="button" class="at2-mapctrl at2-levelnav-btn" id="at2-level-down" title="${escHtml(l)}" aria-label="${escHtml(l)}"${r?` data-at2-level="${r.id}"`:" disabled"}>${_MAP_ACT_ICO.chevDown}</button>
3366    </div>
3367    <span class="at2-levelnav-lbl" title="${escHtml(u)}">${_MAP_ACT_ICO.stairs}<span class="at2-levelnav-n">${a+1} of ${t.length}</span>${d}</span>
3368  </nav>`}function _at2LevelGo(e){!e||e===_at2MapId||(_at2MapPrefix[e]=_at2MapPrefix[_at2MapId],_at2Go(e))}async function _at2PortalGo(e){const t=(_at2Portals||[]).find(s=>s.id===e);if(!t)return;const n=_at2PortalTarget(t);if(!n){typeof showToast=="function"&&showToast("This mark doesn\u2019t lead anywhere yet, give the place another level first.");return}const a=currentCampaign&&currentCampaign.id;try{const s=await api("GET",`/api/campaigns/${a}/maps/${n.id}/portals`),i=t.kind==="exit"?"entry":"exit",o=s&&s.portals||[],r=o.find(c=>c.kind===i&&c.target_map_id===_at2MapId)||o.find(c=>c.kind===i&&!c.target_map_id)||o.find(c=>c.kind===i);r&&(_at2PendingFocusXY={x:r.x,y:r.y})}catch{}try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2LevelGo(n.id)}async function _at2DeletePortal(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=await confirmDialog({title:"Remove this mark?",message:"This removes the entrance/exit mark from this level. The level itself stays.",confirmLabel:"Remove",danger:!0});if(!(!n||n.confirmed===!1))try{await api("DELETE",`/api/campaigns/${t}/maps/${_at2MapId}/portals/${e}`),typeof showToast=="function"&&showToast("Mark removed."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}function _at2PortalCardHtml(e,t){const n=_at2PortalTarget(e),a=_at2PortalTitle(e),s=n?`${escHtml(_at2LevelPlaceName(n))} &middot; ${escHtml(_at2LevelLabel(n))}`:"No level to lead to yet",i=n?`<button type="button" class="at2-pop-drill-in" data-at2-portalgo="${e.id}">${e.kind==="entry"?"Back to":"Go to"} ${escHtml(_at2LevelLabel(n))} &#9656;</button>`:"",o=_at2IsGm()?`<div class="at2-pop-toolbar"><button type="button" class="at2-pop-chip" data-at2-portalmove="${e.id}" title="Move this mark: tap its new spot on the map" aria-label="Move mark">${_MAP_ACT_ICO.move}</button><button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-portaldel="${e.id}" title="Remove this mark" aria-label="Remove mark">${_MAP_ACT_ICO.trash}</button></div>`:"";return`<div class="at2-pop at2-pop--toolbar"><b class="at2-pop-title">${escHtml(a)}</b><div class="at2-pop-sub">${s}</div>${i}${o}</div>`}
3368function _at2RenderPortals(e,t,n){(_at2Portals||[]).forEach(a=>{const s=_at2PortalTarget(a),i=_at2PortalTitle(a),o=`<div class="at2-portal-mark at2-portal-mark--${a.kind==="entry"?"entry":"exit"}${s?"":" at2-portal-mark--dead"}">${a.kind==="entry"?_MAP_ACT_ICO.levelIn:_MAP_ACT_ICO.levelOut}</div>`,r=L.marker(t(a.x,a.y),{icon:L.divIcon({className:"at2-portal-wrap",html:o,iconSize:[26,26],iconAnchor:[13,13],popupAnchor:[0,-16]}),title:i,keyboard:!1,zIndexOffset:260}).addTo(e),c=_at2PortalCardHtml(a,n);_at2Coarse?r.on("click",()=>_at2OpenSheet(c)):(r.bindPopup(c,{maxWidth:300,maxHeight:300,autoPan:!1}),r.off("click"),r.on("click",()=>r.openPopup()))})}function _at2ScrollHost(){const e=_atlas2Panel().querySelector(".party2-stage");if(e)return e;let t=_atlas2Panel();for(;t&&t!==document.body;){const n=getComputedStyle(t).overflowY;if((n==="auto"||n==="scroll")&&t.scrollHeight>t.clientHeight+4)return t;t=t.parentElement}return document.scrollingElement||document.documentElement}function _at2Rerender(){const e=currentCampaign&&currentCampaign.id;if(!e)return;const t=(()=>{const s=_at2ScrollHost();return s?s.scrollTop:0})(),n=()=>{try{const s=_at2ScrollHost();s&&s.scrollTop!==t&&(s.scrollTop=t)}catch{}},a=renderAtlas2({cid:e,mapId:_at2MapId});(a&&a.then?a:Promise.resolve()).then(()=>{n(),requestAnimationFrame(()=>{n(),requestAnimationFrame(()=>{n(),setTimeout(n,90)})})})}function _at2Go(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return;_at2CancelPlace();const a=`#atlas/${n}/${e}${t?"/"+t:""}`,s=_atlas2Panel(),i=()=>{const u=(()=>{const f=_at2ScrollHost();return f?f.scrollTop:0})();_atlas2Show(s,a,!0);const p=()=>{try{const f=_at2ScrollHost();f&&f.scrollTop!==u&&(f.scrollTop=u)}catch{}},m=renderAtlas2({cid:n,mapId:e,focusEntityId:t||null});(m&&m.then?m:Promise.resolve()).then(()=>{p(),requestAnimationFrame(()=>{p(),requestAnimationFrame(()=>{p(),setTimeout(p,90)})})})},o=_at2MapById(_at2MapId),r=_at2MapById(e);let c=!1;try{c=matchMedia("(prefers-reduced-motion: reduce)").matches}catch{}if(r&&r.location_entity_id&&_at2Gaz){const u=(_at2Gaz.locations||[]).findIndex(p=>p.entity_id===r.location_entity_id);if(u>=0){const p=_at2MapPrefix[_at2MapId];_at2MapPrefix[e]=(p?p+".":"")+(u+1),_at2DrillFromMap=_at2MapId}}const l=o?_at2ParentsOf(o.id).find(u=>u.map&&u.map.id===e):null;if(l&&l.map&&l.map.id===e){if(c||o.location_entity_id==null){_at2DrillAnchor=null,i();return}_at2DrillAnchor={dir:"out",entityId:o.location_entity_id},s.querySelector(".at2-plate-col")?.classList.add("at2-drilling"),setTimeout(i,200);return}const d=r&&r.location_entity_id&&_at2Leaflet&&_at2Leaflet.__at2Pins?_at2Leaflet.__at2Pins[r.location_entity_id]:null;if(d&&!c)try{_at2DrillAnchor={dir:"in"},s.querySelector(".at2-plate-col")?.classList.add("at2-drilling"),_at2Leaflet.flyTo(d.getLatLng(),Math.min(_at2Leaflet.getMaxZoom?_at2Leaflet.getMaxZoom():1,_at2Leaflet.getZoom()+1.5),{duration:.45,easeLinearity:.2}),setTimeout(i,400);return}catch{}_at2DrillAnchor=null,i()}const _AT2_NOISE=new Set(["dragged","atlas-drag"]);function _at2CleanLabel(e){const t=(e.label||"").trim();return!t||_AT2_NOISE.has(t.toLowerCase())||e.source==="drag"?null:t}function _at2LegTitle(e,t){const n=_at2CleanLabel(e);if(n&&e.source!=="log_entry")return n;if(e.location_entity_id){const a=(allEntities||[]).find(s=>s.id===e.location_entity_id);if(a&&a.name)return a.name}return t.session_title||"Waypoint"}function _at2FragQuote(e){const t=_at2CleanLabel(e);if(!t||e.source!=="log_entry")return null;const n=t.length>90?t.slice(0,90).replace(/\s+\S*$/,"")+"\u2026":t;return`\u201C${/^[a-z]/.test(n)?"\u2026":""}${n}\u201D`}async function renderAtlas2(e){const t=_atlas2Panel(),n=e&&e.cid||currentCampaign&&currentCampaign.id;_at2Coarse=(()=>{try{return matchMedia("(max-width: 900px) and (pointer: coarse)").matches}catch{return!1}})();const a=h=>`<div class="party2-stage"><div class="party2-page"><div class="index-inner tf-entering">${h}</div></div></div>`;if(!n){t.innerHTML=a('<p class="placeholder--padded">Loading the atlas\u2026</p>');return}if(_at2WireGlobalDelegation(),_at2LoadedCid!==n){t.innerHTML=a('<p class="placeholder--padded">Loading the atlas\u2026</p>');try{const[h,v]=await Promise.all([api("GET",`/api/campaigns/${n}/maps`),api("GET",`/api/campaigns/${n}/render-spec?subject_type=map`).catch(()=>null)]);_at2Maps=(h||[]).sort((b,y)=>
3368b.sort_order-y.sort_order||b.id-y.id),_at2Specs={},(v&&v.items||[]).forEach(b=>{_at2Specs[b.map_id]=b}),_at2LoadedCid=n,_at2MapId=null}catch(h){t.innerHTML=a(`<p class="placeholder--padded error">${escHtml(h.message)}</p>`);return}if(!currentCampaign||currentCampaign.id!==n)return}if(_at2ViewMode=0,_at2DrawMode=null,_at2RouteEdit=null,_at2ScaleMode=null,_at2PlaceMode=null,_at2Editing=!1,!_at2Maps.length){const h=_at2IsGm();t.innerHTML=a((h?_introBannerHtml("atlas"):"")+`<header class="index-head"><div class="index-eyebrow">Atlas</div><h1 class="index-title">${escHtml(currentCampaign&&currentCampaign.atlas_title||"The Road So Far")}</h1></header><div class="ornament index-orn"><span class="l"></span>${_CHR2_MARK_SVG}<span class="r"></span></div><p class="placeholder--padded">${h?"Chart your world: upload your first map to begin. It will appear here as Area Map I.":"Your GM hasn\u2019t charted this campaign yet."}</p>`+(h?`<div class="at2-empty-cta"><button type="button" class="at2-pill-cta" onclick="_at2OpenDrawer('plates')">+ Upload your first map</button></div>`:"")+_at2Colophon());return}let s=e&&e.mapId||_at2MapId;if(!s)try{const h=await api("GET",`/api/campaigns/${n}/party-positions?limit=1`),v=h&&h[0]&&h[0].map_id;v&&_at2MapById(v)&&(s=v)}catch{}(!s||!_at2MapById(s))&&(s=_at2Maps[0].id),_at2MapId=s;const i=_at2MapById(s),o=_at2IsGm();let r=null,c=null,l=null,d=null,u=null,p=null,m=null,f=null;try{[r,c,l,d,u,p,m,f]=await Promise.all([api("GET",`/api/campaigns/${n}/maps/${s}/journey`),api("GET",`/api/campaigns/${n}/maps/${s}/gazetteer`),o?api("GET",`/api/campaigns/${n}/atlas/pin-inbox?map_id=${s}`).catch(()=>null):Promise.resolve(null),api("GET",`/api/campaigns/${n}/maps/${s}/roads`).catch(()=>null),api("GET",`/api/campaigns/${n}/placements`).catch(()=>null),api("GET",`/api/campaigns/${n}/maps/${s}/encounters`).catch(()=>null),api("GET",`/api/campaigns/${n}/maps/${s}/portals`).catch(()=>null),api("GET",`/api/campaigns/${n}/party-positions?limit=1`).catch(()=>null)])}catch(h){t.innerHTML=a(`<p class="placeholder--padded error">${escHtml(h.message)}</p>`);return}if(!currentCampaign||currentCampaign.id!==n||_at2MapId!==s)return;_at2Placements={},(u&&u.placements||[]).forEach(h=>{(_at2Placements[h.entity_id]||(_at2Placements[h.entity_id]=[])).push(h.map_id)}),_at2Roads=d&&d.roads||[],_at2Encounters=p&&p.encounters||[],_at2Portals=m&&m.portals||[],_at2PartyMapId=f&&f[0]&&f[0].map_id||null,_at2InboxCount=l&&l.items?l.items.filter(h=>h.session_id).length:0,_at2Inbox=l,r&&r.beacon&&(r.beacon.nearest_entity_id=_at2NearestToBeacon(c,r.beacon,i)),_at2Gaz=c,_at2Journey=r,_at2SubGaz={};const g=[...new Set((c.locations||[]).filter(h=>h.area_map_id).map(h=>h.area_map_id))];if(g.length){const h=await Promise.all(g.map(v=>api("GET",`/api/campaigns/${n}/maps/${v}/gazetteer`).catch(()=>null)));if(!currentCampaign||currentCampaign.id!==n||_at2MapId!==s)return;g.forEach((v,b)=>{h[b]&&(_at2SubGaz[v]=h[b])})}t.innerHTML=a((o?_introBannerHtml("atlas"):"")+_at2Masthead(c)+(o?_at2InboxStrip():"")+_at2SecBar("Directory","","",o?_at2GmActions():"")+'<div class="at2-shell">'+_at2DirectoryRail(c,r)+`<div class="at2-plate-col">${_at2PlateHtml(i,c,r)}</div></div>`+_at2JourneyHtml(r,c)+_at2AreaMapsHtml(c)+_at2Colophon()),_at2Wire(t,n,i,c,r,e&&e.focusEntityId)}function _at2SecBar(e,t,n,a){return`<div class="at2-section">${e}${t?`<span class="at2-sec-ro">${t}</span>`:""}${a?`<span class="at2-sec-actions">${a}</span>`:""}</div>`+(n?`<p class="at2-sec-intro">${n}</p>`:"")}function _at2Masthead(e){const t=currentCampaign||{},n=_at2IsGm(),a=(t.atlas_title||"").trim()||"The Road So Far",s=(t.atlas_lead||"").trim()||`The world as ${_at2PartyProse()} has charted it.`,i=new Set;(e&&e.locations||[]).forEach(d=>(d.appearances||[]).forEach(u=>i.add(u.session_id)));const o=[],r=(e&&e.locations||[]).length;_at2Maps.length>1&&o.push(`<b>${_at2Maps.length}</b> maps`),r&&o.push(`<b>${r}</b> ${_at2Plural(r,"place","places")}`),i.size&&o.push(`<b>${i.size}</b> ${_at2Plural(i.size,"chapter","chapters")}`);const c=n?'<button type="button" class="chr2-edit-pen" data-at2-edit="title" title="Edit the atlas title">&#9998;</button>':"",l=n?'<button type="button" class="chr2-edit-pen" data-at2-edit="lead" title="Edit the lead line">&#9998;</button>':"";return _tf2SectionPlate({eyebrow:"Atlas",titleHtml:escHtml(a),titleId:"at2-title-el",titlePen:c,stats:o,leadHtml:escHtml(s),leadId:"at2-lead-el",leadPen:l})}function _at2GmActions(){const e=(_at2Maps||[]).length>0;return`<button type="button" class="at2-pill-cta" id="at2-managemaps-btn" title="${e?"Upload, rename, reorder, hide or delete maps":"Add the first map to your atlas"}">${e?"Manage Maps":"+ Add Map"}</button>`}function _at2InboxStrip(){return _at2InboxCount?`<button type="button" class="at2-inbox-strip" data-at2-editenter>
3369    <span class="at2-inbox-dot"></span>
3370    ${_at2InboxCount} ${_at2Plural(_at2InboxCount,"place","places")} from recent chapters ${_at2Plural(_at2InboxCount,"isn't","aren't")} on the map yet, chart ${_at2Plural(_at2InboxCount,"it","them")} &#9656;
3371  </button>`:""}function _at2TrayHtml(){const e=_at2Inbox&&_at2Inbox.items||[],t=_at2Inbox&&_at2Inbox.elsewhere||[],n=_at2Inbox&&_at2Inbox.off_map||[],a=e.map(l=>`<div class="at2-tray-chip" data-at2-chip="${l.entity_id}
3371" data-name="${escHtml(l.name)}" title="Click, then tap the map to place ${escHtml(l.name)}">
3372      <button type="button" class="at2-tray-name" data-at2-chipplace="${l.entity_id}" data-name="${escHtml(l.name)}">${escHtml(l.name)}${l.session_number!=null?`<span class="at2-tray-from">${_at2Roman(l.session_number)}</span>`:""}</button>
3373      <button type="button" class="at2-tray-more" data-at2-chipmenu="${l.entity_id}" data-name="${escHtml(l.name)}" aria-label="More for ${escHtml(l.name)}">&#8943;</button>
3374    </div>`).join(""),s=e.length?"":'<span class="at2-tray-empty">Every charted place is pinned, new places land here as your recaps name them.</span>',i='<button type="button" class="btn-ghost btn-xs at2-tray-extract" data-at2-extract title="AI-scan this map&rsquo;s art for labelled places to chart">&#8634; Extract (AI)</button>',o=t.length?`<button type="button" class="btn-ghost btn-xs at2-tray-aux" data-at2-traypop="else" title="Add a place that&rsquo;s charted on another map to this one">Charted elsewhere<span class="at2-tray-auxn">${t.length}</span></button>`:"",r=n.length?`<button type="button" class="btn-ghost btn-xs at2-tray-aux" data-at2-traypop="off" title="Places set aside as off the map">Set aside<span class="at2-tray-auxn">${n.length}</span></button>`:"",c=e.length>=8?'<input type="search" class="cx2-rail-search at2-tray-search" placeholder="Find a place&hellip;" aria-label="Filter places to chart">':"";return`<div class="at2-tray" id="at2-tray" aria-label="Places to chart">
3375    <div class="at2-tray-head">
3376      <span class="at2-tray-lbl">To chart</span>${e.length?'<span class="at2-tray-hint">click a place, then tap the map</span>':""}
3377      ${c}
3378      <div class="at2-tray-aux-row">${i}${o}${r}</div>
3379    </div>
3380    <div class="at2-tray-scroll">${s}${a}</div>
3381  </div>`}function _at2ScaleNudgeHtml(e){if(!e||e.scale_px_per_mile||!(_at2Roads||[]).length)return"";try{if(localStorage.getItem("tf_at2_scalenudge_"+e.id)==="1")return""}catch{}return`<p class="form-helper-text at2-wb-nudge" id="at2-wb-nudge">
3382    Set a scale to turn these roads into journeys &mdash; you&rsquo;ll get a distance and a travel time on every route.
3383    <button type="button" class="at2-wb-nudge-x" data-at2-nudgex="${e.id}" aria-label="Dismiss this note">&#10005;</button>
3384  </p>`}function _at2ViewCycleLabel(){const e=typeof _at2IsGm=="function"?_at2IsGm():!1,t=[_MAP_ACT_ICO.pinLocs,_MAP_ACT_ICO.route,_MAP_ACT_ICO.eye];return!e&&_at2ViewMode===2&&(_at2ViewMode=0),t[_at2ViewMode]||t[0]}function _at2ViewCycleTitle(){const e=typeof _at2IsGm=="function"?_at2IsGm():!1,t=["Locations","The Journey","GM Lens"];return!e&&_at2ViewMode===2&&(_at2ViewMode=0),"Map view: "+(t[_at2ViewMode]||t[0])+" \u2014 click to change"}function _at2SetView(e){const n=(typeof _at2IsGm=="function"?_at2IsGm():!1)?3:2;_at2ViewMode=(e%n+n)%n;const a=_atlas2Panel();if(!a)return;const s=a.querySelector(".at2-plate");s&&(s.classList.remove("at2-view--locs","at2-view--journey","at2-view--gmlens"),_at2ViewMode===2?s.classList.add("at2-view--locs","at2-view--gmlens"):s.classList.add("at2-view--"+["locs","journey"][_at2ViewMode])),_at2SetPrepLens(_at2ViewMode===2);const i=a.querySelector("#at2-viewcycle-btn");if(i){i.innerHTML=_at2ViewCycleLabel();const o=_at2ViewCycleTitle();i.title=o,i.setAttribute("aria-label",o)}}function _at2PlateHtml(e,t,n){const a=_at2IsGm(),s=_at2Maps.findIndex(u=>u.id===e.id),i=_at2Maps.length>1?`<div class="at2-mapsnav">
3385      <div class="at2-mapsnav-row">
3386        <button type="button" class="at2-mapctrl at2-mapsnav-btn" id="at2-map-prev" title="Previous map" aria-label="Previous map"${s<=0?" disabled":""}>${_MAP_ACT_ICO.chevL}</button>
3387        <button type="button" class="at2-mapctrl at2-mapsnav-btn" id="at2-map-next" title="Next map" aria-label="Next map"${s>=_at2Maps.length-1?" disabled":""}>${_MAP_ACT_ICO.chevR}</button>
3388      </div>
3389      <button type="button" class="at2-mapsnav-lbl" id="at2-maps-jump" title="See every map in this campaign" aria-label="Map ${s+1} of ${_at2Maps.length} \u2014 see every map in this campaign">${s+1} of ${_at2Maps.length}</button>
3390    </div>`:"",o=_at2ParentsOf(e.id),r=u=>escHtml(u.map.title||_at2PlateLabel(u.map)),c=o.length?`<span class="at2-plate-of">within ${_at2JoinNames(o.map(r))}</span>`:"",l=_at2LevelGroup(e.id).length?`<button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-portal-entry-btn" title="Mark where the party arrives on this level from the preceding level">${_MAP_ACT_ICO.levelIn} Mark entrance</button><button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-portal-exit-btn" title="Mark where the party leaves this level for the next level">${_MAP_ACT_ICO.levelOut} Mark exit</button>`:"",d=a?`<section class="at2-workbench" id="at2-workbench" aria-label="Surveyor&rsquo;s Tools">
3391      <div class="at2-wb-head">
3392        <span class="at2-section at2-wb-eyebrow">Surveyor&rsquo;s Tools</span>
3393        ${_isGm()?'<button type="button" class="btn-ghost btn-xs at2-dir-btn" id="at2-dir-btn" title="Get directions between two places: tap a start, tap a destination"><span class="at2-dir-btn-glyph">&#10547;</span> Directions</button>':""}
3394        <button type="button" class="at2-wb-toggle" id="at2-wb-toggle" aria-expanded="true" aria-controls="at2-wb-body" title="Show or hide the map tools"><span class="at2-wb-toggle-txt">Hide</span></button>
3395      </div>
3396      <div class="at2-wb-body" id="at2-wb-body">
3397        <!-- Three groups, ordered by what the GM is doing, and a group never breaks across
3398             a row (owner 2026-09-20: "the order/bunching of the buttons below the map needs
3399             to be rationalized"). It used to run Add / Draw / Encounter / Entrance on one
3400             line and strand Mark exit alone on the next, because the row wrapped wherever
3401             it ran out of width rather than at a group boundary.
3402             MARK puts something on THIS map. CONNECT joins this map to somewhere else,
3403             which is what a route and a pair of level portals both do. MEASURE is the pair
3404             that turns geometry into distance and time. -->
3405        <div class="at2-wb-palette">
3406          <span class="at2-wb-group" title="Put something on this map.">
3407            <button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-addloc-btn" title="Add a new place to this map">${_MAP_ACT_ICO.addPlace} Add location</button>
3408            <button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-encounter-btn" title="Mark where an encounter happened: a map-only mark with chapter, encounter number and what went down (never a Codex place)">${_MAP_ACT_ICO.swords} Mark encounter</button>
3409          </span>
3410          <span class="at2-wb-group" title="Join this map to somewhere else.">
3411            <button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool at2-draw-btn" id="at2-drawroute-btn" title="Draw a road or water route: click a start, click waypoints, finish on a place or another road">${_MAP_ACT_ICO.route} Draw route</button>
3412            ${l}
3413          </span>
3414          <span class="at2-wb-group at2-wb-group--measure" title="Scale unlocks distances; Travel sets units and speed.">
3415            <button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-setscale-btn" title="Set the map scale: click the two ends of a known distance (a scale-bar segment or one hex), then enter how far it is. Unlocks distance + travel-time on roads.">${_MAP_ACT_ICO.ruler} Set scale</button>
3416            <button type="button" class="btn-ghost btn-sm at2-ctrl-gm at2-wb-tool" id="at2-travel-btn" title="Travel settings: display unit (incl. homebrew) + how fast each mode travels (miles per day).">${_MAP_ACT_ICO.clock} Travel&hellip;</button>
3417          </span>
3418        </div>
3419        ${_at2ScaleNudgeHtml(e)}
3420        ${_at2TrayHtml()}
3421      </div>
3422    </section>`:"";return`<figure class="at2-plate at2-view--${["locs","journey"][_at2ViewMode]||"locs"}">
3423    <!-- The map's own controls: a column of icon squares tucked under Leaflet's +/-, so
3424         they read as one stack and stop laying a row of text across the art (owner
3425         2026-09-20). Every one carries a title AND an aria-label, because an icon-only
3426         control has no accessible name otherwise. Directions moved to the workbench head. -->
3427    <!-- Grouped by what they do, with a gap between groups (owner 2026-09-20): the two
3428         camera controls sit straight under Leaflet's +/- because they are the same kind of
3429         thing as zooming; the view cycle is its own group and wears a small cycle mark so
3430         it reads as "this steps through states" rather than "this does one thing"; the
3431         party pin is a GM authoring action and stands alone. -->
3432    <!-- .at2-mapbox is exactly the map's box and nothing else. The overlays used to anchor
3433         to .at2-plate, which was fine while the figure ended at the map - but the figure
3434         now carries the workbench and the legend too, so it runs ~300px past the map's
3435         bottom edge and the level pager's bottom:10px put it down in the workbench
3436         instead of on the map (owner 2026-09-20: "those arrows are not inside the map
3437         box"). Anchoring to the map itself is what makes that structurally impossible
3438         rather than a number someone has to keep in sync. -->
3439    <div class="at2-mapbox">
3440    <div class="at2-plate-ctrls">
3441      <button type="button" class="at2-mapctrl" id="at2-zoom-btn" title="Zoom in on the party" aria-label="Zoom in on the party">${_MAP_ACT_ICO.here}</button>
3442      <button type="button" class="at2-mapctrl" id="at2-fit-btn" title="Fit the whole map" aria-label="Fit the whole map">${_MAP_ACT_ICO.fitFrame}</button>
3443      <span class="at2-mapctrl-unit at2-mapctrl--gap">
3444        <button type="button" class="at2-mapctrl at2-view-cycle" id="at2-viewcycle-btn" title="${_at2ViewCycleTitle()}" aria-label="${_at2ViewCycleTitle()}">${_at2ViewCycleLabel()}</button>
3445        <span class="at2-mapctrl-cyc" aria-hidden="true">${_MAP_ACT_ICO.cycle}</span>
3446      </span>
3447      ${a?`<button type="button" class="at2-mapctrl at2-ctrl-gm at2-mapctrl--gap" id="at2-party-btn" title="Drop a pin where the party stands now" aria-label="Drop a pin where the party stands now">${_MAP_ACT_ICO.person}</button>`:""}
3448    </div>
3449    ${i}
3450    <div id="at2-map" class="at2-map"></div>
3451    ${_at2LevelPickerHtml(e)}
3452    </div>
3453    ${d}
3454    ${_at2LegendHtml(t,n,e)}
3455    <div class="at2-place-banner hidden" id="at2-place-banner"></div>
3456    <!-- Directions (Atlas \u2463 parity): banner while picking A/B, then a bottom-docked result card. -->
3457    <div class="at2-dir-banner hidden" id="at2-dir-banner"></div>
3458    <div class="at2-dir-card cool-paper-surface hidden" id="at2-dir-card"></div>
3459  </figure>`}function _at2LegendHtml(e,t,n){const a=e&&e.locations||[],s=[],i=new Set;a.forEach(m=>{const f=_at2KindSlug(m.kind);f&&f!=="other"&&i.add(f)}),_AT2_FAMILY.forEach(m=>{i.has(m.slug)&&s.push(`<span class="at2-key-row"><span class="at2-key-swatch" style="${_at2SlugStyle(m.slug)}"></span><span class="at2-key-label">${escHtml(m.label)}</span></span>`)}),(_at2Roads||[]).some(m=>m.terrain==="difficult"||Array.isArray(m.diff_sections)&&m.diff_sections.length)&&s.push('<span class="at2-key-row"><span class="at2-key-hatch"></span><span class="at2-key-label">Difficult terrain</span></span>'),s.push('<span class="at2-key-row"><span class="at2-key-route"></span><span class="at2-key-label">Route</span></span>'),(t&&t.segments||[]).some(m=>(m.deaths||[]).length&&(m.points||[]).length)&&s.push('<span class="at2-key-row"><span class="at2-key-death">&#9760;</span><span class="at2-key-label">Fell here</span></span>'),(_at2Encounters||[]).some(m=>!m.hidden||_at2IsGm())&&s.push(`<span class="at2-key-row"><span class="at2-key-enc">${_MAP_ACT_ICO.swords}</span><span class="at2-key-label">Encounter</span></span>`),(_at2Roads||[]).some(m=>m.encounter_id&&(!m.hidden||_at2IsGm()))&&s.push('<span class="at2-key-row"><span class="at2-key-trail"></span><span class="at2-key-label">Trail</span></span>'),t&&t.beacon&&s.push('<span class="at2-key-row"><span class="at2-key-beacon"></span><span class="at2-key-label">The party</span></span>'),_at2IsGm()&&a.some(m=>m.map_pin_hidden)&&s.push('<span class="at2-key-row"><span class="at2-key-pin at2-key-pin--hidden"></span><span class="at2-key-label">Hidden (GM)</span></span>');const r=_at2IsGm(),c=n?_at2ParentOf(n.id):null,l=c&&c.map?`<span class="at2-plate-of">within ${escHtml(c.map.title||_at2PlateLabel(c.map))}</span>`:"",d=n?`<span class="at2-key-identity">
3460      <span class="at2-plate-no">${escHtml(_at2PlateLabel(n).toUpperCase())}</span>
3461      <span class="at2-plate-title" id="at2-platetitle-el">${escHtml(n.title||"")}</span>
3462      ${r?'<button type="button" class="chr2-edit-pen at2-pt-pen" data-at2-edit="platetitle" title="Edit the map title">&#9998;</button>':""}
3463      ${(()=>{const m=_at2LevelGroup(n.id);return m.length?`<span class="at2-plate-level">${escHtml(_at2LevelLabel(n,m))} of ${m.length}</span>`:""})()}
3464      ${l}
3465    </span>`:"",u=s.length?`<span class="at2-key-title">Key</span>${s.join("")}`:"";return!d&&!u?"":`<aside class="at2-map-key" aria-label="Map key">${d}${d&&u?'<span class="at2-key-divider"></span>':""}${u}</aside>`}function _at2JourneyHtml(e,t){const n=e&&e.segments||[],a=window.AtlasRoutes,s=t&&t.locations||[],i={},o={};
3465s.forEach(x=>{o[x.entity_id]={key:x.entity_id,x:x.x,y:x.y,name:x.name},x.name&&(i[x.name.trim().toLowerCase()]=x.entity_id)});const r={};s.forEach(x=>{if(!x.area_map_id)return;const M=_at2SubGaz&&_at2SubGaz[x.area_map_id];(M&&M.locations||[]).forEach(D=>{D.name&&(r[D.name.trim().toLowerCase()]=x.entity_id)})});const c=x=>{if(!x)return null;const M=String(x).trim().toLowerCase();return i[M]!=null?i[M]:r[M]!=null?r[M]:null};let l=()=>"";if(a&&a.projectJourney&&a.sequenceLetters){const x=[];n.forEach(G=>{const F=Array.isArray(G.chain_legs)?G.chain_legs:null;F&&F.length?F.forEach(W=>{W.kind==="transit"?(!x.length&&W.from&&x.push({name:W.from}),W.to&&x.push({name:W.to})):(W.kind==="at"||W.kind==="inside")&&W.place&&x.push({name:W.place})}):G.place&&x.push({name:G.place})});const M=a.projectJourney(x,c,o),D=a.sequenceLetters(M),O={};let N=null;l=G=>{const F=c(G);if(F==null||!D.byKey[F])return"";const W=String(D.byKey[F]).split(",").map(V=>V.trim()).filter(Boolean);F!==N&&(O[F]=(O[F]||0)+1,N=F);const z=Math.max(0,(O[F]||1)-1);return W[z]||W[W.length-1]||""}}const d=_at2IsGm();if(!n.length)return d?_at2SecBar("The Journey","","")+'<p class="p2-empty-note">The party&rsquo;s road appears here chapter by chapter once sessions have recaps.</p>':"";const u=currentCampaign&&currentCampaign.id,p=escHtml,m={foot:'<svg class="at2-itin-ico" viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="8.5" cy="7.5" rx="2.8" ry="4.2"/><ellipse cx="15" cy="14.5" rx="2.8" ry="4.2"/></svg>',boat:'<svg class="at2-itin-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2v10H5z"/><path d="M13.5 5 19 12h-5.5z"/><path d="M3 15h18l-2.4 5H5.4z"/></svg>',horse:'<svg class="at2-itin-ico at2-itin-ico--stroke" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3C3.8 6 3.4 14 7 20"/><path d="M16 3c4.2 3 4.6 11 1 17"/></svg>',air:'<svg class="at2-itin-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12 22 3 14 21 11 14z"/></svg>'},f=x=>{const M=(x||"").toLowerCase();return/boat|sail|ship|ferry|river|barge|raft|canoe/.test(M)?m.boat:/horse|ride|mount|steed|cart|wagon|carriage|caravan/.test(M)?m.horse:/air|fly|flight|wing|griffon|gryphon|broom|teleport/.test(M)?m.air:m.foot},g=x=>(x||[]).map(M=>M.entity_id?`<a class="at2-itin-death" href="#codex/${u}/${M.entity_id}">&#9760; ${p(M.name)}</a>`:`<span class="at2-itin-death">&#9760; ${p(M.name)}</span>`).join(""),h=x=>{try{return linkifyText(p(x||""),allEntities,null)}catch{return p(x||"")}}
3465,v=x=>{const M=(x||[])[0];return M?`<span class="at2-itin-beat at2-itin-beat--${M.type==="battle"?"battle":"event"}">${M.type==="battle"?"&#9876;":"&#10022;"} ${h(M.text)}</span>`:""},b=[];let y=null;const _=()=>{for(let x=b.length-1;x>=0;x--){if(b[x].t==="stop")return b[x];if(b[x].t==="ch")return null}return null},w={};n.forEach(x=>(Array.isArray(x.chain_legs)?x.chain_legs:[]).forEach(M=>{M.kind==="inside"&&M.place&&M.parent&&(w[M.place]=M.parent)}));const $=x=>w[x]||x,E=x=>{if(!x)return;const M=_();(y!==x||!M||M.place!==x)&&(b.push({t:"stop",place:x,deaths:[],beats:[]}),y=x)},k=(x,M,D)=>{w[x]&&E(w[x]),b.push({t:"sub",place:x,deaths:M,beats:D})};n.forEach(x=>{b.push({t:"ch",num:x.session_number,title:(x.session_title||"").trim(),sid:x.session_id,quote:x.quote,tally:x.tally});const M=Array.isArray(x.chain_legs)&&x.chain_legs.length?x.chain_legs:null;if(M)M.forEach(D=>{if(D.kind==="transit"){E($(D.from));const O=_();O&&(O.dep=f(D.mode)),b.push({t:"conn",mode:D.mode,deaths:D.death_links,beats:D.beats});const N=$(D.to);N&&(b.push({t:"stop",place:N,deaths:[],beats:[]}),y=N)}else if(D.kind==="inside")k(D.place,D.death_links,D.beats);else{const O=D.place||x.session_title||"\u2026";if(w[O])k(O,D.death_links,D.beats);else{const N=_();O!==y||!N?(b.push({t:"stop",place:O,deaths:D.death_links,beats:D.beats}),y=O):(N.deaths=(N.deaths||[]).concat(D.death_links||[]),N.beats=(N.beats||[]).concat(D.beats||[]))}}});else{const D=x.location_mode==="transit"?`${p(x.transit_from||"")} &rarr; ${p(x.transit_to||"")}`:p(x.place||x.session_title||"Unmapped");b.push({t:"stop",place:D,raw:!0,deaths:(x.deaths||[]).map(O=>({name:O.name,entity_id:O.entity_id})),beats:[],blurb:(x.blurb||"").trim()}),(x.inside||[]).forEach(O=>b.push({t:"sub",place:p(O),raw:!0,deaths:[],beats:[]})),y=null}});const C=x=>{if(x.t==="ch"){const W=x.sid?`<a class="at2-rr-open" href="#chronicle/${u}/${x.sid}">Open chapter &#8594;</a>`:"",z=p((x.quote&&x.quote.speaker||"").trim()),V=p((x.quote&&x.quote.context||"").trim()),K=[z,V].filter(Boolean).join(" &middot; "),se=x.quote&&x.quote.quote&&V?`<blockquote class="at2-leg-quote at2-itin-quote">&ldquo;${p(x.quote.quote)}&rdquo;<cite>\u2014 ${K}</cite></blockquote>`:"";return`<div class="at2-itin-ch"><span class="at2-itin-ch-label">Chapter ${x.num!=null?_at2Roman(x.num):""}${x.title?` &middot; ${p(x.title)}`:""}</span><span class="at2-itin-ch-right">${W}</span></div>${se}`}if(x.t==="conn"){const W=`${g(x.deaths)}${v(x.beats)}`;return`<div class="at2-itin-conn">${x.mode?`<span class="at2-itin-mode">${p(x.mode)}</span>`:""}${W?`<span class="at2-itin-marks">${W}</span>`:""}</div>`}const M=x.dep?`<span class="at2-itin-node at2-itin-node--dep">${x.dep}</span>`:'<span class="at2-itin-node"></span>',D=`${g(x.deaths)}${v(x.beats)}`,O=x.blurb?x.blurb.length>160?x.blurb.slice(0,160).replace(/\s+\S*$/,"")+"\u2026":x.blurb:"",N=O?`<span class="at2-itin-blurb">${h(O)}</span>`:"",G=x.t==="sub"?"":l(x.place),F=G?`<span class="at2-itin-letter">${p(G)}</span>`:"";return`<div class="at2-itin-stop${x.t==="sub"?" at2-itin-stop--sub":""}">${M}${F}<span class="at2-itin-place">${x.raw?x.place:p(x.place)}</span>${D?`<span class="at2-itin-marks">${D}</span>`:""}${N}</div>`},S=[];let q="",I=[],T=[];const P=()=>{T.length&&(I.push(`<div class="at2-itin-sub-block">${T.join("")}</div>`),T=[])},B=()=>{P(),(q||I.length)&&S.push(`<div class="at2-itin-chapter">${q}${I.length?`<div class="at2-itin-legs">${I.join("")}</div>`:""}</div>`),q="",I=[]};b.forEach(x=>{if(x.t==="ch"){B(),q=C(x);return}const M=C(x);x.t==="sub"?T.push(M):(P(),I.push(M))}),B();const H=n.some(x=>Array.isArray(x.chain_legs)&&x.chain_legs.length);return _at2SecBar("The Journey",d?`<button type="button" class="btn-ghost btn-xs at2-journey-rebuild" id="at2-journey-rebuild" title="Re-trace the party's continuous path from the recaps (AI)">&#10227; ${H?"Re-trace":"Trace the road"}</button>`:"","The party&rsquo;s road: one continuous trip, chapter by chapter.")+`<div class="at2-itin">${S.join("")}</div><div class="ornament index-orn"><span class="l"></span>${typeof _CHR2_MARK_SVG<"u"?_CHR2_MARK_SVG:""}<span class="r"></span></div>`}async function _at2RebuildJourney(){const e=currentCampaign&&currentCampaign.id;if(!e)return;const t=document.getElementById("at2-journey-rebuild");t&&(t.disabled=!0,t.textContent="\u27F3 Tracing\u2026");try{const n=await api("POST",`/api/campaigns/${e}/journey/rebuild`);typeof showToast=="function"&&showToast(`Road traced: ${n&&n.chapters||0} chapters.`),_at2Rerender()}catch(n){typeof showToast=="function"&&showToast("Trace failed: "+(n&&n.message||"try again")),t&&(t.disabled=!1,t.textContent="\u27F3 Re-trace")}}function _at2DirectoryRail(e,t){const n=e&&e.locations||[],a=_at2IsGm(),i=((_at2MapById(_at2MapId)||{}).title||"").trim()||currentCampaign&&currentCampaign.name||"The Atlas",o=_at2MapPrefix[_at2MapId]||"",r=b=>o?`${o}.${b}`:`${b}`,l=n.length+n.reduce((b,y)=>b+(y.area_map_id&&_at2SubGaz[y.area_map_id]?(_at2SubGaz[y.area_map_id].locations||[]).length:0),0)>=12?'<input type="search" class="cx2-rail-search at2-dir-search" placeholder="Find a place\u2026" aria-label="Filter the directory">':"",d=_at2ParentOf(_at2MapId),p=(d&&d.map?`<button type="button" class="at2-dir-return" data-at2-go="${d.map.id}" title="Back out to ${escHtml(d.map.title||_at2PlateLabel(d.map))}">&#8249; Return to ${escHtml(d.map.title||_at2PlateLabel(d.map))}</button>`:"")+l,m=p?`<div class="cx2-rail-head at2-dir-head">${p}</div>`:"";if(!n.length)return`<aside class="at2-dir-rail cx2-rail">${m}<div class="cx2-rail-list"><p class="placeholder--padded">${a?"No places charted on this map yet, open Surveyor&rsquo;s Tools to chart your first.":"No places have been charted on this map yet."}</p></div></aside>`;const f=t&&t.beacon,g=f&&f.nearest_entity_id,h=(b,y)=>{const _=b.snippet?linkifyText(escHtml(b.snippet),allEntities,b.entity_id):"",w=!!b.area_map_id,$=w?_at2SubGaz[b.area_map_id]:null,E=$?$.locations||[]:[],k=w?`<button type="button" class="at2-dir-chev" data-at2-toggle="${b.area_map_id}" aria-expanded="true" title="Collapse ${escHtml(b.name)}">&#9662;</button>`:"",C=(b.kind||"").trim(),S=w?`<button type="button" class="at2-dir-enter" data-at2-go="${b.area_map_id}" title="Enter ${escHtml(b.name)}">Enter ${C?escHtml(C)+" ":""}&#9656;</button>`:"",q=b.party_here?" at2-dir-row--party-here":b.party_within?" at2-dir-row--party-within":"",I=!w&&C?`<span class="at2-dir-kind-meta">${escHtml(C)}</span>`:"",T=I+(I&&_?" &middot; ":"")+_,P=`<div class="at2-dir-row${b.map_pin_hidden?" at2-dir-row--hidden":""}${w?" at2-dir-row--map at2-dir-row--node":""}${b.entity_id===g?" at2-dir-row--current":""}${q}" id="at2-gaz-${b.entity_id}" data-at2-kind="${_at2KindSlug(b.kind)}" style="${_at2KindStyle(b.kind)}">
3466      ${k}
3467      <button type="button" class="at2-dir-main" data-at2-fly="g${b.entity_id}" data-x="${b.x}" data-y="${b.y}" title="Find ${escHtml(b.name)} on the map">
3468        <span class="at2-dir-num${w?" at2-dir-num--map":""}">${r(y+1)}</span>
3469        <span class="cx2-ri-text">
3470          ${b.entity_id===g?'<span class="at2-dir-yah-inline"><span class="at2-dir-yah-beacon"></span>You Are Here</span>':""}
3471          <span class="cx2-ri-name">${escHtml(b.name)}${b.map_pin_hidden?' <span class="at2-gaz-hidden">Hidden</span>':""}</span>
3472          ${T?`<span class="cx2-ri-meta">${T}</span>`:""}
3473        </span>
3474      </button>
3475      ${S}
3476    </div>`;if(!w)return P;const B=E.length?E.map((H,A)=>`<div class="at2-dir-row at2-dir-row--l2${H.map_pin_hidden?" at2-dir-row--hidden":""}${H.party_here?" at2-dir-row--party-here":H.party_within?" at2-dir-row--party-within":""}" data-at2-kind="${_at2KindSlug(H.kind)}" style="${_at2KindStyle(H.kind)}">
3477        <span class="at2-dir-guide"></span>
3478        <button type="button" class="at2-dir-main" data-at2-go="${b.area_map_id}" data-at2-focus="${H.entity_id}" title="Open ${escHtml(H.name)} in ${escHtml(b.name)}">
3479          <span class="at2-dir-num at2-dir-num--sub">${r(y+1)}.${A+1}</span>
3480          <span class="cx2-ri-text"><span class="cx2-ri-name">${escHtml(H.name)}</span>${(H.kind||"").trim()?`<span class="cx2-ri-meta"><span class="at2-dir-kind-meta">${escHtml((H.kind||"").trim())}</span></span>`:""}</span>
3481        </button>
3482      </div>`).join(""):`<p class="at2-dir-subempty placeholder--padded">${a?"No places charted inside yet.":"Nothing charted here yet."}</p>`;return P+`<div class="at2-dir-subtree" id="at2-sub-${b.area_map_id}">${B}</div>`},v=n.map(h).join("");return`<aside class="at2-dir-rail cx2-rail">${m}<div class="cx2-rail-list">${v}${_at2RailRouteRows(e,a)}</div></aside>`}function _at2ParsedRoads(){return(_at2Roads||[]).map((e,t)=>{let n=e.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}n=Array.isArray(n)?n:[];const a=e.encounter_id&&(_at2Encounters||[]).some(s=>s.id===e.encounter_id&&s.hidden);return{id:e.id,type:e.route_type==="water"?"water":"land",from:e.from_entity_id,to:e.to_entity_id,hidden:!!e.hidden||!!a,wps:n,roman:_at2Roman(t+1),label:e.label,entity_id:e.entity_id,terrain:e.terrain||"road",diff_sections:e.diff_sections||null,route_type:e.route_type,ports:e.ports||null,encounter_id:e.encounter_id||null,encounter_session_id:e.encounter_session_id||null}})}function _at2ParsePorts(e){let t=e&&e.ports;if(typeof t=="string")try{t=JSON.parse(t)}catch{t=[]}return Array.isArray(t)?t.map(n=>n&&typeof n=="object"?n.frac:n).filter(n=>typeof n=="number"&&n>=0&&n<=1):[]}let _at2CurrentGaz=null;function _at2RoadDisplayName(e,t){t=t||_at2CurrentGaz;const n=(e.label||"").trim();if(n)return n;if(e.encounter_id){const p=_at2EncById(e.encounter_id);return p&&p.encounter_no?`Trail to Encounter ${p.encounter_no}`:"Trail to encounter"}const a=window.AtlasRoutes,s=_at2ParsedRoads(),i={};(t&&t.locations||[]).forEach(p=>{i[p.entity_id]=p});const o=s.filter(p=>p.wps.length>=2).map(p=>({id:p.id,pts:p.wps})),r=5,c=p=>p&&_at2ImgW&&_at2ImgH&&(p.x<=r||p.x>=_at2ImgW-r||p.y<=r||p.y>=_at2ImgH-r),l=(p,m)=>{if(p!=null&&i[p])return i[p].name;if(m&&a){const f=a.nearestOnPolylines(m,o.filter(g=>g.id!==e.id),8);if(f){const g=s.find(h=>h.id===f.id);if(g)return(g.label||"").trim()||"Road "+g.roman}}return c(m)?"the edge":null},d=l(e.from,e.wps[0]),u=l(e.to,e.wps[e.wps.length-1]);return d&&u?`${d} \u27F7 ${u}`:d||u?`${d||u} \u27F7 open`:"Road "+e.roman}function _at2RailRouteRows(e,t){const n=_at2ParsedRoads();return n.length?`<span class="form-section-label at2-dir-routes-hdr">Routes</span>${n.map(s=>{const i=escHtml(_at2RoadDisplayName(s,e)),o=_at2MapById(_at2MapId)||{},r=window.AtlasRoutes,c=o.scale_px_per_mile&&r?r.pathMiles(s.wps,o.scale_px_per_mile):null,l=c!=null&&r?r.formatDistance(c,o.scale_unit||"mi",o.map_travel||null):"",d=t&&s.hidden?' <span class="at2-gaz-hidden">Hidden</span>':"",u=(s.encounter_id?"trail":(s.type==="water"?"water":"land")+" route")+(l?" &middot; "+l:"")+(s.entity_id?" &middot; in Codex":""),p=`<span class="at2-rail-route at2-rail-route--${s.type}" aria-hidden="true"><span class="at2-rail-route-num">${s.roman}</span><span class="at2-rail-route-line"></span></span>`;return`<div class="at2-dir-row at2-dir-row--route${s.hidden?" at2-dir-row--hidden":""}" id="at2-road-${s.id}"><button type="button" class="at2-dir-main" data-at2-routefocus="${s.id}" title="Open this route: frame it on the map + show its menu">`+p+`<span class="cx2-ri-text"><span class="cx2-ri-name">${i}${d}</span><span class="cx2-ri-meta">${u}</span></span></button></div>`}).join("")}`:t?'<span class="form-section-label at2-dir-routes-hdr">Routes</span>
3482<p class="at2-dir-route-empty placeholder--padded">No routes drawn yet, <button type="button" class="at2-dir-route-empty-cta" data-at2-drawroute>draw a road or water route</button>.</p>':""}function _at2FocusRoute(e){const t=(_at2Roads||[]).find(r=>r.id===e);if(!t||!_at2Leaflet||!_at2Leaflet.__at2ToLL)return;let n=t.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}if(!Array.isArray(n)||n.length<2)return;_at2ViewMode!==0&&_at2SetView(0);const a=_at2Leaflet.__at2ToLL;try{_at2Leaflet.flyToBounds(L.latLngBounds(n.map(r=>a(r.x,r.y))).pad(.5),{duration:.5})}catch{}const s=window.AtlasRoutes,i=_at2MapById(_at2MapId)||{},o=(_at2ParsedRoads()||[]).find(r=>r.id===e);if(o&&s){o._name=_at2RoadDisplayName(o);const r=s.pointAtFraction(n,.5),c=_at2RoadMenuHtml(o,currentCampaign&&currentCampaign.id,{pxPerMile:i.scale_px_per_mile||null,unit:i.scale_unit||"mi",travel:i.map_travel||null,allowScaleCta:_at2IsGm()&&!(i.scale_px_per_mile>0),gmEdit:_at2IsGm()});r&&setTimeout(()=>{try{_at2Coarse?_at2OpenSheet(c):L.popup({maxWidth:280,autoPanPadding:L.point(28,28)}).setLatLng(a(r.x,r.y)).setContent(c).openOn(_at2Leaflet)}catch{}},520)}}async function _at2NameRoad(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=(_at2Roads||[]).find(o=>o.id===e),a=n&&(n.label||"").trim(),s=await confirmDialog({title:a?"Rename this route":"Name this route",message:"A named road becomes a location in your Codex (kind: Road), linkable in your story.",confirmLabel:"Save",cancelLabel:"Cancel",danger:!1,textInput:{placeholder:"e.g. The Ten-Trail",defaultValue:a||"",rows:1,maxlength:80}});if(!s||!s.confirmed)return;const i=(s.text||"").trim();if(i)try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{label:i}),typeof showToast=="function"&&showToast("Route named, added to the Codex."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(o){typeof showToast=="function"&&showToast("Failed: "+(o&&o.message||"try again"))}}async function _at2CenterRoadOnArt(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=(_at2Roads||[]).find(r=>r.id===e);if(!n)return;let a=n.waypoints;if(typeof a=="string")try{a=JSON.parse(a)}catch{a=[]}if(!Array.isArray(a)||a.length<2)return;const i=(_at2MapById(_at2MapId)||{}).image_url,o=r=>{typeof showToast=="function"&&showToast(r)};if(!i||typeof window.TFSnap>"u"||typeof _loadImagePixels!="function"){o("Snap unavailable on this map.");return}o("\u{1F9F2} Looking for the road\u2026"),_loadImagePixels(i,1600,async r=>{if(!r){o("Couldn\u2019t read the map image (CORS).");return}let c;try{c=window.TFSnap.snapStroke(a,r,{polarity:n.route_type==="water"?"dark":"bright",workDim:1100})}catch{o("Snap failed, try again.");return}if(!c.found){o("Couldn\u2019t find a clear enough road here, your route is unchanged.");return}const l=c.snapped.map(d=>({x:Math.round(d.x),y:Math.round(d.y)}));try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{waypoints:l}),o("\u{1F9F2} Route centered on the map."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(d){o("Failed to save: "+(d&&d.message||"try again"))}})}async function _at2ToggleRoadHidden(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=(_at2Roads||[]).find(s=>s.id===e);if(!n)return;const a=!n.hidden;try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{hidden:a}),typeof showToast=="function"&&showToast(a?"Route unrevealed, only you can see it.":"Route revealed to players."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}async function _at2ToggleRoadType(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=(_at2Roads||[]).find(s=>s.id===e);if(!n)return;const a=n.route_type==="water"?"land":"water";try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{route_type:a}),typeof showToast=="function"&&showToast(a==="water"?"Now a water route.":"Now a land route."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}async function _at2ToggleRoadTerrain(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=(_at2Roads||[]).find(s=>s.id===e);if(!n)return;const a=n.terrain==="difficult"?"road":"difficult";try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{terrain:a}),typeof showToast=="function"&&showToast(a==="difficult"?"Whole road marked difficult terrain.":"Road back to normal pace."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}async function _at2DeleteRoad(e){const t=currentCampaign&&currentCampaign.id;if(!(!t||!await confirmDialog({title:"Delete route?",message:"This removes the road from this map.",confirmLabel:"Delete",danger:!0})))try{await api("DELETE",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`),typeof showToast=="function"&&showToast("Route deleted."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}let _at2EncEdit=null;function _at2EncById(e){return(_at2Encounters||[]).find(t=>
3482t.id===e)}function _at2OpenEncModal(e){const t=document.getElementById("at2-enc-modal");if(!t)return;const n=e&&e.enc;_at2EncEdit=n?{enc:n}:{x:e.x,y:e.y},t.querySelector("#at2-enc-title").textContent=n?"Edit encounter":"Mark encounter",t.querySelector("#at2-enc-save").textContent=n?"Save changes":"Save encounter";const a=t.querySelector("#at2-enc-session"),s=(Array.isArray(allSessions)?allSessions:[]).filter(i=>i&&i.id&&!i.is_session_zero);a.innerHTML='<option value="">Not linked</option>'+s.map(i=>{const o=(i.title||"").trim();return`<option value="${i.id}">Chapter ${_at2Roman(_chr2SessionNum(i))}${o?": "+escHtml(o.length>48?o.slice(0,47)+"\u2026":o):""}</option>`}).join(""),a.value=n&&n.session_id?String(n.session_id):"",t.querySelector("#at2-enc-no").value=n&&n.encounter_no||"",t.querySelector("#at2-enc-desc").value=n&&n.description||"",t.querySelector("#at2-enc-hidden").checked=!!(n&&n.hidden),t.classList.remove("hidden"),a.focus()}function _at2CloseEncModal(){_at2EncEdit=null,document.getElementById("at2-enc-modal")?.classList.add("hidden")}async function _at2SaveEncModal(){const e=_at2EncEdit,t=currentCampaign&&currentCampaign.id;if(!e||!t||!_at2MapId){_at2CloseEncModal();return}const n=s=>(document.getElementById(s)?.value||"").trim()||null,a={session_id:parseInt(document.getElementById("at2-enc-session")?.value,10)||null,encounter_no:n("at2-enc-no"),description:n("at2-enc-desc"),hidden:!!document.getElementById("at2-enc-hidden")?.checked};try{e.enc?await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/encounters/${e.enc.id}`,a):await api("POST",`/api/campaigns/${t}/maps/${_at2MapId}/encounters`,Object.assign({x:e.x,y:e.y},a)),_at2CloseEncModal(),typeof showToast=="function"&&showToast(e.enc?"Encounter updated.":"Encounter marked."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}(function(){function t(){const n=document.getElementById("at2-enc-modal");!n||n.__wired||(n.__wired=!0,n.querySelector("#at2-enc-close")?.addEventListener("click",_at2CloseEncModal),n.querySelector("#at2-enc-cancel")?.addEventListener("click",_at2CloseEncModal),n.addEventListener("click",a=>{a.target===n&&_at2CloseEncModal()}),n.querySelector("#at2-enc-form")?.addEventListener("submit",a=>{a.preventDefault(),_at2SaveEncModal()}))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t):t()})();async function _at2ToggleEncHidden(e){const t=currentCampaign&&currentCampaign.id,n=_at2EncById(e);if(!t||!n)return;const a=!n.hidden;try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/encounters/${e}`,{hidden:a}),typeof showToast=="function"&&showToast(a?"Encounter hidden, only you can see it.":"Encounter revealed to players."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}async function _at2DeleteEnc(e){const t=currentCampaign&&currentCampaign.id;if(!(!t||!await confirmDialog({title:"Delete encounter mark?",message:"This removes the encounter mark from this map, and any trails drawn to it.",confirmLabel:"Delete",danger:!0})))try{await api("DELETE",`/api/campaigns/${t}/maps/${_at2MapId}/encounters/${e}`),typeof showToast=="function"&&showToast("Encounter mark deleted."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}function _at2EncChapterLabel(e){if(e.session_id){const t=(Array.isArray(allSessions)?allSessions:[]).find(n=>n.id===e.session_id);if(t){const n=_chr2SessionNum(t);return n>0?"Chapter "+_at2Roman(n):"Session Zero"}if(e.session_title)return String(e.session_title)}return e.chapter?"Chapter "+String(e.chapter):""}function _at2EncCardHtml(e,t){const n=e.encounter_no?`Encounter ${escHtml(String(e.encounter_no))}`:"Encounter",a=[escHtml(_at2EncChapterLabel(e)),e.hidden?'<span class="at2-pop-rough">hidden from players</span>':""].filter(Boolean).join(" &middot; "),s=(e.description||"").trim(),i=s?`<div class="at2-pop-sub at2-enc-pop-desc">${linkifyText(escHtml(s),allEntities,null)}</div>`:"",o=e.session_id?`<a class="q2-ch-link" href="#chronicle/${t}/${e.session_id}">Open chapter &#8594;</a>`:"",c=_at2IsGm()?`<div class="at2-pop-toolbar"><button type="button" class="at2-pop-chip" data-at2-encedit="${e.id}" title="Edit chapter, encounter number or description" aria-label="Edit encounter"><svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 21v-2.5L17 6.5l2.5 2.5L7.5 21z"/><path d="M14.5 9l2.5 2.5"/></svg></button><button type="button" class="at2-pop-chip" data-at2-encmove="${e.id}" title="Move this mark: tap its new spot on the map" aria-label="Move encounter">${_MAP_ACT_ICO.move}</button><button type="button" class="at2-pop-chip${e.hidden?" is-on":""}" data-at2-enchide="${e.id}" title="${e.hidden?"Reveal to players":"Hide from players"}" aria-label="${e.hidden?"Reveal to players":"Hide from players"}">${e.hidden?_MAP_ACT_ICO.eyeSlash:_MAP_ACT_ICO.eye}</button><button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-encdel="${e.id}" title="Delete this encounter mark" aria-label="Delete encounter mark">${_MAP_ACT_ICO.trash}</button></div>`:"";return`<div class="at2-pop at2-pop--toolbar"><b class="at2-pop-title">${n}</b>${a?`<div class="at2-pop-sub">${a}
3482</div>`:""}${i}${o}${c}</div>`}function _at2RenderEncounters(e,t,n){const a=_at2IsGm();(_at2Encounters||[]).forEach(s=>{if(s.hidden&&!a)return;const i=`<div class="at2-enc-mark${s.hidden?" at2-enc-mark--hidden":""}">${_MAP_ACT_ICO.swords}</div>`,o=[_at2EncChapterLabel(s),s.encounter_no?"Enc. "+s.encounter_no:""].filter(Boolean).join(" \xB7 "),r=L.marker(t(s.x,s.y),{icon:L.divIcon({className:"at2-enc-wrap",html:i,iconSize:[22,22],iconAnchor:[11,11],popupAnchor:[0,-14]}),title:o?`Encounter \xB7 ${o}`:"Encounter",keyboard:!1,zIndexOffset:240}).addTo(e),c=_at2EncCardHtml(s,n);_at2Coarse?r.on("click",l=>{_at2EncDrawIntercept(s,l)||_at2OpenSheet(c)}):(r.bindPopup(c,{maxWidth:320,maxHeight:340,autoPan:!1}),r.off("click"),r.on("click",l=>{if(_at2EncDrawIntercept(s,l)){r.closePopup();return}r.openPopup()}))})}function _at2EncDrawIntercept(e,t){if(!_at2DrawMode)return!1;t&&t.originalEvent&&L.DomEvent.stop(t.originalEvent);const n=_at2DrawMode.points[_at2DrawMode.points.length-1];return n&&Math.abs(n.x-e.x)<.75&&Math.abs(n.y-e.y)<.75||_at2DrawMode.points.push({x:e.x,y:e.y,entityId:null,encounterId:e.id}),_at2DrawMode._hoverSnap=null,_at2DrawRedraw(null),_at2DrawBanner(),_at2DrawMode.points.length>=2&&_at2DrawFinish(),!0}function _at2AreaMapsHtml(e){const t=_at2Maps;if(t.length<=1)return"";const n=new Set((e&&e.locations||[]).filter(o=>o.area_map_id).map(o=>o.area_map_id)),a=t.filter(o=>o.id!==_at2MapId&&!n.has(o.id));if(!a.length)return"";const s=o=>{const r=(o.image_url||"").replace(/\/upload\/(f_auto,q_auto\/)?/,"/upload/w_120,h_80,c_fill,f_auto,q_auto/");return o.image_url?`<span class="at2-pr-thumb" style="background-image:url('${r}')"></span>`:'<span class="at2-pr-thumb at2-pr-thumb--empty"></span>'},i=o=>`<button type="button" class="at2-plate-row" data-at2-go="${o.id}">
3483      ${s(o)}
3484      <span class="at2-pr-no">${escHtml(_at2PlateLabel(o).toUpperCase())}</span>
3485      <span class="at2-pr-title">${escHtml(o.title||"")}</span>
3486      <span class="at2-rr-leader"></span>
3487    </button>`;return'<div id="at2-area-maps">'+_at2SecBar("Other Maps",`${a.length} ${_at2Plural(a.length,"map","maps")}`,_at2IsGm()?"Maps not yet pinned on this plate, link them in Surveyor&rsquo;s Tools so they join the Directory.":"Other maps in this campaign.")+`<div class="at2-plates">${a.map(i).join("")}</div></div>`}function _at2Colophon(){return`<div class="colophon"><span class="mark">${_CHR2_MARK_SVG} Threadfall Atlas</span><span>${escHtml(currentCampaign&&currentCampaign.name||"")}</span></div>`}async function _at2SavePinName(e,t,n){await api("PUT",`/api/campaigns/${n}/entities/${e}`,{name:t});const a=(allEntities||[]).find(s=>s.id===e);a&&(a.name=t)}async function _at2SaveRoadName(e,t,n){await api("PATCH",`/api/campaigns/${n}/maps/${_at2MapId}/roads/${e}`,{label:t})}
3487function _at2ToolbarInlineRename(e){if(!e||e.__inlineEditing)return;const t=e.getAttribute("data-at2-rename-inline"),n=parseInt(e.getAttribute("data-at2-rename-target"),10);if(!t||!n)return;const a=currentCampaign&&currentCampaign.id;if(!a)return;e.__inlineEditing=!0;const s=e.textContent.trim(),i=e.innerHTML,o=document.createElement("input");o.type="text",o.className="at2-pop-title-input",o.value=s,o.maxLength=120,e.innerHTML="",e.appendChild(o),o.focus(),o.select();let r=!1;const c=async l=>{if(r)return;r=!0,e.__inlineEditing=!1;const d=o.value.trim();if(!l||!d||d===s){e.innerHTML=i;return}try{t==="pin"?await _at2SavePinName(n,d,a):t==="road"&&await _at2SaveRoadName(n,d,a),e.textContent=d,typeof showToast=="function"&&showToast(`Renamed to ${d}.`);try{await renderAtlas2({cid:a,mapId:_at2MapId})}catch{}}catch(u){e.innerHTML=i,typeof showToast=="function"&&showToast("Rename failed: "+(u&&u.message||"try again"))}};o.addEventListener("keydown",l=>{l.key==="Escape"?(l.preventDefault(),c(!1)):l.key==="Enter"&&(l.preventDefault(),c(!0))}),o.addEventListener("blur",()=>c(!0))}function _at2EditField(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n={title:{sel:"#at2-title-el",multi:!1,get:()=>(currentCampaign.atlas_title||"").trim()||"The Road So Far",save:l=>api("PUT",`/api/campaigns/${t}`,{atlas_title:l||null}).then(()=>{currentCampaign.atlas_title=l})},lead:{sel:"#at2-lead-el",multi:!0,get:()=>(currentCampaign.atlas_lead||"").trim(),save:l=>api("PUT",`/api/campaigns/${t}`,{atlas_lead:l||null}).then(()=>{currentCampaign.atlas_lead=l})},platetitle:{sel:"#at2-platetitle-el",multi:!1,get:()=>((_at2MapById(_at2MapId)||{}).title||"").trim(),save:l=>api("PUT",`/api/campaigns/${t}/maps/${_at2MapId}`,{title:l||"Map"}).then(()=>{const d=_at2MapById(_at2MapId);d&&(d.title=l||"Map")})},caption:{sel:"#at2-cap-el",multi:!0,get:()=>((_at2MapById(_at2MapId)||{}).description||"").trim(),save:l=>api("PUT",`/api/campaigns/${t}/maps/${_at2MapId}`,{description:l||null}).then(()=>{const d=_at2MapById(_at2MapId);d&&(d.description=l)})}}[e];if(!n)return;const a=_atlas2Panel();let s=a.querySelector(n.sel);if(e==="caption"&&!s){const l=a.querySelector(".at2-caption-add");if(l){const d=document.createElement("span");d.id="at2-cap-el",l.replaceWith(d),s=d}}if(!s||s.__at2Editing)return;s.__at2Editing=!0;const i=s.innerHTML,o=n.get(),r=document.createElement(n.multi?"textarea":"input");r.className="form-input at2-inline-ed"+(n.multi?" at2-inline-ed--multi":""),r.value=o,s.innerHTML="",s.appendChild(r),r.focus(),r.select();const c=async l=>{if(!s.__at2Editing)return;s.__at2Editing=!1;const d=r.value.trim();if(!l||d===o){s.innerHTML=i;return}try{await n.save(d),_at2Rerender()}catch(u){s.innerHTML=i,typeof showToast=="function"&&showToast("Save failed: "+(u.message||"try again"))}};r.addEventListener("keydown",l=>{l.key==="Escape"?(l.preventDefault(),c(!1)):l.key==="Enter"&&(!n.multi||l.ctrlKey||l.metaKey)&&(l.preventDefault(),c(!0))}),r.addEventListener("blur",()=>c(!0))}function _at2MountScale(e,t,n){e.querySelectorAll(":scope > .at2-scalebar").forEach(l=>l.remove());const a=n&&n.scale_px_per_mile;if(!a||a<=0)return;const s=n&&n.scale_unit||"mi",i=document.createElement("div");i.className="at2-scalebar",i.innerHTML='<span class="at2-scalebar-line"></span><span class="at2-scalebar-label"></span>',e.appendChild(i);const o={mi:[[100/5280,"100 ft"],[200/5280,"200 ft"],[300/5280,"300 ft"],[500/5280,"500 ft"],[1e3/5280,"1000 ft"],[1,"1 mi"],[2,"2 mi"],[5,"5 mi"],[10,"10 mi"],[25,"25 mi"],[50,"50 mi"],[100,"100 mi"],[250,"250 mi"],[500,"500 mi"],[1e3,"1000 mi"]],km:[[100/1e3,"100 m"],[200/1e3,"200 m"],[300/1e3,"300 m"],[500/1e3,"500 m"],[1,"1 km"],[2,"2 km"],[5,"5 km"],[10,"10 km"],[25,"25 km"],[50,"50 km"],[100,"100 km"],[250,"250 km"],[500,"500 km"],[1e3,"1000 km"]]},r=o[s]||o.mi,c=()=>{const l=a*t.getZoomScale(t.getZoom(),0),d=110/l;let u=r[0];for(let p=0;p<r.length&&r[p][0]<=d;p++)u=r[p];i.querySelector(".at2-scalebar-line").style.width=Math.round(u[0]*l)+"px",i.querySelector(".at2-scalebar-label").textContent=u[1]};c(),t.on("zoomend",c)}async function _at2InitLeaflet(e,t,n,a,s,i){const o=e.querySelector("#at2-map");if(!o)return;if(typeof L>"u"){o.innerHTML='<p class="placeholder--padded">Loading the map\u2026</p>',o.__at2Tries=(o.__at2Tries||0)+1,o.__at2Tries<=20?setTimeout(()=>_at2InitLeaflet(e,t,n,a,s,i),400):o.innerHTML='<p class="placeholder--padded error">The map failed to load, refresh to retry.</p>';return}const r=++_at2InitSeq;if(typeof _at2DirReset=="function"&&_at2DirReset(),_at2Leaflet){try{_at2Leaflet.__at2Ro&&_at2Leaflet.__at2Ro.disconnect()}catch{}try{_at2Leaflet.remove()}catch{}_at2Leaflet=null}if(!o.clientWidth&&(await new Promise(x=>requestAnimationFrame(x)),r!==_at2InitSeq))return;const c=_at2Specs[n.id]||{},l=(c.mode==="upscaled",3);let d=n.width,u=n.height;const p=c.display_url||n.image_url;if((!d||!u)&&(await new Promise(x=>{const M=new Image;M.onload=()=>
3487{d=M.naturalWidth,u=M.naturalHeight,x()},M.onerror=()=>x(),M.src=p}),r!==_at2InitSeq))return;if(!d||!u){o.innerHTML='<p class="placeholder--padded error">Couldn&rsquo;t load this map&rsquo;s image.</p>';return}const m=Math.min(window.devicePixelRatio||1,2),f=_at2Coarse,g=.25,h=()=>{const x=o.clientWidth||800,M=o.clientHeight||520;return Math.ceil(Math.min(Math.log2(x/d),Math.log2(M/u))/g)*g},v=Math.min(f?2560:4096,Math.max(1024,Math.round((o.clientWidth||800)*m*Math.pow(2,l-h())))),b=p.replace(/\/upload\/(f_auto,q_auto\/)?/,`/upload/w_${v},c_limit,f_auto,q_auto:good/`),y=L.latLngBounds([[0,0],[u,d]]),_=L.map(o,{crs:L.CRS.Simple,attributionControl:!1,zoomSnap:g,zoomDelta:.5,wheelPxPerZoomLevel:120,wheelDebounceTime:35,zoomControl:!f,minZoom:h(),maxZoom:l,maxBounds:y.pad(.05),maxBoundsViscosity:1,bounceAtZoomLimits:!1,scrollWheelZoom:!1,keyboard:!1}),w=(x,M)=>L.latLng(u-M,x),$=x=>({x:x.lng,y:u-x.lat}),E=L.imageOverlay(b,y,{interactive:!1,opacity:0}).addTo(_);E.on("load",()=>E.setOpacity(1)),E.on("error",()=>{b!==n.image_url?E.setUrl(n.image_url):o.innerHTML='<p class="placeholder--padded error">Couldn&rsquo;t load this map&rsquo;s image.</p>'});const k=_at2DrillAnchor;_at2DrillAnchor=null;let C=!1;if(k&&!f){let x=!1;try{x=matchMedia("(prefers-reduced-motion: reduce)").matches}catch{}const M=(D,O)=>{_.setView(O,D,{animate:!1}),C=!0,requestAnimationFrame(()=>{try{_.flyToBounds(y,{padding:[12,12],duration:k.dir==="out"?.8:.55,easeLinearity:.25})}catch{_.fitBounds(y,{padding:[12,12],animate:!1})}})};if(!x&&k.dir==="out"&&k.entityId!=null){const D=(a&&a.locations||[]).find(O=>O.entity_id===k.entityId);D&&M(l,w(D.x,D.y))}else!x&&k.dir==="in"&&!i&&M(Math.min(l,h()+.9),y.getCenter())}if(!C){const x=_at2SavedView[n.id];if(x)try{_.setView([x.lat,x.lng],x.zoom,{animate:!1})}catch{_.fitBounds(y,{padding:[12,12],animate:!1})}else _.fitBounds(y,{padding:[12,12],animate:!1})}_at2Leaflet=_,_.__at2RawBounds=y,_.on("moveend",()=>{try{const x=_.getCenter();_at2SavedView[n.id]={lat:x.lat,lng:x.lng,zoom:_.getZoom()}}catch{}}),_.on("zoomend",()=>
3487{try{const x=Math.max(1,_.getZoom()-_.getMinZoom()+1),M=Math.max(.02,.05/x);_.setMaxBounds(y.pad(M))}catch{}}),_.on("popupopen",x=>{const M=x.popup;!M||!M._container||(M._container.classList.remove("at2-popup--below"),requestAnimationFrame(()=>{try{const O=_.getContainer().getBoundingClientRect();if(M._container.getBoundingClientRect().top<O.top+8){M._container.classList.add("at2-popup--below");const G=M._container.querySelector(".leaflet-popup-content-wrapper");G&&M._container.style.setProperty("--at2-pop-h",G.offsetHeight+"px")}}catch{}}))}),o.__at2HoverGuardWired||(o.__at2HoverGuardWired=!0,o.addEventListener("mousemove",x=>{const M=document.elementFromPoint(x.clientX,x.clientY),D=!!(M&&(M.classList&&M.classList.contains("at2-road-hit")||M.closest&&M.closest(".at2-road-hit"))),O=!!(M&&M.closest&&M.closest(".at2-pin-wrap"));D||(o.querySelectorAll(".at2-road-hit--lit").forEach(G=>G.classList.remove("at2-road-hit--lit")),document.querySelectorAll(".at2-dir-row--route.at2-dir-hover").forEach(G=>G.classList.remove("at2-dir-hover")),_at2LitRid=null),O||(o.querySelectorAll(".at2-pin--lit").forEach(N=>N.classList.remove("at2-pin--lit")),_at2LitEid=null)}),o.addEventListener("mouseleave",()=>{o.querySelectorAll(".at2-road-hit--lit").forEach(x=>x.classList.remove("at2-road-hit--lit")),o.querySelectorAll(".at2-pin--lit").forEach(x=>x.classList.remove("at2-pin--lit")),document.querySelectorAll(".at2-dir-row.at2-dir-hover").forEach(x=>x.classList.remove("at2-dir-hover")),_at2LitRid=null,_at2LitEid=null})),_.on("focus mouseover",()=>_.scrollWheelZoom.enable()),o.addEventListener("pointerenter",()=>_.scrollWheelZoom.enable()),o.addEventListener("pointerdown",()=>_.scrollWheelZoom.enable()),_.on("blur",()=>_.scrollWheelZoom.disable()),_.on("mouseout",()=>_.scrollWheelZoom.disable());const S=new ResizeObserver(()=>{if(r!==_at2InitSeq||!_._loaded)return;_.invalidateSize();const x=h();_.setMinZoom(x),_.getZoom()<x&&_.setZoom(x)});S.observe(o),_.__at2Ro=S;try{_at2MountScale(o,_,n)}catch(x){console.warn("[atlas] scale-bar mount failed",x)}_.on("click",x=>_at2OnMapClick($(x.latlng),t,n)),_.on("mousemove",x=>{_at2ScaleMode&&_at2ScaleHover($(x.latlng)),_at2DiffSecMode?_at2DiffSecHover($(x.latlng)):_at2DrawMode?_at2DrawHover($(x.latlng)):_at2RouteEdit&&_at2RouteEdit.extend&&_at2RouteEditHover($(x.latlng))}),_.on("mouseout",()=>{_at2DrawMode&&_at2DrawHideHover()});const q={pt:null,raf:null},I=()=>_at2DrawMode||_at2RouteEdit&&_at2RouteEdit.extend,T=()=>{if(q.raf=null,!I()||!q.pt)return;const x=_.getSize(),M=q.pt,D=38,O=7;let N=0,G=0;if(M.x<D?N=M.x-D:M.x>x.x-D&&(N=M.x-(x.x-D)),M.y<D?G=M.y-D:M.y>x.y-D&&(G=M.y-(x.y-D)),!N&&!G)return;const F=O/D;_.panBy([Math.max(-O,Math.min(O,N*F)),Math.max(-O,Math.min(O,G*F))],{animate:!1});try{const W=$(_.containerPointToLatLng(M));_at2DrawMode?_at2DrawHover(W):_at2RouteEdit&&_at2RouteEdit.extend&&_at2RouteEditHover(W)}catch{}q.raf=requestAnimationFrame(T)};_.on("mousemove",x=>{if(!I()){q.pt=null;return}q.pt=x.containerPoint,q.raf||(q.raf=requestAnimationFrame(T))}),_.on("mouseout",()=>{q.pt=null});const P={},B=a&&a.locations||[],H={},A=_at2MapPrefix[n.id]||"";B.forEach((x,M)=>{const D=!!x.area_map_id,O=x.party_here?" at2-pin--party-here":x.party_within?" at2-pin--party-within":"",N=_at2IsGm()&&(allEntities||[]).some(le=>le.id===x.entity_id&&le.ledger&&le.ledger.gm_note),G=window.MapStyle,F=(allEntities||[]).find(le=>le.id===x.entity_id),W=F&&thumbUrl(F.image_crop_url||F.image_url)||"",z=A?`${A}.${M+1}`:`${M+1}`,V=["tf-pin","tf-pin--"+(G?G.bucketOf(x.kind):"other"),D?"tf-pin--map":"",x.map_pin_hidden?"tf-pin--hidden":"",N?"tf-pin--noted":"",W?"tf-pin--portrait":"",x.is_port?"tf-pin--port":"",x.party_here?"tf-pin--party-here":x.party_within?"tf-pin--party-within":""].filter(Boolean).join(" "),K=G?G.pinInnerHtml({kind:x.kind,number:z,portraitUrl:W,hasMap:D}):`<div class="tf-pin-well">${W?`<img class="tf-pin-img" src="${W}" alt="" />`:""}</div><span class="tf-pin-num">${z}</span>`,se=x.is_port?`<span class="tf-pin-port" aria-hidden="true">${_TF_PORT_GLYPH}</span>`:"",Q=`<div class="${V}" style="--pin-color:${G?G.colorCss(x.kind):"var(--at2-kind-other)"}">${K}${se}</div>`,X=_at2IsGm(),te=L.marker(w(x.x,x.y),{icon:L.divIcon({className:"at2-pin-wrap"+(X?" at2-pin-wrap--draggable":""),html:Q,iconSize:[32,42],iconAnchor:[16,41],popupAnchor:[0,-40]}),title:`${z} \xB7 ${x.name}${(x.kind||"").trim()?" \xB7 "+x.kind.trim():""}`,keyboard:!1,zIndexOffset:D?300:200,draggable:X}).addTo(_);if(_at2BindMarker(te,_at2PinCardHtml(x,t),_,x,t),te.on("mouseover",()=>_at2Highlight(x.entity_id,!0,!0)),te.on("mouseout",()=>_at2Highlight(x.entity_id,!1,!0)),X){let le=null;te.on("dragstart",()=>{le=te.getLatLng();try{te.closePopup()}catch{}}),te.on("dragend",()=>_at2MovePin(te,x,t,n.id,le))}H[x.entity_id]=te});try{_at2RenderLabels(_,B,w)}catch(x){console.warn("[atlas] label render failed",x)}try{_at2RenderRoutes(_,w,B,a,s,t)}catch(x){console.warn("[atlas] route render failed",x)}try{_at2RenderEncounters(_,w,t)}catch(x){console.warn("[atlas] encounter render failed",x)}try{_at2RenderPortals(_,w,t)}catch(x){console.warn("[atlas] portal render failed",x)}if(s&&s.beacon){const x=s.beacon,M=x.nearest_entity_id?((a.locations||[]).find(D=>D.entity_id===x.nearest_entity_id)||{}).name:"";L.marker(w(x.x,x.y),{icon:L.divIcon({className:"at2-beacon-wrap",html:`<span class="at2-beacon"></span>${M?`<span class="at2-yah-label">You Are Here<span class="at2-yah-place">${escHtml(M)}</span></span>`:""}`,iconSize:[0,0],iconAnchor:[10,10]}),keyboard:!1,interactive:!1,zIndexOffset:500}).addTo(_)}if(i&&H[i]){const x=H[i],M=_.getZoom();_.setView(x.getLatLng(),Math.max(M,Math.min(M+1.5,0)),{animate:!1}),_.once("moveend",()=>{try{x.openPopup()}catch{}});const D=e.querySelector(`#at2-gaz-${i}`);D&&D.classList.add("at2-focus")}if(_at2PendingFocusXY){const x=_at2PendingFocusXY;_at2PendingFocusXY=null;try{_.setView(w(x.x,x.y),Math.min(l,Math.max(_.getZoom(),h()+1.25)),{animate:!1})}catch{}}_.__at2ToLL=w,_.__at2FromLL=$,_.__at2Pins=H,_.__at2Wps=P,_.__at2W=d,_.__at2H=u,_at2ImgW
3487=d,_at2ImgH=u}function _smoothPolyline(e,t){const n=t||10,a=.5,s=[e[0]];for(let c=1;c<e.length;c++){const l=s[s.length-1];(e[c].x!==l.x||e[c].y!==l.y)&&s.push(e[c])}if(s.length<2)return s.slice();if(s.length===2){const c=s[0],l=s[1],d=[];for(let u=0;u<=n;u++){const p=u/n;d.push({x:c.x+p*(l.x-c.x),y:c.y+p*(l.y-c.y)})}return d}const i=(c,l,d)=>d+Math.pow(Math.max(Math.hypot(l.x-c.x,l.y-c.y),1e-9),a),o=[s[0],...s,s[s.length-1]],r=[];for(let c=1;c<o.length-2;c++){const l=o[c-1],d=o[c],u=o[c+1],p=o[c+2],m=0,f=i(l,d,m),g=i(d,u,f),h=i(u,p,g);for(let v=0;v<n;v++){const b=f+v/n*(g-f),y=(C,S,q,I)=>{const T=I-q;if(Math.abs(T)<1e-9)return{x:(C.x+S.x)/2,y:(C.y+S.y)/2};const P=(b-q)/T;return{x:C.x+P*(S.x-C.x),y:C.y+P*(S.y-C.y)}},_=y(l,d,m,f),w=y(d,u,f,g),$=y(u,p,g,h),E=y(_,w,m,g),k=y(w,$,f,h);r.push(y(E,k,f,g))}}return r.push({x:s[s.length-1].x,y:s[s.length-1].y}),r}function _rgb2lab(e,t,n){let a=e/255,s=t/255,i=n/255;a=a>.04045?Math.pow((a+.055)/1.055,2.4):a/12.92,s=s>.04045?Math.pow((s+.055)/1.055,2.4):s/12.92,i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92;let o=(a*.4124+s*.3576+i*.1805)/.95047,r=a*.2126+s*.7152+i*.0722,c=(a*.0193+s*.1192+i*.9505)/1.08883;const l=d=>d>.008856?Math.cbrt(d):7.787*d+16/116;return o=l(o),r=l(r),c=l(c),[116*r-16,500*(o-r),200*(r-c)]}function _labDE(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1],e[2]-t[2])}function _loadImagePixels(e,t,n){const a=new Image;a.crossOrigin="anonymous",a.onload=()=>{const s=a.naturalWidth,i=a.naturalHeight,o=Math.min(1,(t||3e3)/s),r=Math.round(s*o),c=Math.round(i*o),l=document.createElement("canvas");l.width=r,l.height=c;const d=l.getContext("2d");d.drawImage(a,0,0,r,c);let u;try{u=d.getImageData(0,0,r,c).data}catch(p){n(null,p);return}n({data:u,cw:r,ch:c,s:o})},a.onerror=s=>n(null,s),a.src=e}function _snapPolylineToArt(e,t,n){n=n||{};const a=n.searchRadius||32,s=n.tol||40,i=n.maxSnap||28,o=(b,y)=>{const _=Math.round(b*t.s),w=Math.round(y*t.s);if(_<0||w<0||_>=t.cw||w>=t.ch)return null;const $=(w*t.cw+_)*4;return _rgb2lab(t.data[$],t.data[$+1],t.data[$+2])},r=Math.max(1,Math.floor(e.length/40)),c=[];for(let b=0;b<e.length;b+=r){const y=o(e[b].x,e[b].y);y&&c.push(y)}if(!c.length)return{pts:e.slice(),moved:e.map(()=>!1)};const l=c.reduce((b,y)=>[b[0]+y[0],b[1]+y[1],b[2]+y[2]],[0,0,0]).map(b=>b/c.length),d=c.filter(b=>_labDE(b,l)<=25),u=d.length?d:c,p=u.reduce((b,y)=>[b[0]+y[0],b[1]+y[1],b[2]+y[2]],[0,0,0]).map(b=>b/u.length),m=new Array(e.length).fill(0),f=new Array(e.length).fill(!1);for(let b=0;b<e.length;b++){const y=e[Math.max(0,b-1)],_=e[Math.min(e.length-1,b+1)];let w=_.x-y.x,$=_.y-y.y;const E=Math.hypot(w,$)||1;w/=E,$/=E;const k=-$,C=w;let S=0,q=0,I=0;for(let T=-a;T<=a;T++){const P=o(e[b].x+k*T,e[b].y+C*T);if(!P)continue;const B=Math.max(0,s-_labDE(P,p));S+=B,q+=B*T,B>I&&(I=B)}S<s*2||I<s*.25||(m[b]=Math.max(-i,Math.min(i,q/S)),f[b]=!0)}const g=[.06,.24,.4,.24,.06],h=m.slice();for(let b=0;b<e.length;b++){if(!f[b])continue;let y=0,_=0;for(let w=-2;w<=2;w++){const $=b+w;$<0||$>=e.length||!f[$]||(y+=m[$]*g[w+2],_+=g[w+2])}_&&(h[b]=y/_)}return{pts:e.map((b,y)=>{if(!f[y])return{x:b.x,y:b.y};const _=e[Math.max(0,y-1)],w=e[Math.min(e.length-1,y+1)];let $=w.x-_.x,E=w.y-_.y;const k=Math.hypot($,E)||1;return{x:b.x+-E/k*h[y],y:b.y+$/k*h[y]}}),moved:f,target:p}}function _tfSnapPreview(e){e=e||{};const t=typeof liveAllMaps<"u"&&liveAllMaps[liveCurrentMapIdx],n=t&&t.image_url;if(!n||!liveMap){console.warn("[snap] no live map");return}const a=liveMap.__roadRaw||[];_loadImagePixels(n,e.maxW||3e3,(s,i)=>{if(!s){console.error("[snap] pixel read failed (CORS?)",i);return}(liveMap.__snapPreview||[]).forEach(l=>l.remove()),liveMap.__snapPreview=[];const o=liveMap.__at2ToLL;let r=0,c=0;a.forEach(l=>{const d=_snapPolylineToArt(l.wps,s,e),u=_smoothPolyline(d.pts);liveMap.__snapPreview.push(L.polyline(u.map(p=>o(p.x,p.y)),{color:"#ff2d9b",weight:2.5,opacity:.95,interactive:!1,dashArray:l.type==="water"?"6 4":null}).addTo(liveMap)),r+=d.moved.filter(Boolean).length,c+=d.moved.length}),liveMap.getContainer().querySelectorAll(".at2-road-line").forEach(l=>l.style.opacity="0.3"),console.log(`[snap] ${a.length} roads, ${r}/${c} verts moved \xB7 magenta = snapped, dim = original`)})}function _tfRenderRoadLayer(e,t){const n=window.AtlasRoutes;if(!e||!n||!t)return;const a=t.toLL;e.__at2ToLL=a;const s=e.__at2RoadLabels=[];e.__roadNameData=[],e.__roadRaw=[],e.__at2RoadHits={},e.__at2RoadLines={}
3487,e.__at2DiffSegs=[],(t.roads||[]).forEach(i=>{if(!t.includeHidden&&i.hidden)return;const o=i.wps;if(!Array.isArray(o)||o.length<2)return;e.__roadRaw.push({type:i.type,wps:o,name:i._name||i.label||""});const r=_smoothPolyline(o),c="at2-road-line at2-road-line--"+i.type+(i.encounter_id?" at2-road-line--trail":"")+(i.hidden?" at2-road-line--hidden":""),l=L.polyline(r.map(p=>a(p.x,p.y)),{className:c,interactive:!1}).addTo(e);i.id!=null&&(e.__at2RoadLines[i.id]=l),i.type!=="water"&&(i.terrain==="difficult"?e.__at2DiffSegs.push({pts:r,hidden:!!i.hidden}):_at2ParseDiffSections(i).forEach(([p,m])=>{const f=n.subPath(r,p,m);f.length>=2&&e.__at2DiffSegs.push({pts:f,hidden:!!i.hidden})}));const d=L.polyline(r.map(p=>a(p.x,p.y)),{className:"at2-road-hit at2-road-hit--"+i.type,weight:t.hitWeight||22,interactive:!0}).addTo(e);i.id!=null&&(e.__at2RoadHits[i.id]=d),_roadBindInteractive(d,i,t.cid,{onClick:t.onRoadClick,pxPerMile:t.pxPerMile,unit:t.unit,travel:t.travel,allowScaleCta:t.allowScaleCta,popupMaxWidth:t.popupMaxWidth,gmEdit:t.gmEdit,entityTip:t.entityTip,roadId:i.id,onLiveRoadClick:t.onLiveRoadClick}),i.type==="water"&&(typeof _at2ParsePorts=="function"?_at2ParsePorts(i):[]).forEach(p=>{const m=n.pointAtFraction(o,p);m&&L.marker(a(m.x,m.y),{icon:L.divIcon({className:"at2-dock-port"+(i.hidden?" at2-dock-port--hidden":""),html:_TF_PORT_GLYPH,iconSize:[16,16],iconAnchor:[8,8]}),interactive:!1,keyboard:!1,pane:"markerPane"}).addTo(e)});const u=n.pointAtFraction(r,.5);if(u&&(t.entityTip||t.roadNames)){const p=i.label&&String(i.label).trim()||"";p&&(e.__roadNameData||(e.__roadNameData=[])).push({name:p,type:i.type,ll:r.map(m=>a(m.x,m.y))})}else if(u){const p=t.pxPerMile>0?n.pathMiles(r,t.pxPerMile):null,m=p!=null?n.formatDistance(p,t.unit||"mi",t.travel):"",f=`<span class="at2-road-label at2-road-label--${i.type}">${i.roman}${m?` <span class="at2-road-label-dist">${m}</span>`:""}</span>`,g=L.marker(a(u.x,u.y),{icon:L.divIcon({className:"at2-road-label-wrap",html:f,iconSize:[0,0],iconAnchor:[0,0]}),keyboard:!1,interactive:!1,zIndexOffset:260}).addTo(e);m&&s.push({mk:g,pts:r})}}),_at2BindRoadDeclutter(e);try{_at2DeclutterRoadLabels(e)}catch{}if(t.entityTip)try{_liveRoadCanvasEnsure(),_liveLabelsLOD()}catch{}else if(t.roadNames)try{_liveRoadCanvasEnsure(e),_at2LabelsLOD(e)}catch{}try{_diffHatchCanvasEnsure(e),_diffHatchCanvasDraw(e)}catch{}}function _at2RoadHatchTicks(e,t,n,a){let o=0,r=4.3;for(let c=1;c<t.length;c++){const l=t[c-1],d=t[c],u=d.x-l.x,p=d.y-l.y,m=Math.hypot(u,p);if(m<1e-6)continue;const f=u/m,g=p/m,h=-g,v=f;for(;r<=o+m;){const b=r-o,y=l.x+f*b,_=l.y+g*b;L.polyline([n(y+h*5,_+v*5),n(y-h*5,_-v*5)],{className:a,interactive:!1}).addTo(e),r+=4.3}o+=m}}function _at2DeclutterRoadLabels(e){const t=e.__at2RoadLabels,n=e.__at2ToLL;if(!t||!n)return;const a=64;t.forEach(({mk:s,pts:i})=>{const o=s.getElement&&s.getElement();if(!o)return;const r=o.querySelector(".at2-road-label");if(!r)return;let c=0;for(let l=1;l<i.length;l++){const d=e.latLngToContainerPoint(n(i[l-1].x,i[l-1].y)),u=e.latLngToContainerPoint(n(i[l].x,i[l].y));c+=d.distanceTo(u)}r.classList.toggle("at2-road-label--nodist",c<a)})}function _at2BindRoadDeclutter(e){e.__at2DeclutterBound||(e.__at2DeclutterBound=!0,e.on("zoomend",()=>{try{_at2DeclutterRoadLabels(e)}catch{}}))}function _at2RenderRoutes(e,t,n,a,s,i){const o=window.AtlasRoutes;if(!o)return;const r={},c={};(n||[]).forEach(_=>{r[_.entity_id]={key:_.entity_id,x:_.x,y:_.y,name:_.name},_.name&&(c[_.name.trim().toLowerCase()]=_.entity_id)}),_at2CurrentGaz=a;const l=_at2ParsedRoads();l.forEach(_=>{_._name=_at2RoadDisplayName(_,a)});const d=_at2MapById(_at2MapId)||{};_tfRenderRoadLayer(e,{roads:l,toLL:t,cid:i,includeHidden:!0,hitWeight:22,pxPerMile:d.scale_px_per_mile||null,unit:d.scale_unit||"mi",travel:d.map_travel||null,roadNames:!0,allowScaleCta:_at2IsGm()&&!(d.scale_px_per_mile>0),gmEdit:_at2IsGm(),onRoadClick:_=>_at2ScaleMode?(_at2ScaleAddPoint(e.__at2FromLL(_.latlng)),!0):_at2DiffSecMode?(_at2DiffSecAddPoint(e.__at2FromLL(_.latlng)),!0):_at2PortMode?(_at2PortAddPoint(e.__at2FromLL(_.latlng)),!0):_at2DrawMode?(_at2DrawAddPoint(e.__at2FromLL(_.latlng)),!0):_at2RouteEdit&&_at2RouteEdit.extend?(_at2RouteEditAddPoint(e.__at2FromLL(_.latlng)),!0):!1});const u=l.filter(_=>!_.encounter_id).map(_=>({route_type:_.type,waypoints:_.wps})),p={};
3487(n||[]).forEach(_=>{if(!_.area_map_id)return;const w=_at2SubGaz&&_at2SubGaz[_.area_map_id];(w&&w.locations||[]).forEach($=>{$.name&&(p[$.name.trim().toLowerCase()]=_.entity_id)})});const m=_=>{if(!_)return null;const w=String(_).trim().toLowerCase();return c[w]!=null?c[w]:p[w]!=null?p[w]:null},f=[];(s&&s.segments||[]).forEach(_=>{const w=Array.isArray(_.chain_legs)?_.chain_legs:null;w&&w.length?w.forEach($=>{$.kind==="transit"?(!f.length&&$.from&&f.push({name:$.from,mode_class:"land",beats:[],deaths:[],death_links:[]}),f.push({name:$.to,mode:$.mode||"",mode_class:$.mode_class||"land",beats:$.beats||[],deaths:$.deaths||[],death_links:$.death_links||[]})):($.kind==="at"||$.kind==="inside")&&f.push({name:$.place,mode_class:"land",beats:$.beats||[],deaths:$.deaths||[],death_links:$.death_links||[]})}):_.place&&f.push({name:_.place,mode_class:"land",beats:[],deaths:_.deaths||[],death_links:[]})});const g=o.projectJourney(f,m,r);if(g.length<2){_at2JourneyNote(e,g.length===0?"No charted journey on this map yet, the party\u2019s road fills in as sessions place them here.":"The party hasn\u2019t travelled between charted places on this map yet.");return}const h=o.buildGraph(u,Object.values(r),14),v=o.sequenceLetters(g);let b=0;for(let _=1;_<g.length;_++){const w=g[_-1],$=g[_],E=$.mode_class||"land";let k,C;if(E==="air")k=o.arcPoints({x:w.x,y:w.y},{x:$.x,y:$.y}),C="air";else{const S=o.shortestPath(h,w.key,$.key,[E]);S&&S.length>=2?(k=S,C="routed"):(k=[{x:w.x,y:w.y},{x:$.x,y:$.y}],C="straight",b++)}L.polyline(k.map(S=>t(S.x,S.y)),{className:"at2-journey-line at2-journey-line--"+C,interactive:!1}).addTo(e),_at2RouteBeats(e,t,o.pointAtFraction(k,.5),$,i)}_at2IsGm()&&b>0&&_at2JourneyNote(e,`${b} leg${b===1?"":"s"} ${b===1?"isn\u2019t":"aren\u2019t"} on a road yet, draw one to route ${b===1?"it":"them"}.`);const y={};g.forEach(_=>{y[_.key]||(y[_.key]=1,L.marker(t(_.x,_.y),{icon:L.divIcon({className:"at2-route-node-wrap",html:`<span class="at2-route-node">${v.byKey[_.key]||""}</span>`,iconSize:[0,0],iconAnchor:[20,20]}),keyboard:!1,interactive:!1,zIndexOffset:460}).addTo(e))})}function _at2RouteBeats(e,t,n,a,s){if(!n)return;let i=0;(a.beats||[]).forEach(r=>{const c=r.type==="battle",l=c?"&#9876;":"&#10022;",d=L.marker(t(n.x,n.y),{icon:L.divIcon({className:"at2-beat-wrap",html:`<span class="at2-beat at2-beat--${c?"battle":"event"}">${l}</span>`,iconSize:[0,0],iconAnchor:[9,9+i]}),keyboard:!1,zIndexOffset:440}).addTo(e);_at2BindMarker(d,`<div class="at2-pop"><b>${c?"Battle":"Turning point"}</b><div class="at2-pop-sub">${escHtml(r.text||"")}</div></div>`,e),i+=18}),(a.death_links&&a.death_links.length?a.death_links:(a.deaths||[]).map(r=>({name:r,entity_id:null}))).forEach((r,c)=>{const l=typeof r=="string"?r:r.name||"",d=typeof r=="object"?r.entity_id:null,u=L.marker(t(n.x,n.y),{icon:L.divIcon({className:"at2-death-wrap",html:'<span class="at2-death">&#9760;</span>',iconSize:[0,0],iconAnchor:[10,24+i+c*20]}),keyboard:!1,zIndexOffset:400}).addTo(e);_at2BindMarker(u,`<div class="at2-pop"><b>${escHtml(l)} fell here</b>${d?`<a class="q2-ch-link" href="#codex/${s}/${d}">In Memoriam &#8594;</a>`:""}</div>`,e)})}function _at2JourneyNote(e,t){const n=_atlas2Panel()&&_atlas2Panel().querySelector(".at2-plate");if(!n)return;let a=n.querySelector(".at2-journey-note");a||(a=document.createElement("div"),a.className="at2-journey-note",n.appendChild(a)),a.innerHTML=`<span>${escHtml(t)}</span>`}function _tfFmtTravel(e,t){if(e==null)return"";const n=t||8;if(e<1)return Math.round(e*60)+" min";if(e<n){const i=Math.round(e*10)/10;return i+(i===1?" hr":" hrs")}const a=e/n,s=a<10?Math.round(a*10)/10:Math.round(a);return s+(s===1?" day":" days")}function _at2ParseDiffSections(e){let t=e&&e.diff_sections;if(typeof t=="string")try{t=JSON.parse(t)}catch{t=[]}return Array.isArray(t)?t.filter(n=>Array.isArray(n)&&n.length===2):[]}function _at2RoadDiffFrac(e){if(!e||e.type==="water")return 0;if(e.terrain==="difficult")return 1;let t=0;return _at2ParseDiffSections(e).forEach(([n,a])=>{t+=Math.max(0,a-n)}),Math.min(1,t)}function _tfRoadDistanceLine(e,t,n,a,s,i,o){const r=window.AtlasRoutes;if(!r||!(t>0))return"";const c=r.pathMiles(e,t);if(c==null)return"";const l=r.formatDistance(c,n||"mi",i);if(typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart())return l;const d=o>0?Math.min(1,o):0,u=c*(1+d),m=(a?s?["ship","rowboat"]:["ship"]:s?["foot","mounted"]:["foot"]).map(f=>{const g=r.resolvePace(f,i);return`${_tfFmtTravel(u/g.mph,g.dayH)} ${g.label}`});return[l].concat(m).join(" &middot; ")}function _at2RoadMenuHtml(e,t,n){n=n||{};const a=window.AtlasRoutes,s=e._name||e.label&&e.label.trim()||(e.encounter_id?"Trail to encounter"
3487:"Road "+e.roman),i=e.type==="water",o=e.encounter_id?"Trail":(i?"Water":"Land")+" route",r=!i&&e.terrain==="difficult",c=i?[]:_at2ParseDiffSections(e),l=_at2RoadDiffFrac(e),d=_tfRoadDistanceLine(e.wps,n.pxPerMile,n.unit,i,!0,n.travel,l),u=!d&&!!n.allowScaleCta,p=r?' &middot; <span class="at2-pop-rough">difficult terrain</span>':c.length?` &middot; <span class="at2-pop-rough">${c.length} difficult stretch${c.length>1?"es":""}</span>`:"",m=d?`<div class="at2-pop-dist">${d}</div>`:u?'<button type="button" class="at2-pop-scale-cta" data-at2-setscale="1">&#8596; Set map scale to show distance &amp; travel time &#8594;</button>':"",f=e.entity_id?`<a class="q2-ch-link" href="#codex/${t}/${e.entity_id}">Open in Codex &#8594;</a>`:"";let g="";if(n.gmEdit){const v=i?_at2ParsePorts(e):[],b=[];b.push(`<button type="button" class="at2-pop-chip" data-at2-roadshape="${e.id}" title="Edit the road waypoints" aria-label="Edit shape">${_MAP_ACT_ICO.shape}</button>`),b.push(`<button type="button" class="at2-pop-chip${e.hidden?" is-on":""}" data-at2-roadhide="${e.id}" data-hidden="${e.hidden?1:0}" title="${e.hidden?"Reveal to players":"Hide from players"}" aria-label="${e.hidden?"Reveal to players":"Hide from players"}">${e.hidden?_MAP_ACT_ICO.eyeSlash:_MAP_ACT_ICO.eye}</button>`),b.push('<span class="at2-pop-chip-pipe" aria-hidden="true"></span>'),b.push(`<button type="button" class="at2-pop-chip" data-at2-roadcenter="${e.id}" title="Snap to map road geometry" aria-label="Snap to map art">${_MAP_ACT_ICO.magnet}</button>`),i?(b.push(`<button type="button" class="at2-pop-chip" data-at2-roadport="${e.id}" title="Add a DOCK: click a place this route reaches to make it a port, or click the route itself" aria-label="Add a dock">${_MAP_ACT_ICO.dock}</button>`),v.length&&b.push(`<button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-roadportclear="${e.id}" data-at2-confirm="Clear all docks on this water route?" title="Clear all docks on this water route" aria-label="Clear docks">${_MAP_ACT_ICO.dockClear}</button>`)):(b.push(`<button type="button" class="at2-pop-chip${r?" is-on":""}" data-at2-roadterrain="${e.id}" title="Whole road difficult terrain = half pace (5e)" aria-label="Whole road difficult terrain">${_MAP_ACT_ICO.rough}</button>`),b.push(`<button type="button" class="at2-pop-chip" data-at2-roaddiffsec="${e.id}" title="Mark a STRETCH difficult: click its start and end on the road" aria-label="Mark a difficult section">${_MAP_ACT_ICO.roughSec}</button>`),c.length&&b.push(`<button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-roaddiffclear="${e.id}" data-at2-confirm="Clear all difficult stretches on this road?" title="Clear all difficult stretches" aria-label="Clear difficult stretches">${_MAP_ACT_ICO.roughClear}</button>`)),b.push(`<button type="button" class="at2-pop-chip" data-at2-roadtype="${e.id}" title="${i?"Switch to a land road":"Switch to a water route"}" aria-label="${i?"Switch to land":"Switch to water"}">${i?_MAP_ACT_ICO.toLand:_MAP_ACT_ICO.toWater}</button>`),b.push(`<button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-roaddel="${e.id}" data-at2-confirm="Delete this road?" title="Delete this road" aria-label="Delete road">${_MAP_ACT_ICO.trash}</button>`),g=`<div class="at2-pop-toolbar">${b.join("")}</div>`}return`<div class="at2-pop at2-road-pop at2-pop--toolbar">${n.gmEdit?`<span class="at2-pop-title-row"><b class="at2-pop-title at2-pop-title--editable" data-at2-rename-inline="road" data-at2-rename-target="${e.id}" title="Click to ${e.label?"rename":"name"}">${escHtml(s)}</b><button type="button" class="at2-pop-title-pen" data-at2-rename-inline-trigger title="${e.label?"Rename":"Name"}" aria-label="${e.label?"Rename":"Name"}">&#9998;</button></span>`:`<b class="at2-pop-title">${escHtml(s)}</b>`}<div class="at2-pop-sub">${o} &middot; ${e.roman}${p}</div>${m}${f}${g}</div>`}function _roadBindInteractive(e,t,n,a){a=a||{};const s=window.AtlasRoutes,i=t._name||t.label&&t.label.trim()||"Road "+t.roman,o=t.type==="water",r=_at2RoadDiffFrac(t),c=_tfRoadDistanceLine(t.wps,a.pxPerMile,a.unit,o,!1,a.travel,r),l=!c&&!!a.allowScaleCta,d=c?`<span class="at2-road-tip-dist">${c}</span>
3487`:l?'<span class="at2-road-tip-dist at2-road-tip-cta">Set scale for distance &#8594;</span>':"";if(a.entityTip){const p=c?`<div class="map-hover-card-desc">${escHtml(c)}</div>`:l?'<div class="map-hover-card-desc">Set scale for distance &#8594;</div>':"";e.bindTooltip(`<div class="map-hover-card map-hover-card--road map-hover-card--${o?"water":"land"}"><div class="map-hover-card-name">${escHtml(i)}</div>${p}</div>`,{sticky:!0,direction:"auto",offset:[0,-8],className:"map-hover-tooltip",opacity:1})}else e.bindTooltip(escHtml(i)+d,{sticky:!0,direction:"auto",offset:[0,-2],className:"at2-road-tip",opacity:1});if(!a.entityTip&&a.roadId!=null&&(e.on("mouseover",()=>_at2HighlightRoad(a.roadId,!0,!0)),e.on("mouseout",()=>_at2HighlightRoad(a.roadId,!1,!0))),a.entityTip){typeof a.onLiveRoadClick=="function"&&e.on("click",p=>a.onLiveRoadClick(p,t));return}const u=()=>_at2RoadMenuHtml(t,n,Object.assign({},a,{gmEdit:_at2IsGm()}));if(_at2Coarse){e.on("click",p=>{if(a.onClick&&a.onClick(p)){p.originalEvent&&L.DomEvent.stop(p.originalEvent);return}_at2OpenSheet(u())});return}e.on("click",p=>{if(a.onClick&&a.onClick(p)){p.originalEvent&&L.DomEvent.stop(p.originalEvent);return}e.bindPopup(u,{maxWidth:a.popupMaxWidth||320,maxHeight:340,autoPan:!1}).openPopup(p.latlng)})}function _at2BindMarker(e,t,n,a,s){const i=o=>_at2Dir&&!_at2Dir.result&&a?(o&&o.originalEvent&&L.DomEvent.stop(o.originalEvent),_at2DirPickPin(a,e),!0):a&&_at2MarkerPlaceAction(a,s,n)?(o&&o.originalEvent&&L.DomEvent.stop(o.originalEvent),!0):!1;_at2Coarse?e.on("click",o=>{i(o)||_at2OpenSheet(t)}):(e.bindPopup(t,{maxWidth:320,maxHeight:340,autoPan:!1}),e.off("click"),e.on("click",o=>{if(i(o)){e.closePopup();return}e.openPopup()}))}function _at2PinCardHtml(e,t){const n=e.area_map_id?`<button type="button" class="at2-pop-drill-in" data-at2-go="${e.area_map_id}">Enter ${escHtml(e.name)} &#9656;</button>`:"",a=e.snippet?linkifyText(escHtml(e.snippet),allEntities,e.entity_id):"",s=e.map_pin_hidden?1:0,i=_at2IsGm(),o=i?(allEntities||[]).find(m=>m.id===e.entity_id):null,r=o&&o.ledger&&o.ledger.gm_note,c=r?`<div class="at2-pop-gmnote" title="GM-only: players never see this"><span class="at2-pop-gmnote-tag">GM</span> ${linkifyText(escHtml(r),allEntities,e.entity_id)}</div>`:"";let l="";if(i){const m=(_at2Placements[e.entity_id]||[]).filter(f=>f!==_at2MapId).map(f=>{const g=_at2MapById(f);return g?g.title||_at2PlateLabel(g):null}).filter(Boolean);m.length&&(l=`<div class="at2-pop-alsoon">Also on ${escHtml(_at2JoinNames(m))}</div>`)}const d=i?`<span class="at2-pop-title-row"><b class="at2-pop-title at2-pop-title--editable" data-at2-rename-inline="pin" data-at2-rename-target="${e.entity_id}" title="Click to rename">${escHtml(e.name)}</b><button type="button" class="at2-pop-title-pen" data-at2-rename-inline-trigger title="Rename" aria-label="Rename">&#9998;</button></span>`:`<b class="at2-pop-title">${escHtml(e.name)}</b>`,p=i?`<div class="at2-pop-toolbar"><button type="button" class="at2-pop-chip" data-at2-partyhere="${e.entity_id}" data-x="${e.x}" data-y="${e.y}" title="Move the party to ${escHtml(e.name)}" aria-label="Move the party here">${_MAP_ACT_ICO.partyHere}</button><button type="button" class="at2-pop-chip${s?" is-on":""}" data-at2-hide="${e.entity_id}" data-hidden="${s}" title="${s?"Reveal to players":"Hide from players"}" aria-label="${s?"Reveal to players":"Hide from players"}">${s?_MAP_ACT_ICO.eyeSlash:_MAP_ACT_ICO.eye}</button><span class="at2-pop-chip-pipe" aria-hidden="true"></span><button type="button" class="at2-pop-chip${e.is_port?" is-on":""}" data-at2-portpin="${e.entity_id}" data-on="${e.is_port?1:0}" title="${e.is_port?"Remove port: boats can no longer board here":"Mark as a port: water routes can board here (a dock)"}" aria-label="${e.is_port?"Remove port flag":"Mark as a port"}">${_MAP_ACT_ICO.dock}</button><button type="button" class="at2-pop-chip" data-at2-gmnote="${e.entity_id}" title="${r?"Edit GM-only note":"Add a GM-only note for this place"}" aria-label="${r?"Edit GM note":"Add GM note"}"><svg class="at2-tb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 21v-2.5L17 6.5l2.5 2.5L7.5 21z"/><path d="M14.5 9l2.5 2.5"/></svg></button>`+(e.area_map_id?`<button type="button" class="at2-pop-chip" data-at2-addlevel="${e.entity_id}
3487" data-name="${escHtml(e.name)}" title="Add another map for ${escHtml(e.name)}: a level, floor or wing (it joins the level picker)" aria-label="Add a level">${_MAP_ACT_ICO.levelAdd}</button>`:`<button type="button" class="at2-pop-chip" data-at2-ownmap="${e.entity_id}" data-name="${escHtml(e.name)}" title="Upload an image and give ${escHtml(e.name)} its own map" aria-label="Give its own map">${_MAP_ACT_ICO.drill}</button>`)+`<button type="button" class="at2-pop-chip at2-pop-chip--danger" data-at2-del="${e.entity_id}" data-at2-confirm="Delete ${escHtml(e.name)}?" title="Delete this place" aria-label="Delete">${_MAP_ACT_ICO.trash}</button></div>`:"";return`<div class="at2-pop at2-pop--toolbar">${d}${a?`<div class="at2-pop-sub">${a}</div>`:""}${c}${l}${n}<a class="q2-ch-link" href="#codex/${t}/${e.entity_id}">Open in Codex &#8594;</a>${p}</div>`}function _at2OpenSheet(e){_at2CloseSheet();const t=document.createElement("div");t.className="at2-sheet-wrap"+(_at2Editing?" at2-editing":""),t.id="at2-sheet",t.innerHTML=`<div class="at2-sheet-scrim"></div><div class="at2-sheet" role="dialog"><div class="at2-sheet-handle"></div><div class="at2-sheet-body">${e}</div></div>`,document.body.appendChild(t),requestAnimationFrame(()=>t.classList.add("open")),t.querySelector(".at2-sheet-scrim").addEventListener("click",_at2CloseSheet),t.querySelector(".at2-sheet-body").addEventListener("click",n=>{const a=n.target.closest("[data-at2-go]");if(a){n.preventDefault(),_at2CloseSheet(),_at2Go(parseInt(a.getAttribute("data-at2-go")));return}if(n.target.closest("[data-at2-setscale]")){n.preventDefault(),_at2CloseSheet(),typeof _at2ToggleScale=="function"&&_at2ToggleScale();return}const i=n.target.closest("[data-at2-roadterrain]");if(i){n.preventDefault(),_at2CloseSheet(),_at2ToggleRoadTerrain(parseInt(i.getAttribute("data-at2-roadterrain")));return}const o=n.target.closest("[data-at2-roadname]");if(o){n.preventDefault(),_at2CloseSheet(),_at2NameRoad(parseInt(o.getAttribute("data-at2-roadname")));return}const r=n.target.closest("[data-at2-roadshape]");if(r){n.preventDefault(),_at2CloseSheet(),_at2StartRouteEdit(parseInt(r.getAttribute("data-at2-roadshape")));return}const c=n.target.closest("[data-at2-roadcenter]");if(c){n.preventDefault(),_at2CloseSheet(),_at2CenterRoadOnArt(parseInt(c.getAttribute("data-at2-roadcenter")));return}const l=n.target.closest("[data-at2-roadtype]");if(l){n.preventDefault(),_at2CloseSheet(),_at2ToggleRoadType(parseInt(l.getAttribute("data-at2-roadtype")));return}const d=n.target.closest("[data-at2-roadhide]");if(d){n.preventDefault(),_at2CloseSheet(),_at2ToggleRoadHidden(parseInt(d.getAttribute("data-at2-roadhide")));return}const u=n.target.closest("[data-at2-roaddel]");if(u){n.preventDefault(),_at2CloseSheet(),_at2DeleteRoad(parseInt(u.getAttribute("data-at2-roaddel")));return}const p=n.target.closest("[data-at2-roaddiffsec]");if(p){n.preventDefault(),_at2StartDiffSection(parseInt(p.getAttribute("data-at2-roaddiffsec")));return}const m=n.target.closest("[data-at2-roaddiffclear]");if(m){n.preventDefault(),_at2CloseSheet(),_at2ClearDiffSections(parseInt(m.getAttribute("data-at2-roaddiffclear")));return}const f=n.target.closest("[data-at2-roadport]");if(f){n.preventDefault(),_at2StartPort(parseInt(f.getAttribute("data-at2-roadport")));return}const g=n.target.closest("[data-at2-roadportclear]");if(g){n.preventDefault(),_at2CloseSheet(),_at2ClearPorts(parseInt(g.getAttribute("data-at2-roadportclear")));return}const h=n.target.closest("[data-at2-hide]");if(h){n.preventDefault(),_at2ToggleHide(h);return}const v=n.target.closest("[data-at2-portpin]");if(v){n.preventDefault(),_at2TogglePort(parseInt(v.getAttribute("data-at2-portpin")));return}const b=n.target.closest("[data-at2-partyhere]");if(b){n.preventDefault(),_at2PartyToPin(parseInt(b.getAttribute("data-at2-partyhere")),parseFloat(b.getAttribute("data-x")),parseFloat(b.getAttribute("data-y")));return}const y=n.target.closest("[data-at2-gmnote]");if(y){n.preventDefault(),_at2EditGmNote(parseInt(y.getAttribute("data-at2-gmnote")));return}const _=n.target.closest("[data-at2-rename]");if(_){n.preventDefault(),_at2RenamePin(parseInt(_.getAttribute("data-at2-rename")));return}const w=n.target.closest("[data-at2-ownmap]");if(w){n.preventDefault(),_at2GiveOwnMap(parseInt(w.getAttribute("data-at2-ownmap")),w.getAttribute("data-name"));return}const $=n.target.closest("[data-at2-del]");
3487if($){n.preventDefault(),_at2DeletePin(parseInt($.getAttribute("data-at2-del")));return}const E=n.target.closest("[data-at2-encedit]");if(E){n.preventDefault(),_at2CloseSheet(),_at2OpenEncModal({enc:_at2EncById(parseInt(E.getAttribute("data-at2-encedit")))});return}const k=n.target.closest("[data-at2-encmove]");if(k){n.preventDefault(),_at2CloseSheet(),_at2StartPlace({kind:"enc-move",encId:parseInt(k.getAttribute("data-at2-encmove"))});return}const C=n.target.closest("[data-at2-enchide]");if(C){n.preventDefault(),_at2CloseSheet(),_at2ToggleEncHidden(parseInt(C.getAttribute("data-at2-enchide")));return}const S=n.target.closest("[data-at2-encdel]");if(S){n.preventDefault(),_at2CloseSheet(),_at2DeleteEnc(parseInt(S.getAttribute("data-at2-encdel")));return}const q=n.target.closest("[data-at2-portalgo]");if(q){n.preventDefault(),_at2CloseSheet(),_at2PortalGo(parseInt(q.getAttribute("data-at2-portalgo")));return}const I=n.target.closest("[data-at2-portalmove]");if(I){n.preventDefault(),_at2CloseSheet(),_at2StartPlace({kind:"portal-move",portalId:parseInt(I.getAttribute("data-at2-portalmove"))});return}const T=n.target.closest("[data-at2-portaldel]");if(T){n.preventDefault(),_at2CloseSheet(),_at2DeletePortal(parseInt(T.getAttribute("data-at2-portaldel")));return}const P=n.target.closest("[data-at2-addlevel]");if(P){n.preventDefault(),_at2CloseSheet(),_at2GiveOwnMap(parseInt(P.getAttribute("data-at2-addlevel")),P.getAttribute("data-name"),{level:!0});return}n.target.closest("a")&&_at2CloseSheet()})}function _at2CloseSheet(){const e=document.getElementById("at2-sheet");e&&(e.classList.remove("open"),setTimeout(()=>e.remove(),240))}function _at2ShowPlaceBanner(){if(!_at2PlaceMode)return;const e=_atlas2Panel(),t=e.querySelector("#at2-place-banner");t&&(t.innerHTML=(_at2PlaceMode.kind==="party"?"Tap the map where the party stands now":_at2PlaceMode.kind==="encounter"?"Tap the map where the encounter happened":_at2PlaceMode.kind==="enc-move"?"Tap the map where this encounter belongs":_at2PlaceMode.kind==="portal"?(()=>{const n=_at2AdjacentLevel(_at2MapId,_at2PlaceMode.pkind==="exit"?1:-1);return _at2PlaceMode.pkind==="entry"?`Tap where the party arrives${n?" from <b>"+escHtml(_at2LevelLabel(n))+"</b>":""}`:`Tap where the party leaves${n?" for <b>"+escHtml(_at2LevelLabel(n))+"</b>":""}`})():_at2PlaceMode.kind==="portal-move"?"Tap the map where this mark belongs":`Tap the map to chart <b>${escHtml(_at2PlaceMode.name||"this place")}</b>`)+' &nbsp; <button type="button" class="at2-place-cancel">Cancel</button>',t.classList.remove("hidden"),t.querySelector(".at2-place-cancel").addEventListener("click",_at2CancelPlace)),e.querySelector("#at2-map")?.classList.add("at2-placing")}function _at2StartPlace(e){typeof _at2DirCancel=="function"&&_at2Dir&&_at2DirCancel(),_at2PlaceMode=e,_at2CloseDrawer(),_at2ShowPlaceBanner();const t=_atlas2Panel();t.querySelectorAll(".at2-tray-chip--armed").forEach(n=>n.classList.remove("at2-tray-chip--armed")),e&&e.kind==="pin"&&e.entityId&&t.querySelector(`.at2-tray-chip[data-at2-chip="${e.entityId}"]`)?.classList.add("at2-tray-chip--armed"),t.querySelector(".at2-plate")?.scrollIntoView({behavior:"smooth",block:"center"}),_at2SyncAuthoringView()}function _at2MarkerPlaceAction(e,t,n){if(_at2ScaleMode&&e)return _at2ScaleAddPoint({x:e.x,y:e.y}),!0;if(_at2DiffSecMode&&e)return _at2DiffSecAddPoint({x:e.x,y:e.y}),!0;if(_at2PortMode&&e)return _at2PortAddPoint({x:e.x,y:e.y,entityId:e.entity_id}),!0;if(_at2RouteEdit&&_at2RouteEdit.extend&&e){const a={x:e.x,y:e.y,entityId:e.entity_id};return _at2RouteEdit.extend==="start"?_at2RouteEdit.wps.unshift(a):_at2RouteEdit.wps.push(a),_at2RouteEdit.extend=null,_at2RouteEdit.dirty=!0,_at2RouteEdit._hoverSnap=null,_at2RouteEditRedraw(),_at2RouteEditBanner(),!0}if(_at2DrawMode&&e)return _at2DrawMode.points.push({x:e.x,y:e.y,entityId:e.entity_id}),_at2DrawMode._hoverSnap=null,_at2DrawPreview(),_at2DrawBanner(),_at2DrawMode.points.length>=2&&_at2DrawFinish(),!0;if(!_at2PlaceMode||_at2PlaceMode.kind!=="party"||!e)return!1;if(e.area_map_id){const a=_at2PlaceMode;return _at2Go(e.area_map_id),_at2PlaceMode=a,_at2SyncAuthoringView(),!0}return _at2CancelPlace(),(async()=>{try{await api("POST",`/api/campaigns/${t}/party-positions`,{map_x:e.x,map_y:e.y,map_id:_at2MapId,location_entity_id:e.entity_id,source:"manual"}),typeof showToast=="function"&&showToast(`The party is at ${e.name}.`),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}})(),!0}function _at2CancelPlace(){_at2PlaceMode=null;const e=_atlas2Panel();e.querySelector("#at2-place-banner")?.classList.add("hidden"),e.querySelector("#at2-map")?.classList.remove("at2-placing"),e.querySelectorAll(".at2-tray-chip--armed").forEach(t=>t.classList.remove("at2-tray-chip--armed")),_at2SyncAuthoringView()}function _at2SyncAuthoringView(){const e=!!(_at2PlaceMode||_at2DrawMode||_at2ScaleMode||typeof _at2PortMode<"u"&&_at2PortMode||typeof _at2DiffSecMode<"u"&&_at2DiffSecMode||typeof _at2RouteEdit<"u"&&_at2RouteEdit);e!==_at2Editing&&(_at2Editing=e,_atlas2Panel()?.classList.toggle("at2-editing",e))}function _at2SetEditing(e){if(!e){_at2PlaceMode&&_at2CancelPlace(),_at2DrawMode&&typeof _at2DrawCancel=="function"&&_at2DrawCancel(),_at2ScaleMode&&typeof _at2ScaleCancel=="function"&&_at2ScaleCancel(),_at2SyncAuthoringView();return}const t=_atlas2Panel();t&&(t.querySelector("#at2-wb-body")?.classList.contains("at2-wb-collapsed")&&_at2WbToggle(!0),t.querySelector("#at2-workbench")?.scrollIntoView({behavior:"smooth",block:"center"}))}function _at2WbToggle(e,t){const n=_atlas2Panel();if(!n)return;const a=n.querySelector("#at2-wb-body");if(!a)return;const s=typeof e=="boolean"?e:a.classList.contains("at2-wb-collapsed");a.classList.toggle("at2-wb-collapsed",!s);const i=n.querySelector("#at2-wb-toggle");if(i){i.setAttribute("aria-expanded",s?"true":"false");const o=i.querySelector(".at2-wb-toggle-txt");o&&(o.textContent=s?"Hide":"Show")}if(t!==!1)try{localStorage.setItem("tf_at2_wb_open",s?"1":"0")}catch{}}function _at2WbRestore(){const e=_atlas2Panel();if(!e||!e.querySelector("#at2-wb-body"))return;let t=null;try{const a=localStorage.getItem("tf_at2_wb_open");t=a===null?null:a==="1"}catch{t=null}const n=t!==null;if(!n){const a=document.documentElement.clientWidth||window.innerWidth||0;t=!(a&&a<=900)}_at2WbToggle(t,n)}function _at2SetPrepLens(e){_at2PrepLens=!!e;const t=_atlas2Panel();t&&t.classList.toggle("at2-preplens",_at2PrepLens);const n=t&&t.querySelector("#at2-preplens-btn");n&&n.setAttribute("aria-pressed",_at2PrepLens?"true":"false")}async function _at2OnMapClick(e,t,n){if(_at2Dir&&!_at2Dir.result){_at2DirPickPoint(e);return}if(_at2ScaleMode){_at2ScaleAddPoint(e);return}
3487if(_at2DiffSecMode){_at2DiffSecAddPoint(e);return}if(_at2PortMode){_at2PortAddPoint(e);return}if(_at2RouteEdit&&_at2RouteEdit.extend){_at2RouteEditAddPoint(e);return}if(_at2DrawMode){_at2DrawAddPoint(e,t,n);return}if(!_at2PlaceMode)return;const a=_at2PlaceMode;_at2CancelPlace();try{if(a.kind==="pin")await api("POST",`/api/campaigns/${t}/maps/${n.id}/placements`,{entity_id:a.entityId,x:e.x,y:e.y});else if(a.kind==="encounter"){_at2OpenEncModal({x:e.x,y:e.y});return}else if(a.kind==="enc-move")await api("PATCH",`/api/campaigns/${t}/maps/${n.id}/encounters/${a.encId}`,{x:e.x,y:e.y}),typeof showToast=="function"&&showToast("Encounter moved.");else if(a.kind==="portal")await api("POST",`/api/campaigns/${t}/maps/${n.id}/portals`,{x:e.x,y:e.y,kind:a.pkind==="entry"?"entry":"exit"}),typeof showToast=="function"&&showToast(a.pkind==="entry"?"Entrance marked.":"Exit marked.");else if(a.kind==="portal-move")await api("PATCH",`/api/campaigns/${t}/maps/${n.id}/portals/${a.portalId}`,{x:e.x,y:e.y}),typeof showToast=="function"&&showToast("Mark moved.");else if(a.kind==="party")await api("POST",`/api/campaigns/${t}/party-positions`,{map_x:e.x,map_y:e.y,map_id:n.id,source:"manual",label:a.label||null}),typeof showToast=="function"&&showToast("The party has moved.");else if(a.kind==="new"){let s=(a.name||"").trim();if(!s){const c=await confirmDialog({title:"Add a place",message:"Name this place: it\u2019s charted here (and reuses an existing place if the name already matches one).",confirmLabel:"Add",cancelLabel:"Cancel",danger:!1,textInput:{label:"Name",placeholder:"Place name",rows:1,maxlength:120}});if(!c||c.confirmed===!1)return;s=((c.text!=null?c.text:c.value)||"").trim()}if(!s)return;const i=await api("POST",`/api/campaigns/${t}/replace-locations`,{locations:[{name:s,x:e.x,y:e.y}],map_id:n.id});[...i&&i.created||[],...i&&i.updated||[]].forEach(c=>{c&&c.id&&!allEntities.some(l=>l.id===c.id)&&allEntities.push(c)});const r=i&&i.updated&&i.updated.length;typeof showToast=="function"&&showToast(r?`Charted ${s}, merged with the existing place.`:`Charted ${s}.`)}if(await renderAtlas2({cid:t,mapId:n.id}),a.kind==="pin"){const s=(_at2Inbox&&_at2Inbox.items||[]).find(i=>i.entity_id!==a.entityId);s?(_at2StartPlace({kind:"pin",entityId:s.entity_id,name:s.name}),typeof showToast=="function"&&showToast(`Charted ${a.name||"it"}. Next: ${s.name}, Esc to stop.`)):typeof showToast=="function"&&showToast(`Charted ${a.name||"the place"}.`)}}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s.message||"try again"))}}let _at2Dir=null,_at2DirRouteLayers=[],_at2DirCampMarkers=[],_at2DirAltLayers=[];function _at2DirEl(e){return _atlas2Panel()?.querySelector("#"+e)||document.getElementById(e)}function _at2DirRoadsForGraph(){return(_at2Roads||[]).filter(e=>!e.encounter_id).map(e=>{let t=e.waypoints;if(typeof t=="string")try{t=JSON.parse(t)}catch{t=[]}return{id:e.id,route_type:e.route_type,terrain:e.terrain||"road",label:e.label||null,hidden:!!e.hidden,diff_sections:e.diff_sections||null,ports:e.ports||null,waypoints:Array.isArray(t)?t:[]}})}function _at2DirClearRouteLayers(){if((_at2DirRouteLayers||[]).forEach(e=>{try{e.remove()}catch{}}),_at2DirRouteLayers=[],(_at2DirAltLayers||[]).forEach(e=>{try{e.remove()}catch{}}),_at2DirAltLayers=[],(_at2DirCampMarkers||[]).forEach(e=>{try{e.remove()}catch{}}),_at2DirCampMarkers=[],_at2Leaflet){_at2Leaflet.__atDirRouteSegs=[],_at2Leaflet.__atDirAltSegs=[];try{_diffHatchCanvasDraw(_at2Leaflet)}catch{}}}function _at2DirReset(){_at2DirClearRouteLayers(),_at2Dir&&(_at2Dir.fromEl&&_at2Dir.fromEl.classList.remove("tf-pin--route-endpoint"),_at2Dir.toEl&&_at2Dir.toEl.classList.remove("tf-pin--route-endpoint")),_at2Dir=null;const e=_atlas2Panel();e?.querySelector("#at2-map")?.classList.remove("is-routing","is-dir-arming"),e?.querySelector("#at2-dir-banner")?.classList.add("hidden"),e?.querySelector("#at2-dir-card")?.classList.add("hidden"),e?.querySelector("#at2-dir-btn")?.classList.remove("at2-dir-btn--armed")}function _at2DirCancel(){_at2DirReset()}function _at2DirStart(){if(!_at2Leaflet||!_isGm())return;typeof _at2CancelPlace=="function"&&_at2CancelPlace(),_at2DrawMode&&typeof _at2DrawCancel=="function"&&_at2DrawCancel(),_at2DirReset(),_at2Dir={from:null,to:null,fromEl:null,toEl:null,result:null},_at2DirSetBanner("Tap your start, a place or any point");const e=_atlas2Panel();e?.querySelector("#at2-dir-banner")?.classList.remove("hidden"),e?.querySelector("#at2-map")?.classList.add("is-dir-arming"),e?.querySelector("#at2-dir-btn")?.classList.add("at2-dir-btn--armed")}function _at2DirSetBanner(e){const t=_at2DirEl("at2-dir-banner");if(!t)return;const n=_at2Dir&&_at2Dir.from?`<span class="live-dir-chip" title="Start">${escHtml(_at2Dir.from.name)}</span>`:"";t.innerHTML=`<span class="live-dir-banner-glyph">&#10547;</span><span class="live-dir-banner-text">${escHtml(e)}</span>${n}<button type="button" class="live-dir-banner-cancel" data-at2dir-cancel>Cancel</button>`}function _at2DirMarkEndpoint(e){const t=e&&e.getElement&&e.getElement(),n=t&&t.querySelector(".tf-pin");return n&&n.classList.add("tf-pin--route-endpoint"),n||null}function _at2DirPickPin(e,t){if(!_at2Dir||_at2Dir.result)return!1;
3487const n=e.x!=null?e.x:e.map_x,a=e.y!=null?e.y:e.map_y;if(n==null||a==null)return!1;const s={key:e.entity_id||e.id,x:n,y:a,name:e.name,kind:_dirKindOf(e)};if(!_at2Dir.from)_at2Dir.from=s,_at2Dir.fromEl=_at2DirMarkEndpoint(t),_at2DirSetBanner("Tap your destination");else{if(s.key===_at2Dir.from.key)return!0;_at2Dir.to=s,_at2Dir.toEl=_at2DirMarkEndpoint(t),_at2DirCompute()}return!0}function _at2DirPickPoint(e){if(!_at2Dir||_at2Dir.result||!e)return;const t=_at2CurrentGaz||{},n=typeof _at2IsGm=="function"?_at2IsGm():!1,a=Math.sqrt((_at2ImgW||1e3)**2+(_at2ImgH||1e3)**2),s=a*.03;let i=null,o=s;(t.locations||[]).forEach(c=>{if(c.x==null||c.y==null||c.map_pin_hidden&&!n)return;const l=Math.hypot(c.x-e.x,c.y-e.y);l<o&&(o=l,i=c)});const r=i?{key:i.entity_id,x:i.x,y:i.y,name:i.name,kind:_dirKindOf(i)}:{key:"pt:"+Math.round(e.x)+":"+Math.round(e.y),x:e.x,y:e.y,name:"Map point",kind:""};if(!_at2Dir.from)_at2Dir.from=r,_at2DirSetBanner("Tap your destination");else{if(r.key===_at2Dir.from.key)return;_at2Dir.to=r,_at2DirCompute()}}function _at2DirSnapDist(){const e=_at2ImgW||1e3,t=_at2ImgH||1e3;return Math.sqrt(e*e+t*t)*.05}function _at2DirSnapToGraph(e,t){if(e&&_at2IsOffroadKind(e.kind))return{x:e.x,y:e.y};let n=null,a=_at2DirSnapDist();return(t||[]).forEach(s=>(s.waypoints||[]).forEach(i=>{const o=Math.hypot(i.x-e.x,i.y-e.y);o<a&&(a=o,n={x:i.x,y:i.y})})),n||{x:e.x,y:e.y}}function _at2DirCompute(){const e=window.AtlasRoutes;if(!e||!_at2Dir||!_at2Dir.from||!_at2Dir.to)return;const t=typeof _at2MapById=="function"&&_at2MapById(_at2MapId)||{},n=_at2DirRoadsForGraph().filter(l=>_at2Dir.useHidden||!l.hidden);_at2Dir.mode||(_at2Dir.mode=_liveDirDefaultMode(t)),_at2Dir.watermode||(_at2Dir.watermode=_liveDirDefaultWaterMode(t)),_at2Dir.pace||(_at2Dir.pace="normal");const a=_at2DirSnapToGraph(_at2Dir.from,n),s=_at2DirSnapToGraph(_at2Dir.to,n),i=[{key:_at2Dir.from.key,x:a.x,y:a.y},{key:_at2Dir.to.key,x:s.x,y:s.y}],o=_at2DirPortPins(),r=e.planRoute(n,i,_at2Dir.from.key,_at2Dir.to.key,["land","water"],{portPins:o});if(_at2DirEl("at2-dir-banner")?.classList.add("hidden"),r){_at2Dir.crow=!1,_at2Dir.result=r,_at2Dir.altResult=_dirPickAlternate(e,n,i,_at2Dir.from.key,_at2Dir.to.key,r,o),_at2DirShowCard(_dirCardHtml(r,t,_at2Dir,"at2dir",_at2DirRailCtx())),_at2DirDrawRoute(r.points,r.legs);return}const c=e.planRouteFrom(n,[{key:_at2Dir.to.key,x:_at2Dir.to.x,y:_at2Dir.to.y}],{x:_at2Dir.from.x,y:_at2Dir.from.y},_at2Dir.to.key,["land","water"],{portPins:o});if(c){_at2Dir.crow=(c.connectorLenPx||0)>.5,_at2Dir.result=c,_at2DirShowCard(_dirCardHtml(c,t,_at2Dir,"at2dir",_at2DirRailCtx())),_at2DirDrawRoute(c.points,c.legs);return}_at2Dir.crow=!1,_at2Dir.result={none:!0},_at2DirShowCard(_dirNoRouteHtml(_at2Dir,"at2dir"))}function _at2DirReverse(){if(!_at2Dir||!_at2Dir.from||!_at2Dir.to)return;const e=_at2Dir.to,t=_at2Dir.from,n=_at2Dir.toEl,a=_at2Dir.fromEl,s=_at2Dir.mode,i=_at2Dir.pace,o=_at2Dir.watermode;_at2DirClearRouteLayers(),_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("is-routing"),_at2Dir={from:e,to:t,fromEl:n,toEl:a,result:null,mode:s,pace:i,watermode:o},_at2DirCompute()}function _at2DirRailCtx(){const t=((_at2CurrentGaz||{}).locations||[]).map(a=>({x:a.x,y:a.y,name:a.name,hidden:!!a.map_pin_hidden,kind:_dirKindOf(a)})),n={};return(typeof _at2ParsedRoads=="function"?_at2ParsedRoads():[]).forEach(a=>{const s=a.label&&String(a.label).trim();s&&(n[a.id]=s)}),{locations:t,imgW:_at2ImgW,imgH:_at2ImgH,isGm:typeof _at2IsGm=="function"?_at2IsGm():!1,roadLabels:n,hasHiddenRoads:(_at2Roads||[]).some(a=>a.hidden)}}function _at2DirPortPins(){return((_at2CurrentGaz||_at2Gaz||{}).locations||[]).filter(e=>e.is_port).map(e=>({x:e.x,y:e.y}))}function _at2DirShowCard(e){const t=_at2DirEl("at2-dir-card");t&&(t.innerHTML=e,t.classList.remove("hidden"))}function _at2DirDrawRoute(e,t){_at2DirClearRouteLayers();const n=_at2Leaflet;if(!n||!Array.isArray(e)||e.length<2)return;const a=n.__at2ToLL||((o,r)=>L.latLng((_at2ImgH||1e3)-r,o));_at2DirDrawAlt();const s=e.map(o=>a(o.x,o.y)),i=[L.polyline(s,{className:"tf-route-line tf-route-line--casing",interactive:!1}).addTo(n)];Array.isArray(t)&&t.length?t.forEach(o=>{const r=o.type==="connector"?"tf-route-line tf-route-line--crow":o.type==="water"?"tf-route-line tf-route-line--core-water":"tf-route-line tf-route-line--core";i.push(L.polyline([a(o.a.x,o.a.y),a(o.b.x,o.b.y)],{className:r,interactive:!1}).addTo(n))}):i.push(L.polyline(s,{className:"tf-route-line tf-route-line--core",interactive:!1}).addTo(n)),Array.isArray(t)&&t.forEach(o=>{if(o.type==="water"||!o.diffLen||o.diffLen<1)return;const r=o.len>0?Math.min(o.diffLen,o.len)/o.len:0;r<.02||_at2DirRouteTicks([o.a,{x:o.a.x+(o.b.x-o.a.x)*r,y:o.a.y+(o.b.y-o.a.y)*r}],a,i,n)}),_at2DirRouteLayers=i,_atlas2Panel()?.querySelector("#at2-map")?.classList.add("is-routing");try{_diffHatchCanvasDraw(n)}catch{}try{n.fitBounds(i[0].getBounds(),{padding:L.point(28,80)})}catch{}_at2DirDrawCamps()}
3487function _at2DirDrawAlt(){(_at2DirAltLayers||[]).forEach(r=>{try{r.remove()}catch{}}),_at2DirAltLayers=[];const e=_at2Leaflet,t=_at2Dir&&_at2Dir.altResult;if(!e||!t||!Array.isArray(t.points)||t.points.length<2)return;const n=e.__at2ToLL||((r,c)=>L.latLng((_at2ImgH||1e3)-c,r)),a=t.points.map(r=>n(r.x,r.y)),s=L.polyline(a,{className:"tf-route-line tf-route-line--alt-hit",interactive:!0,bubblingMouseEvents:!1,opacity:0}).addTo(e);s.bindTooltip("Alternate route, tap to use",{direction:"top",sticky:!0,className:"tf-route-alt-tip"}),s.on("click",_at2DirPromoteAlt);const i=[s],o=t.legs||[];o.length?(o.forEach(r=>{const c=r.type==="water"?"tf-route-line tf-route-line--alt-water":r.type==="connector"?"tf-route-line tf-route-line--alt-rough":"tf-route-line tf-route-line--alt";i.push(L.polyline([n(r.a.x,r.a.y),n(r.b.x,r.b.y)],{className:c,interactive:!1}).addTo(e))}),o.forEach(r=>{if(r.type==="water"||!r.diffLen||r.diffLen<1)return;const c=r.len>0?Math.min(r.diffLen,r.len)/r.len:0;c<.02||_at2DirAltTicks([r.a,{x:r.a.x+(r.b.x-r.a.x)*c,y:r.a.y+(r.b.y-r.a.y)*c}],n,i,e)})):i.push(L.polyline(a,{className:"tf-route-line tf-route-line--alt",interactive:!1}).addTo(e)),_at2DirAltLayers=i}function _at2DirAltTicks(e,t,n,a){const s=a||_at2Leaflet;!s||!Array.isArray(e)||e.length<2||(s.__atDirAltSegs=s.__atDirAltSegs||[],s.__atDirAltSegs.push({pts:e.slice()}))}function _at2DirPromoteAlt(){if(!_at2Dir||!_at2Dir.altResult)return;const e=window.AtlasRoutes;if(!e)return;const t=typeof _at2MapById=="function"&&_at2MapById(_at2MapId)||{},n=_at2DirRoadsForGraph().filter(o=>_at2Dir.useHidden||!o.hidden),a=_at2DirSnapToGraph(_at2Dir.from,n),s=_at2DirSnapToGraph(_at2Dir.to,n),i=[{key:_at2Dir.from.key,x:a.x,y:a.y},{key:_at2Dir.to.key,x:s.x,y:s.y}];_at2Dir.result=_at2Dir.altResult,_at2Dir.crow=!1,_at2Dir.altResult=_dirPickAlternate(e,n,i,_at2Dir.from.key,_at2Dir.to.key,_at2Dir.result,_at2DirPortPins()),_at2DirShowCard(_dirCardHtml(_at2Dir.result,t,_at2Dir,"at2dir",_at2DirRailCtx())),_at2DirDrawRoute(_at2Dir.result.points,_at2Dir.result.legs)}function _at2DirDrawCamps(){(_at2DirCampMarkers||[]).forEach(s=>{try{s.remove()}catch{}}),_at2DirCampMarkers=[];const e=_at2Leaflet,t=_at2Dir&&_at2Dir.result;if(!e||!t||!Array.isArray(t.points)||t.points.length<2)return;const n=e.__at2ToLL||((s,i)=>L.latLng((_at2ImgH||1e3)-i,s)),a=typeof _at2MapById=="function"&&_at2MapById(_at2MapId)||{};_dirCampNodes(t,a,_at2Dir||{},_at2DirRailCtx()).forEach(s=>{const i=L.marker(n(s.x,s.y),{icon:L.divIcon({className:"",html:_dirCampMarkerHtml(s),iconSize:[28,28],iconAnchor:[14,14]}),interactive:!1,keyboard:!1,zIndexOffset:880}).addTo(e);_at2DirCampMarkers.push(i)})}function _at2DirRouteTicks(e,t,n,a){const s=a||_at2Leaflet;!s||!Array.isArray(e)||e.length<2||(s.__atDirRouteSegs=s.__atDirRouteSegs||[],s.__atDirRouteSegs.push({pts:e.slice()}))}(function(){document.addEventListener("click",t=>{const n=t.target.closest('a[href^="#codex/"], a[href^="#chronicle/"]');if(n&&n.closest("#tab-atlas2, .at2-sheet-wrap")){const s=n.getAttribute("href")||"",i=s.match(/^#codex\/\d+\/(\d+)$/),o=s.match(/^#chronicle\/\d+\/(\d+)$/);if(i||o){t.preventDefault(),typeof _at2CloseSheet=="function"&&_at2CloseSheet(),i&&typeof navigateToEntity=="function"?navigateToEntity(parseInt(i[1],10)):o&&typeof renderChronicleRecap=="function"&&renderChronicleRecap(parseInt(o[1],10));return}}if(t.target.closest("#at2-dir-btn")){_at2DirStart();return}if(t.target.closest("[data-at2dir-cancel]")){_at2DirCancel();return}if(t.target.closest("#at2-dir-card")){if(t.target.closest("[data-dir-promote-alt]")){_at2DirPromoteAlt();return}const s=t.target.closest("[data-at2dir-fear]");if(s){_dhDirAddFear(parseInt(s.getAttribute("data-at2dir-fear"),10));return}const i=t.target.closest("[data-at2dir-mode],[data-at2dir-pace],[data-at2dir-watermode]");if(i){if(!_at2Dir||!_at2Dir.result)return;const r=i.getAttribute("data-at2dir-mode"),c=i.getAttribute("data-at2dir-pace"),l=i.getAttribute("data-at2dir-watermode"),d=currentCampaign&&currentCampaign.id;if(r){_at2Dir.mode=r;try{d&&localStorage.setItem("tf_dir_mode_"+d,r)}catch{}}if(l){_at2Dir.watermode=l;try{d&&localStorage.setItem("tf_dir_watermode_"+d,l)}catch{}}c&&(_at2Dir.pace=c);const u=typeof _at2MapById=="function"&&_at2MapById(_at2MapId)||{};_at2DirShowCard(_dirCardHtml(_at2Dir.result,u,_at2Dir,"at2dir",_at2DirRailCtx())),_at2DirDrawCamps();return}const o=t.target.closest("[data-at2dir]")?.getAttribute("data-at2dir");o==="clear"?_at2DirCancel():o==="reverse"&&_at2DirReverse()}}),document.addEventListener("keydown",t=>{t.key==="Escape"&&_at2Dir&&_at2DirCancel()}),document.addEventListener("change",t=>{const n=t.target.closest("#at2-dir-card [data-at2dir-usehidden]");
3487n&&_at2Dir&&_at2Dir.from&&_at2Dir.to&&(_at2Dir.useHidden=!!n.checked,_at2Dir.result=null,_at2DirClearRouteLayers(),_at2DirCompute())})})();function _at2ToggleDrawRoute(){if(_at2DrawMode){_at2DrawCancel();return}_at2CancelPlace&&_at2CancelPlace(),_at2DrawMode={route_type:"land",points:[]},_at2SyncAuthoringView(),_atlas2Panel()?.querySelector("#at2-map")?.classList.add("at2-drawing"),_at2DrawBanner(),typeof showToast=="function"&&showToast("Drawing: tap to lay the path; tap a place or route to finish.")}function _at2DrawRoadPolys(e){return(_at2Roads||[]).map(t=>{let n=t.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}return n=Array.isArray(n)?n:[],{id:t.id,pts:n}}).filter(t=>t.pts.length>=2&&t.id!==e)}function _at2DrawSnap(e,t){const n=window.AtlasRoutes,a=10;let s=1;try{const c=_at2Leaflet;if(c&&c.__at2ToLL&&c.latLngToContainerPoint){const l=c.latLngToContainerPoint(c.__at2ToLL(0,0)),d=c.latLngToContainerPoint(c.__at2ToLL(100,0));s=Math.hypot(d.x-l.x,d.y-l.y)/100||1}}catch{}const i=a/s;let o=null,r=i;if((_at2Gaz&&_at2Gaz.locations||[]).forEach(c=>{const l=Math.hypot(c.x-e.x,c.y-e.y);l<r&&(r=l,o={x:c.x,y:c.y,entityId:c.entity_id,snap:"pin"})}),(_at2Encounters||[]).forEach(c=>{const l=Math.hypot(c.x-e.x,c.y-e.y);l<r&&(r=l,o={x:c.x,y:c.y,entityId:null,encounterId:c.id,snap:"enc"})}),o)return o;if(n){const c=n.nearestOnPolylines(e,_at2DrawRoadPolys(t),i);if(c)return{x:c.x,y:c.y,entityId:null,snap:"road"}}if(_at2ImgW&&_at2ImgH){const c=i*1.6,l=e.x,d=_at2ImgW-e.x,u=e.y,p=_at2ImgH-e.y,m=Math.min(l,d,u,p);if(m<c){let f=Math.max(0,Math.min(_at2ImgW,e.x)),g=Math.max(0,Math.min(_at2ImgH,e.y));return m===l?f=0:m===d?f=_at2ImgW:m===u?g=0:g=_at2ImgH,{x:f,y:g,entityId:null,snap:"edge"}}}return{x:e.x,y:e.y,entityId:null,snap:null}}function _at2DrawHover(e){if(!_at2DrawMode)return;const t=_at2DrawSnap(e);_at2DrawMode._hoverSnap=t,_at2DrawRedraw(t)}function _at2DrawHideHover(){_at2DrawMode&&(_at2DrawMode._hoverSnap=null),_at2DrawRedraw(null)}function _at2DrawAddPoint(e){if(!_at2DrawMode)return;const t=_at2DrawMode._hoverSnap||_at2DrawSnap(e),n=_at2DrawMode.points[_at2DrawMode.points.length-1];if(n&&Math.abs(n.x-t.x)<.75&&Math.abs(n.y-t.y)<.75){_at2DrawMode._hoverSnap=null;return}if(_at2DrawMode.points.push({x:t.x,y:t.y,entityId:t.entityId||null,encounterId:t.encounterId||null}),_at2DrawMode._hoverSnap=null,_at2DrawRedraw(null),_at2DrawBanner(),(t.snap==="pin"||t.snap==="enc"||t.snap==="road"||t.snap==="edge")&&_at2DrawMode.points.length>=2){_at2DrawFinish();return}}function _at2DrawPreview(){_at2DrawRedraw(null)}function _at2DrawRedraw(e){const t=_at2Leaflet;if(!t||!t.__at2ToLL||!_at2DrawMode)return;t.__at2DrawLayer?t.__at2DrawLayer.clearLayers():t.__at2DrawLayer=L.layerGroup().addTo(t);const n=t.__at2DrawLayer,a=t.__at2ToLL,s=window.AtlasRoutes,i=_at2DrawMode.points,o=e?i.concat([{x:e.x,y:e.y}]):i;
3487if(o.length>=2&&L.polyline(o.map(r=>a(r.x,r.y)),{className:"at2-draw-line at2-draw-line--"+_at2DrawMode.route_type,interactive:!1}).addTo(n),i.forEach(r=>{L.marker(a(r.x,r.y),{icon:L.divIcon({className:"at2-draw-dot-wrap",html:`<span class="at2-draw-dot${r.entityId?" at2-draw-dot--snap":""}"></span>`,iconSize:[0,0],iconAnchor:[5,5]}),keyboard:!1,interactive:!1,zIndexOffset:600}).addTo(n)}),e){const r="at2-draw-hover"+(e.snap?" at2-draw-hover--"+e.snap:"");L.marker(a(e.x,e.y),{icon:L.divIcon({className:"at2-draw-hover-wrap",html:`<span class="${r}"></span>`,iconSize:[0,0],iconAnchor:[7,7]}),keyboard:!1,interactive:!1,zIndexOffset:650}).addTo(n)}}function _at2DrawBanner(){const e=_atlas2Panel();if(!e)return;let t=e.querySelector("#at2-draw-banner");if(!_at2DrawMode){t&&t.remove();return}t||(t=document.createElement("div"),t.id="at2-draw-banner",t.className="at2-draw-banner",(e.querySelector(".at2-plate")||e).appendChild(t),t.addEventListener("click",r=>{const c=r.target.closest("[data-draw]");if(!c)return;const l=c.getAttribute("data-draw");l==="land"||l==="water"?(_at2DrawMode.route_type=l,_at2DrawPreview(),_at2DrawBanner()):l==="undo"?(_at2DrawMode.points.pop(),_at2DrawPreview(),_at2DrawBanner()):l==="name"?_at2NameDrawRoute():l==="min"?(_at2DrawMode._min=!_at2DrawMode._min,_at2DrawBanner()):l==="finish"?_at2DrawFinish():l==="cancel"&&_at2DrawCancel()}));const n=_at2DrawMode.route_type,a=_at2DrawMode.points.length,s=!!_at2DrawMode._min,i=(_at2DrawMode.label||"").trim();t.classList.toggle("at2-draw-banner--min",s);const o=`<div class="at2-draw-panel-hdr">&#9100; Draw route${s?` &middot; ${a} pt${a===1?"":"s"}`:""}<button type="button" class="at2-draw-min" data-draw="min" title="${s?"Expand the panel":"Minimize the panel (it covers the map)"}">${s?"&#9633;":"&minus;"}</button></div>`;if(s){t.innerHTML=o+`<div class="at2-draw-minrow"><button type="button" class="at2-draw-act at2-draw-act--go" data-draw="finish"${a>=2?"":" disabled"} title="Finish (Enter)">&#10003;</button><button type="button" class="at2-draw-act" data-draw="undo"${a?"":" disabled"} title="Undo point (Backspace)">&#8592;</button><button type="button" class="at2-draw-act at2-draw-act--cancel" data-draw="cancel" title="Cancel (Esc)">&#10005;</button></div>`;return}t.innerHTML=o+`<span class="at2-draw-seg"><button type="button" class="at2-draw-type${n==="land"?" is-on":""}" data-draw="land">&#9906; Land</button><button type="button" class="at2-draw-type${n==="water"?" is-on":""}" data-draw="water">&#9875; Water</button></span><span class="at2-draw-count">${a} point${a===1?"":"s"} laid</span><button type="button" class="at2-draw-act${i?" at2-draw-act--named":""}" data-draw="name" title="Name this route: adds it to the Codex">&#9998; ${i?escHtml(i.length>18?i.slice(0,17)+"\u2026":i):"Name route"}</button><button type="button" class="at2-draw-act" data-draw="undo"${a?"":" disabled"} title="Backspace">&#8592; Undo point</button><button type="button" class="at2-draw-act at2-draw-act--go" data-draw="finish"${a>=2?"":" disabled"} title="Enter">&#10003; Finish</button><button type="button" class="at2-draw-act at2-draw-act--cancel" data-draw="cancel" title="Esc">&#10005; Cancel</button>`+(n==="water"?'<div class="at2-draw-hint">Tap to lay points &middot; end on a place, road or edge &middot; add a dock (&#9875;) where the party can board.</div>':'<div class="at2-draw-hint">Tap to lay points &middot; end on a place, road or edge &middot; end on an encounter mark to draw its trail</div>')}async function _at2NameDrawRoute(){if(!_at2DrawMode)return;const e=await confirmDialog({title:"Name this route",message:"Optional, a named road becomes a location in your Codex (kind: Road).",confirmLabel:"Set name",cancelLabel:"Cancel",danger:!1,textInput:{placeholder:"e.g. The High Road",defaultValue:_at2DrawMode.label||"",rows:1,maxlength:80}});!e||!e.confirmed||!_at2DrawMode||(_at2DrawMode.label=(e.text||"").trim()||null,_at2DrawBanner())}async function _at2DrawFinish(){const e=_at2DrawMode,t=currentCampaign&&currentCampaign.id,n=_at2MapById(_at2MapId);if(!e||!t||!n||e._finishing)return;if(e.points.length<2){typeof showToast=="function"&&showToast("A route needs at least 2 points.");return}e._finishing=!0;const a=e.points[e.points.length-1].encounterId||e.points[0].encounterId||null,s={route_type:e.route_type,waypoints:e.points.map(i=>({x:i.x,y:i.y})),from_entity_id:e.points[0].entityId||null,to_entity_id:e.points[e.points.length-1].entityId||null,label:(e.label||"").trim()||null,encounter_id:a};try{await api("POST",`/api/campaigns/${t}/maps/${n.id}/roads`,s),_at2DrawCancel(),typeof showToast=="function"&&showToast(a?"Trail drawn to the encounter.":"Route drawn."),await renderAtlas2({cid:t,mapId:n.id}),_at2SetView(0)}catch(i){e._finishing=!1,typeof showToast=="function"&&showToast("Failed: "+(i&&i.message||"try again"))}}function _at2DrawCancel(){_at2DrawMode=null,_at2SyncAuthoringView(),_at2Leaflet&&_at2Leaflet.__at2DrawLayer&&_at2Leaflet.__at2DrawLayer.clearLayers(),_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("at2-drawing"),_at2DrawBanner()}function _at2StartRouteEdit(e){const t=(_at2Roads||[]).find(a=>a.id===e);if(!t)return;let n=t.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}if(n=(Array.isArray(n)?n:[]).map(a=>({x:a.x,y:a.y,entityId:null})),n.length<2){typeof showToast=="function"&&showToast("This route has no editable shape.");return}n[0].entityId=t.from_entity_id||null,n[n.length-1].entityId=t.to_entity_id||null,_at2DrawMode&&_at2DrawCancel(),_at2RouteEdit={id:e,type:t.route_type==="water"?"water":"land",label:t.label||null,wps:n,extend:null,dirty:!1},_at2SyncAuthoringView(),_at2ViewMode!==0&&_at2SetView(0);try{const a=_at2Leaflet&&_at2Leaflet.__at2RoadLines&&_at2Leaflet.__at2RoadLines[e],s=_at2Leaflet&&_at2Leaflet.__at2RoadHits&&_at2Leaflet.__at2RoadHits[e];
3487[a,s].forEach(i=>{const o=i&&i.getElement&&i.getElement();o&&(o.style.display="none")})}catch{}_at2RouteEditCursor(),_at2RouteEditRedraw(),_at2RouteEditBanner(),typeof showToast=="function"&&showToast("Editing route: drag a node, or click an end to extend. Save when done.")}function _at2RouteEditCursor(){const e=_atlas2Panel()?.querySelector("#at2-map");e&&e.classList.toggle("at2-drawing",!!(_at2RouteEdit&&_at2RouteEdit.extend))}function _at2RouteEditToggleExtend(e){_at2RouteEdit&&(_at2RouteEdit.extend=_at2RouteEdit.extend===e?null:e,_at2RouteEdit._hoverSnap=null,_at2RouteEdit._extAdded=0,_at2RouteEditCursor(),_at2RouteEditBanner(),_at2RouteEditRedraw(),typeof showToast=="function"&&_at2RouteEdit.extend&&showToast("Click the map to extend, end on a place, road or edge. Esc to stop."))}function _at2RouteEditHover(e){const t=_at2RouteEdit;!t||!t.extend||(t._hoverSnap=_at2DrawSnap(e,t.id),_at2RouteEditRedraw())}function _at2RouteEditAddPoint(e){const t=_at2RouteEdit;if(!t||!t.extend)return;const n=t._hoverSnap||_at2DrawSnap(e,t.id),a={x:n.x,y:n.y,entityId:n.entityId||null};t.extend==="start"?t.wps.unshift(a):t.wps.push(a),t.dirty=!0,t._hoverSnap=null,t._extAdded=(t._extAdded||0)+1,(n.snap==="pin"||n.snap==="road"||n.snap==="edge")&&(t.extend=null,_at2RouteEditCursor()),_at2RouteEditRedraw(),_at2RouteEditBanner()}function _at2RouteEditInsert(e){const t=_at2RouteEdit;if(!t)return;const n=t.wps[e],a=t.wps[e+1];!n||!a||(t.wps.splice(e+1,0,{x:(n.x+a.x)/2,y:(n.y+a.y)/2,entityId:null}),t.dirty=!0,_at2RouteEditRedraw(),_at2RouteEditBanner())}function _at2RouteEditDeleteNode(e){const t=_at2RouteEdit;if(t){if(t.wps.length<=2){typeof showToast=="function"&&showToast("A route needs at least 2 points.");return}t.wps.splice(e,1),t.dirty=!0,t.extend=null,_at2RouteEditCursor(),_at2RouteEditRedraw(),_at2RouteEditBanner()}}function _at2RouteEditRedraw(){const e=_at2Leaflet;if(!e||!e.__at2ToLL||!e.__at2FromLL||!_at2RouteEdit)return;_at2RouteEditCursor(),e.__at2EditLayer?e.__at2EditLayer.clearLayers():e.__at2EditLayer=L.layerGroup().addTo(e);const t=e.__at2EditLayer,n=e.__at2ToLL,a=e.__at2FromLL,s=_at2RouteEdit,i=s.wps;let o=i.map(c=>({x:c.x,y:c.y}));const r=s.extend&&s._hoverSnap;if(r&&(s.extend==="start"?o=[{x:r.x,y:r.y}].concat(o):o=o.concat([{x:r.x,y:r.y}])),o.length>=2&&L.polyline(o.map(c=>n(c.x,c.y)),{className:"at2-draw-line at2-draw-line--"+s.type,interactive:!1}).addTo(t),s._line=null,i.forEach((c,l)=>{const d=l===0||l===i.length-1,u=l===0?"start":"end",p=d&&s.extend===u,m=L.marker(n(c.x,c.y),{icon:L.divIcon({className:"at2-edit-node-wrap",html:`<span class="at2-edit-node${d?" at2-edit-node--end":""}${p?" at2-edit-node--armed":""}"></span>`,iconSize:[0,0],iconAnchor:[7,7]}),draggable:!0,keyboard:!1,zIndexOffset:700}).addTo(t);m.on("drag",f=>{const g=a(f.latlng);c.x=g.x,c.y=g.y,c.entityId=null,s.dirty=!0,t.eachLayer(h=>{h instanceof L.Polyline&&h.setLatLngs(i.map(v=>n(v.x,v.y)))})}),m.on("dragend",()=>{s.dirty=!0,s.extend=null,_at2RouteEditRedraw(),_at2RouteEditBanner()}),m.on("click",f=>{const g=f.originalEvent;if(g&&(g.altKey||g.shiftKey)){_at2RouteEditDeleteNode(l);return}d&&_at2RouteEditToggleExtend(u)})}),!s.extend)for(let c=0;c<i.length-1;c++){const l=(i[c].x+i[c+1].x)/2,d=(i[c].y+i[c+1].y)/2;L.marker(n(l,d),{icon:L.divIcon({className:"at2-edit-add-wrap",html:'<span class="at2-edit-add">+</span>',iconSize:[0,0],iconAnchor:[7,7]}),keyboard:!1,zIndexOffset:690}).addTo(t).on("click",()=>_at2RouteEditInsert(c))}if(r){const c="at2-draw-hover"+(r.snap?" at2-draw-hover--"+r.snap:"");L.marker(n(r.x,r.y),{icon:L.divIcon({className:"at2-draw-hover-wrap",html:`<span class="${c}"></span>`,iconSize:[0,0],iconAnchor:[7,7]}),keyboard:!1,interactive:!1,zIndexOffset:650}).addTo(t)}}function _at2RouteEditBanner(){const e=_atlas2Panel();if(!e)return;let t=e.querySelector("#at2-edit-banner");if(!_at2RouteEdit){t&&t.remove();return}t||(t=document.createElement("div"),t.id="at2-edit-banner",t.className="at2-draw-banner",(e.querySelector(".at2-plate")||e).appendChild(t),t.addEventListener("click",s=>{const i=s.target.closest("[data-edit]");if(!i)return;const o=i.getAttribute("data-edit");o==="ext-start"?_at2RouteEditToggleExtend("start"):o==="ext-end"?_at2RouteEditToggleExtend("end"):o==="save"?_at2RouteEditSave():o==="cancel"&&_at2RouteEditCancel()}));const n=_at2RouteEdit,a=n.wps.length;t.innerHTML=`<div class="at2-draw-panel-hdr">&#9998; Edit route</div><span class="at2-draw-count">${a} node${a===1?"":"s"} &middot; drag to reshape</span><button type="button" class="at2-draw-act${n.extend==="start"?" at2-draw-act--go":""}" data-edit="ext-start" title="Add points before the first node">${n.extend==="start"?"&#9679; Click map&hellip;":"&#8676; Extend start"}</button><button type="button" class="at2-draw-act${n.extend==="end"?" at2-draw-act--go":""}" data-edit="ext-end" title="Add points after the last node">${n.extend==="end"?"&#9679; Click map&hellip;":"Extend end &#8677;"}</button>
3487<button type="button" class="at2-draw-act at2-draw-act--go" data-edit="save"${n.dirty?"":" disabled"} title="Save the new shape">&#10003; Save</button><button type="button" class="at2-draw-act at2-draw-act--cancel" data-edit="cancel" title="Esc">&#10005; Cancel</button><div class="at2-draw-hint">Drag a node &middot; &plus; inserts &middot; Alt-click deletes &middot; click an end to extend</div>`}async function _at2RouteEditSave(){const e=_at2RouteEdit,t=currentCampaign&&currentCampaign.id,n=_at2MapById(_at2MapId);if(!e||!t||!n)return;if(e.wps.length<2){typeof showToast=="function"&&showToast("A route needs at least 2 points.");return}const a={waypoints:e.wps.map(s=>({x:s.x,y:s.y})),from_entity_id:e.wps[0].entityId||null,to_entity_id:e.wps[e.wps.length-1].entityId||null};try{await api("PATCH",`/api/campaigns/${t}/maps/${n.id}/roads/${e.id}`,a),_at2RouteEditCancel(),typeof showToast=="function"&&showToast("Route updated."),await renderAtlas2({cid:t,mapId:n.id}),_at2SetView(0)}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s&&s.message||"try again"))}}function _at2RouteEditCancel(){const e=_at2RouteEdit&&_at2RouteEdit.id;_at2RouteEdit=null,_at2SyncAuthoringView(),_at2Leaflet&&_at2Leaflet.__at2EditLayer&&_at2Leaflet.__at2EditLayer.clearLayers();try{const t=_at2Leaflet&&_at2Leaflet.__at2RoadLines&&_at2Leaflet.__at2RoadLines[e],n=_at2Leaflet&&_at2Leaflet.__at2RoadHits&&_at2Leaflet.__at2RoadHits[e];[t,n].forEach(a=>{const s=a&&a.getElement&&a.getElement();s&&(s.style.display="")})}catch{}_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("at2-drawing"),_at2RouteEditBanner()}function _at2ToggleScale(){if(_at2ScaleMode){_at2ScaleCancel();return}_at2DrawMode&&_at2DrawCancel(),_at2RouteEdit&&_at2RouteEditCancel(),_at2ScaleMode={points:[],_hover:null},_at2SyncAuthoringView(),_atlas2Panel()?.querySelector("#at2-map")?.classList.add("at2-drawing"),_at2ScaleBanner(),_at2ScaleRedraw(),typeof showToast=="function"&&showToast("Click the two ends of a known distance: a scale-bar segment, or one hex.")}function _at2ScaleHover(e){!_at2ScaleMode||!_at2ScaleMode.points.length||(_at2ScaleMode._hover=e,_at2ScaleRedraw())}function _at2ScaleAddPoint(e){_at2ScaleMode&&(_at2ScaleMode.points.push({x:e.x,y:e.y}),_at2ScaleMode._hover=null,_at2ScaleRedraw(),_at2ScaleBanner(),_at2ScaleMode.points.length>=2&&_at2ScaleFinish())}function _at2ScaleRedraw(){const e=_at2Leaflet;if(!e||!e.__at2ToLL||!_at2ScaleMode)return;e.__at2ScaleLayer?e.__at2ScaleLayer.clearLayers():e.__at2ScaleLayer=L.layerGroup().addTo(e);const t=e.__at2ScaleLayer,n=e.__at2ToLL,a=_at2ScaleMode.points.slice(),s=_at2ScaleMode._hover&&a.length===1?a.concat([_at2ScaleMode._hover]):a;s.length>=2&&L.polyline(s.map(i=>n(i.x,i.y)),{className:"at2-scale-line",interactive:!1}).addTo(t),a.forEach(i=>L.marker(n(i.x,i.y),{icon:L.divIcon({className:"at2-draw-dot-wrap",html:'<span class="at2-draw-dot at2-draw-dot--snap"></span>',iconSize:[0,0],iconAnchor:[5,5]}),keyboard:!1,interactive:!1,zIndexOffset:600}).addTo(t))}async function _at2ScaleFinish(){const e=_at2ScaleMode,t=currentCampaign&&currentCampaign.id,n=_at2MapById(_at2MapId);if(!e||e.points.length<2||!t||!n){_at2ScaleCancel();return}const a=window.AtlasRoutes,s=a?a.dist(e.points[0],e.points[1]):Math.hypot(e.points[1].x-e.points[0].x,e.points[1].y-e.points[0].y),i=n.scale_unit||"mi",o=a?a.resolveUnit(i,n.map_travel):null,r=o?o.custom?o.plural||"units":(a.DIST_UNITS[i]||a.DIST_UNITS.mi).label.toLowerCase():"miles",c=await confirmDialog({title:"How long is this line?",message:`Your line spans ${Math.round(s)} px on the map. Enter how far it is in ${r}: a scale-bar segment, or one hex. (Change the unit anytime in Travel settings.)`,confirmLabel:"Set scale",cancelLabel:"Cancel",danger:!1,textInput:{label:`Distance (${r})`,placeholder:"e.g. 1",rows:1,maxlength:12}});if(!c||!c.confirmed){_at2ScaleCancel();return}const l=parseFloat((c.text!=null?c.text:c.value)||"");if(!(l>0)){typeof showToast=="function"&&showToast("Enter a positive distance."),_at2ScaleCancel();return}const d=a?a.displayToMiles(l,i,n.map_travel):l;if(!(d>0)){typeof showToast=="function"&&showToast("Could not compute scale."),_at2ScaleCancel();return}const u=s/d,p=l%1===0?String(l):l.toFixed(1),m=o&&o.custom?l===1?o.singular||"unit":o.plural||"units":a?a.unitShort(i):"mi",f=`${p} ${m} reference`;try{await api("PUT",`/api/campaigns/${t}/maps/${n.id}`,{scale_px_per_mile:u,scale_label:f}),n.scale_px_per_mile=u,n.scale_label=f,_at2ScaleCancel(),typeof showToast=="function"&&showToast("Scale set, roads now show distance + travel time."),await renderAtlas2({cid:t,mapId:n.id})}catch(g){_at2ScaleCancel(),typeof showToast=="function"&&showToast("Failed: "+(g&&g.message||"try again"))}}function _at2ScaleCancel(){_at2ScaleMode=null,_at2SyncAuthoringView(),_at2Leaflet&&_at2Leaflet.__at2ScaleLayer&&_at2Leaflet.__at2ScaleLayer.clearLayers(),_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("at2-drawing"),_at2ScaleBanner()}function _at2ScaleBanner(){const e=_atlas2Panel();if(!e)return;let t=e.querySelector("#at2-scale-banner");if(!_at2ScaleMode){t&&t.remove();return}t||(t=document.createElement("div"),t.id="at2-scale-banner",t.className="at2-draw-banner",(e.querySelector(".at2-plate")||e).appendChild(t),t.addEventListener("click",a=>{const s=a.target.closest("[data-scale]");s&&s.getAttribute("data-scale")==="cancel"&&_at2ScaleCancel()}));const n=_at2ScaleMode.points.length;t.innerHTML=`<div class="at2-draw-panel-hdr">&#8596; Set scale</div><span class="at2-draw-count">${n===0?"Click the FIRST end":"Click the SECOND end"}</span>
3487<button type="button" class="at2-draw-act at2-draw-act--cancel" data-scale="cancel" title="Esc">&#10005; Cancel</button><div class="at2-draw-hint">A scale-bar segment, or one hex &middot; then enter the miles.</div>`}function _at2StartDiffSection(e){_at2ScaleMode&&_at2ScaleCancel(),_at2DrawMode&&_at2DrawCancel(),_at2RouteEdit&&_at2RouteEditCancel();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2CloseSheet(),_at2DiffSecMode={roadId:e,points:[],_hoverFrac:null},_at2SyncAuthoringView(),_atlas2Panel()?.querySelector("#at2-map")?.classList.add("at2-drawing"),_at2DiffSecBanner(),_at2DiffSecRedraw(),typeof showToast=="function"&&showToast("Click the START of the difficult stretch, on the road.")}function _at2DiffSecWps(){const e=_at2DiffSecMode;if(!e)return null;const t=(_at2Roads||[]).find(a=>a.id===e.roadId);if(!t)return null;let n=t.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}return Array.isArray(n)&&n.length>=2?n:null}function _at2DiffSecAddPoint(e){const t=_at2DiffSecMode;if(!t||!e)return;const n=_at2DiffSecWps();if(!n){_at2DiffSecCancel();return}const a=window.AtlasRoutes;a&&(t.points.push(a.projectFracOnPath(n,e)),t._hoverFrac=null,_at2DiffSecBanner(),_at2DiffSecRedraw(),t.points.length>=2&&_at2DiffSecFinish())}function _at2DiffSecHover(e){const t=_at2DiffSecMode;if(!t||t.points.length!==1||!e)return;const n=_at2DiffSecWps(),a=window.AtlasRoutes;!n||!a||(t._hoverFrac=a.projectFracOnPath(n,e),_at2DiffSecRedraw())}function _at2DiffSecRedraw(){const e=_at2Leaflet;if(!e||!e.__at2ToLL)return;e.__at2DiffSecLayer||(e.__at2DiffSecLayer=L.layerGroup().addTo(e));const t=e.__at2DiffSecLayer;t.clearLayers();const n=_at2DiffSecMode;if(!n||!n.points.length)return;const a=_at2DiffSecWps(),s=window.AtlasRoutes,i=e.__at2ToLL;if(!a||!s)return;const o=s.pointAtFraction(a,n.points[0]);if(n._hoverFrac!=null){const r=s.subPath(a,n.points[0],n._hoverFrac);r.length>=2&&L.polyline(r.map(l=>i(l.x,l.y)),{className:"at2-diffsec-preview",interactive:!1}).addTo(t);const c=s.pointAtFraction(a,n._hoverFrac);c&&L.circleMarker(i(c.x,c.y),{radius:4,className:"at2-diffsec-dot at2-diffsec-dot--end",interactive:!1}).addTo(t)}o&&L.circleMarker(i(o.x,o.y),{radius:5,className:"at2-diffsec-dot",interactive:!1}).addTo(t)}async function _at2DiffSecFinish(){const e=_at2DiffSecMode,t=currentCampaign&&currentCampaign.id;if(!e||e.points.length<2||!t){_at2DiffSecCancel();return}const n=Math.min(e.points[0],e.points[1]),a=Math.max(e.points[0],e.points[1]);if(a-n<.02){typeof showToast=="function"&&showToast("That stretch is too short, try again."),_at2DiffSecCancel();return}const s=(_at2Roads||[]).find(r=>r.id===e.roadId);let i=s&&s.diff_sections;if(typeof i=="string")try{i=JSON.parse(i)}catch{i=[]}i=Array.isArray(i)?i:[];const o=i.concat([[Math.round(n*1e4)/1e4,Math.round(a*1e4)/1e4]]);try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e.roadId}`,{diff_sections:o}),typeof showToast=="function"&&showToast("Difficult stretch marked."),_at2DiffSecCancel(),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(r){_at2DiffSecCancel(),typeof showToast=="function"&&showToast("Failed: "+(r&&r.message||"try again"))}}function _at2DiffSecCancel(){_at2DiffSecMode=null,_at2SyncAuthoringView(),_at2Leaflet&&_at2Leaflet.__at2DiffSecLayer&&_at2Leaflet.__at2DiffSecLayer.clearLayers(),_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("at2-drawing"),_at2DiffSecBanner()}function _at2DiffSecBanner(){const e=_atlas2Panel();if(!e)return;let t=e.querySelector("#at2-diffsec-banner");if(!_at2DiffSecMode){t&&t.remove();return}t||(t=document.createElement("div"),t.id="at2-diffsec-banner",t.className="at2-draw-banner at2-draw-banner--diff",(e.querySelector(".at2-plate")||e).appendChild(t),t.addEventListener("click",s=>{const i=s.target.closest("[data-diffsec]");if(!i)return;const o=i.getAttribute("data-diffsec");o==="cancel"?_at2DiffSecCancel():o==="undo"&&_at2DiffSecMode&&(_at2DiffSecMode.points.pop(),_at2DiffSecMode._hoverFrac=null,_at2DiffSecRedraw(),_at2DiffSecBanner())}));const n=_at2DiffSecMode.points.length,a=n===1?'<button type="button" class="at2-draw-act" data-diffsec="undo">&#8592; Undo start</button>':"";t.innerHTML=`<div class="at2-draw-panel-hdr">&#9968; Mark difficult stretch</div><span class="at2-draw-count">${n===0?"Click the START on the road":"Click the END on the road"}</span>
3487`+a+'<button type="button" class="at2-draw-act at2-draw-act--cancel" data-diffsec="cancel" title="Esc">&#10005; Cancel</button><div class="at2-draw-hint">The stretch between your two clicks becomes difficult terrain.</div>'}function _at2StartPort(e){const t=(_at2Roads||[]).find(n=>n.id===e);if(!t||t.route_type!=="water"){typeof showToast=="function"&&showToast("Docks go on a water route.");return}_at2ScaleMode&&_at2ScaleCancel(),_at2DrawMode&&_at2DrawCancel(),_at2DiffSecMode&&_at2DiffSecCancel(),_at2RouteEdit&&_at2RouteEditCancel();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2CloseSheet(),_at2PortMode={roadId:e},_at2SyncAuthoringView(),_atlas2Panel()?.querySelector("#at2-map")?.classList.add("at2-drawing"),_at2PortBanner(),typeof showToast=="function"&&showToast("Click a place this route reaches to make it a port, or click the route for a plain dock.")}function _at2PortWps(){const e=_at2PortMode;if(!e)return null;const t=(_at2Roads||[]).find(a=>a.id===e.roadId);if(!t)return null;let n=t.waypoints;if(typeof n=="string")try{n=JSON.parse(n)}catch{n=[]}return Array.isArray(n)&&n.length>=2?n:null}async function _at2PortAddPoint(e){const t=_at2PortMode,n=currentCampaign&&currentCampaign.id;if(!t||!e||!n)return;if(e.entityId){const l=e.entityId;_at2PortCancel(),await _at2TogglePort(l,!0);return}const a=_at2PortWps(),s=window.AtlasRoutes;if(!a||!s){_at2PortCancel();return}const i=Math.round(s.projectFracOnPath(a,e)*1e4)/1e4,o=(_at2Roads||[]).find(l=>l.id===t.roadId);let r=o&&o.ports;if(typeof r=="string")try{r=JSON.parse(r)}catch{r=[]}if(r=Array.isArray(r)?r:[],r.some(l=>Math.abs(((l&&typeof l=="object"?l.frac:l)||0)-i)<.02)){_at2PortCancel();return}const c=r.concat([{frac:i}]);try{await api("PATCH",`/api/campaigns/${n}/maps/${_at2MapId}/roads/${t.roadId}`,{ports:c}),typeof showToast=="function"&&showToast("Dock placed."),_at2PortCancel(),await renderAtlas2({cid:n,mapId:_at2MapId})}catch(l){_at2PortCancel(),typeof showToast=="function"&&showToast("Failed: "+(l&&l.message||"try again"))}}async function _at2ClearPorts(e){const t=currentCampaign&&currentCampaign.id;if(!(!t||!await confirmDialog({title:"Clear all docks?",message:"This removes every dock from this water route. The route will no longer support land\u2194water boarding until you add new docks.",confirmLabel:"Clear docks",danger:!0})))try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{ports:[]}),typeof showToast=="function"&&showToast("Docks cleared."),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}function _at2PortCancel(){_at2PortMode=null,_at2SyncAuthoringView(),_atlas2Panel()?.querySelector("#at2-map")?.classList.remove("at2-drawing"),_at2PortBanner()}function _at2PortBanner(){const e=_atlas2Panel();if(!e)return;let t=e.querySelector("#at2-port-banner");if(!_at2PortMode){t&&t.remove();return}t||(t=document.createElement("div"),t.id="at2-port-banner",t.className="at2-draw-banner at2-draw-banner--diff",(e.querySelector(".at2-plate")||e).appendChild(t),t.addEventListener("click",n=>{n.target.closest('[data-port="cancel"]')&&_at2PortCancel()})),t.innerHTML='<div class="at2-draw-panel-hdr">&#9875; Place a dock</div><span class="at2-draw-count">Click a place this route reaches, or the route itself</span><button type="button" class="at2-draw-act at2-draw-act--cancel" data-port="cancel" title="Esc">&#10005; Cancel</button><div class="at2-draw-hint">Click a town the route is attached to &rarr; it becomes a port. Or click the route for a plain dock &middot; a route can&rsquo;t board the water without one.</div>'}async function _at2ClearDiffSections(e){const t=currentCampaign&&currentCampaign.id;if(!(!t||!await confirmDialog({title:"Clear difficult stretches?",message:"This removes every difficult-terrain mark from this road. Travel times return to normal pace.",confirmLabel:"Clear stretches",danger:!0})))try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/roads/${e}`,{diff_sections:[]}),typeof showToast=="function"&&showToast("Difficult stretches cleared.");try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2CloseSheet(),await renderAtlas2({cid:t,mapId:_at2MapId})}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}function _at2TravelEl(e){return document.getElementById(e)}const _AT2_PACE_DEFAULT_MI={foot:24,mounted:40,ship:96};let _at2TravelPrevCtx=null;function _at2RoundDisp(e){return e==null?"":String(Math.round(e*10)/10)}function _at2TravelLiveCtx(){const e=_at2TravelEl("at2-travel-unit")?.value||"mi";let t=null;if(e==="custom"){const n=(_at2TravelEl("at2-travel-cust-sing")?.value||"").trim(),a=parseFloat(_at2TravelEl("at2-travel-cust-mpu")?.value);n&&a>0&&(t={custom:{singular:n,plural:(_at2TravelEl("at2-travel-cust-plur")?.value||"").trim()||n+"s",miles_per_unit:a}})}return{unit:e,travel:t}}function _at2TravelSyncPaceUnits(e){const t=window.AtlasRoutes;if(!t)return;const{unit:n,travel:a}=_at2TravelLiveCtx(),s=t.resolveUnit(n,a),i=s.custom?s.plural||"units":(t.DIST_UNITS[n]||t.DIST_UNITS.mi).label.toLowerCase(),o=n==="mi"||s.custom&&!(s.miles_per_unit>0),r=_at2TravelEl("at2-travel-pace-lbl");r&&(r.textContent=`Pace: ${i} per day`),["foot","mounted","ship"].forEach(p=>{const m=_at2TravelEl("at2-travel-"+p);if(m){if(e&&m.value&&m.value.trim()){const f=t.displayToMiles(parseFloat(m.value),e.unit,e.travel),g=t.milesToDisplayNum(f,n,a);g!=null&&(m.value=_at2RoundDisp(g))}m.placeholder=_at2RoundDisp(t.milesToDisplayNum(_AT2_PACE_DEFAULT_MI[p],n,a))}});const c=_at2TravelEl("at2-travel-pace-help"),l=typeof _tfIsDaggerheart=="function"&&_tfIsDaggerheart();c&&(c.textContent=o?"Blank = the D&D defaults shown. Sets how fast each mode covers ground.":"Blank = D&D defaults (foot 24 \xB7 mounted 40 \xB7 ship 96 mi/day). Sets how fast each mode covers ground."),_at2TravelEl("at2-travel-pace-col")?.classList.toggle("hidden",!!l),_at2TravelEl("at2-travel-dayh-row")?.classList.toggle("hidden",!!l),_at2TravelEl("at2-travel-modal")?.querySelector(".at2-travel-cols")?.classList.toggle("at2-travel-cols--single",!!l);const d=_at2TravelEl("at2-travel-sub");d&&(d.textContent=l?"How distances read on this map. Daggerheart sets no travel pace, so routes show distance and leave the journey to you.":"How distances and travel time read on this map.");const u=_at2TravelEl("at2-travel-noscale-lead");u&&(u.textContent=l?"\u26A0 No map scale set, distances stay hidden until you":"\u26A0 No map scale set, distances & travel time stay hidden until you")}function _at2TravelSyncCustom(){const e=_at2TravelEl("at2-travel-unit");if(!e)return;const t=e.value==="custom";_at2TravelEl("at2-travel-custom")?.classList.toggle("hidden",!t);const n=(_at2TravelEl("at2-travel-cust-sing")?.value||"").trim(),a=_at2TravelEl("at2-travel-cust-lbl");a&&(a.textContent=n||"unit");const s=_at2TravelEl("at2-travel-cust-preview");if(s){const i=parseFloat(_at2TravelEl("at2-travel-cust-mpu")?.value),o=window.AtlasRoutes;if(t&&n&&i>0&&o){const r=(_at2TravelEl("at2-travel-cust-plur")?.value||"").trim()||n+"s";s.textContent=`A 15-mile road reads as ${o.formatDistance(15,"custom",{custom:{singular:n,plural:r,miles_per_unit:i}})}.`}else s.textContent=""}}let _at2TravelMap=null,_at2TravelOnSave=null;function _at2OpenTravelModal(e){const t=e&&e.map||_at2MapById(_at2MapId);_at2TravelMap=t||null,_at2TravelOnSave=e&&e.onSave||null;const n=_at2TravelEl("at2-travel-modal");if(!t||!n)return;const a=t.map_travel||{},s=a.paces||{},i=a.custom||{},o=_at2TravelEl("at2-travel-sub");o&&(o.textContent=`How distances and travel time read on \u201C${t.title||"this map"}\u201D.`),_at2TravelEl("at2-travel-unit").value=t.scale_unit||"mi",_at2TravelEl("at2-travel-cust-sing").value=i.singular||"",_at2TravelEl("at2-travel-cust-plur").value=i.plural||"",_at2TravelEl("at2-travel-cust-mpu").value=i.miles_per_unit>0?i.miles_per_unit:"",_at2TravelEl("at2-travel-dayh").value=a.day_hours>0?a.day_hours:"";const r=window.AtlasRoutes,c=t.scale_unit||"mi",l=t.map_travel||null,d=u=>s[u]>0&&r?_at2RoundDisp(r.milesToDisplayNum(s[u],c,l)):s[u]>0?s[u]:"";_at2TravelEl("at2-travel-foot").value=d("foot"),_at2TravelEl("at2-travel-mounted").value=d("mounted"),_at2TravelEl("at2-travel-ship").value=d("ship"),_at2TravelEl("at2-travel-noscale")?.classList.toggle("hidden",t.scale_px_per_mile>0),_at2TravelSyncCustom(),_at2TravelSyncPaceUnits(),_at2TravelPrevCtx=_at2TravelLiveCtx(),n.classList.remove("hidden")}function _at2CloseTravelModal(){_at2TravelEl("at2-travel-modal")?.classList.add("hidden")}async function _at2SaveTravelModal(){const e=_at2TravelMap||_at2MapById(_at2MapId),t=currentCampaign&&currentCampaign.id;if(!e||!t){_at2CloseTravelModal();return}const n=window.AtlasRoutes,a=d=>{const u=parseFloat(_at2TravelEl(d)?.value);return u>0?u:null},s=_at2TravelEl("at2-travel-unit").value||"mi",i={},o=a("at2-travel-dayh");if(o&&(i.day_hours=o),s==="custom"){const d=(_at2TravelEl("at2-travel-cust-sing").value||"").trim(),u=a("at2-travel-cust-mpu");if(!d||!u){typeof showToast=="function"&&showToast("Custom unit needs a name and its distance in miles.");return}const p=(_at2TravelEl("at2-travel-cust-plur").value||"").trim()||d+"s";i.custom={singular:d,plural:p,miles_per_unit:u}}const r=i.custom?{custom:i.custom}:null,c={};["foot","mounted","ship"].forEach(d=>{const u=a("at2-travel-"+d);if(!u)return;const p=n?n.displayToMiles(u,s,r):u;p>0&&(c[d]=Math.round(p*100)/100)}),Object.keys(c).length&&(i.paces=c);const l=Object.keys(i).length?i:null;try{if(await api("PUT",`/api/campaigns/${t}/maps/${e.id}`,{scale_unit:s,map_travel:l}),e.scale_unit=s,e.map_travel=l,_at2CloseTravelModal(),typeof showToast=="function"&&showToast("Travel settings saved."),_at2TravelOnSave)try{await _at2TravelOnSave(e)}catch{}else await renderAtlas2({cid:t,mapId:e.id})}catch(d){typeof showToast=="function"&&showToast("Failed: "+(d&&d.message||"try again"))}}function _liveOpenTravel(){const e=liveAllMaps[liveCurrentMapIdx];if(!e||e.id==null){typeof showToast=="function"&&showToast("No map selected.");return}_at2OpenTravelModal({map:e,onSave:()=>initLiveMap()})}(function(){function t(){const n=document.getElementById("at2-travel-modal");if(!n||n.__wired)return;n.__wired=!0,n.querySelector("#at2-travel-close")?.addEventListener("click",_at2CloseTravelModal),n.querySelector("#at2-travel-cancel")?.addEventListener("click",_at2CloseTravelModal),n.addEventListener("click",
3487s=>{s.target===n&&_at2CloseTravelModal()}),n.querySelector("#at2-travel-form")?.addEventListener("submit",s=>{s.preventDefault(),_at2SaveTravelModal()}),n.querySelector("#at2-travel-unit")?.addEventListener("change",()=>{const s=_at2TravelPrevCtx;_at2TravelSyncCustom(),_at2TravelSyncPaceUnits(s),_at2TravelPrevCtx=_at2TravelLiveCtx()});const a=()=>{_at2TravelSyncCustom(),_at2TravelSyncPaceUnits(),_at2TravelPrevCtx=_at2TravelLiveCtx()};n.querySelector("#at2-travel-cust-sing")?.addEventListener("input",a),n.querySelector("#at2-travel-cust-plur")?.addEventListener("input",a),n.querySelector("#at2-travel-cust-mpu")?.addEventListener("input",a),n.querySelector("#at2-travel-setscale-link")?.addEventListener("click",()=>{_at2CloseTravelModal(),typeof _at2ToggleScale=="function"&&_at2ToggleScale()})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t):t()})();async function _at2ChartChipAt(e,t,n){const a=currentCampaign&&currentCampaign.id,s=_at2MapById(_at2MapId);if(!(!a||!s))try{await api("POST",`/api/campaigns/${a}/maps/${s.id}/placements`,{entity_id:e,x:n.x,y:n.y}),typeof showToast=="function"&&showToast(`Charted ${t||"the place"}.`),await renderAtlas2({cid:a,mapId:s.id})}catch(i){typeof showToast=="function"&&showToast("Failed: "+(i&&i.message||"try again"))}}function _at2FloatMenu(e,t,n){document.querySelectorAll(".at2-float-menu").forEach(c=>{try{c.__cleanup?c.__cleanup():c.remove()}catch{c.remove()}});const a=document.createElement("div");a.className="at2-float-menu "+(n||"");const s=()=>{a.remove(),document.removeEventListener("click",i,!0),document.removeEventListener("keydown",o)},i=c=>{!a.contains(c.target)&&!e.contains(c.target)&&s()},o=c=>{c.key==="Escape"&&s()};t.forEach(c=>{const l=document.createElement("button");l.type="button",l.className="at2-float-act",l.innerHTML=c.html,c.tip&&(l.title=c.tip),l.addEventListener("click",()=>{s(),c.fn()}),a.appendChild(l)}),document.body.appendChild(a);const r=e.getBoundingClientRect();return a.style.left=Math.round(Math.max(8,Math.min(window.innerWidth-a.offsetWidth-8,r.left)))+"px",a.style.top=Math.round(Math.max(8,r.top-a.offsetHeight-6))+"px",a.__cleanup=s,setTimeout(()=>{document.addEventListener("click",i,!0),document.addEventListener("keydown",o)},0),a}function _at2ChipMenu(e,t,n){_at2FloatMenu(n,[{html:"Set as World",tip:"Mark this as the campaign\u2019s top-level map: the world every other map sits inside. It leaves the inbox and anchors the Directory.",fn:()=>_at2InboxSetWorld(e,t)},{html:"Off the map",tip:"Won\u2019t sit on a map: too small to chart, or no fixed location. Stays in the Codex; it just leaves the inbox.",fn:()=>_at2InboxSetFlag(e,"off_the_map",!0,`${t} is off the map.`)}],"at2-tray-menu")}function _at2AuxPop(e,t){const n=e==="else"?_at2Inbox&&_at2Inbox.elsewhere||[]:_at2Inbox&&_at2Inbox.off_map||[];if(!n.length)return;const a=n.map(s=>e==="else"?{html:`${escHtml(s.name)}<span class="at2-float-add">Add here &#9656;</span>`,fn:()=>_at2StartPlace({kind:"pin",entityId:s.entity_id,name:s.name})}:{html:`${escHtml(s.name)}<span class="at2-float-add">Back to inbox</span>`,fn:()=>_at2InboxSetFlag(s.entity_id,"off_the_map",!1,"Back in the inbox.")});_at2FloatMenu(t,a,"at2-tray-pop")}async function _at2MovePin(e,t,n,a,s){const i=_at2Leaflet&&_at2Leaflet.__at2FromLL;if(!i)return;if(s&&_at2Leaflet)try{const c=_at2Leaflet.latLngToContainerPoint(s),l=_at2Leaflet.latLngToContainerPoint(e.getLatLng()),d=l.x-c.x,u=l.y-c.y;if(Math.sqrt(d*d+u*u)<8){try{e.setLatLng(s)}catch{}return}}catch{}const o=await confirmDialog({title:`Move ${t.name}?`,message:"Place this pin at the new location?",confirmLabel:"Move pin",cancelLabel:"Cancel",danger:!1});if(!o||o&&o.confirmed===!1){if(s)try{e.setLatLng(s)}catch{}return}const r=i(e.getLatLng());try{await api("PATCH",`/api/campaigns/${n}/maps/${a}/placements/${t.entity_id}`,{x:r.x,y:r.y}),t.x=r.x,t.y=r.y,typeof showToast=="function"&&showToast(`Moved ${t.name}.`)}catch(c){if(s)try{e.setLatLng(s)}catch{}typeof showToast=="function"&&showToast("Move failed: "+(c&&c.message||"try again"))}}async function _at2ToggleHide(e){const t=currentCampaign&&currentCampaign.id,n=parseInt(e.getAttribute("data-at2-hide")),a=e.getAttribute("data-hidden")==="1";
3487if(!(!t||!n)){if(a){const s=(allEntities||[]).find(o=>o.id===n),i=await confirmDialog({title:"Reveal to the party?",message:`Reveal ${s?s.name:"this place"} to the players?`,confirmLabel:"Reveal",cancelLabel:"Keep hidden",danger:!1});if(!i||i&&i.confirmed===!1)return}try{await api("PATCH",`/api/campaigns/${t}/maps/${_at2MapId}/placements/${n}`,{hidden:!a});const s=(allEntities||[]).find(i=>i.id===n);s&&(s.map_pin_hidden=!a),typeof showToast=="function"&&showToast(a?"Revealed to the party.":"Hidden from players."),_at2CloseSheet(),_at2Rerender()}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s.message||"try again"))}}}async function _at2TogglePort(e,t){const n=currentCampaign&&currentCampaign.id;if(!n||!e||!_at2MapId)return;const a=(allEntities||[]).find(r=>r.id===e),s=((_at2CurrentGaz||_at2Gaz||{}).locations||[]).find(r=>r.entity_id===e),i=s?!!s.is_port:!!t,o=typeof t=="boolean"?t:!i;if(o===i&&typeof t=="boolean"){typeof showToast=="function"&&showToast(`${a?a.name:"This place"} is already a port.`);return}try{await api("PATCH",`/api/campaigns/${n}/maps/${_at2MapId}/placements/${e}`,{is_port:o}),s&&(s.is_port=o),typeof showToast=="function"&&showToast(o?`${a?a.name:"Place"} is now a port, water routes can board here.`:`Port removed from ${a?a.name:"this place"}.`),_at2CloseSheet(),_at2Rerender()}catch(r){typeof showToast=="function"&&showToast("Failed: "+(r&&r.message||"try again"))}}async function _at2RenamePin(e){const t=currentCampaign&&currentCampaign.id;if(!t||!e)return;const n=(allEntities||[]).find(o=>o.id===e),a=n?n.name:"",s=await confirmDialog({title:"Rename place",message:"Give this place a new name.",confirmLabel:"Rename",cancelLabel:"Cancel",danger:!1,textInput:{label:"Name",placeholder:"Place name",defaultValue:a,rows:1,maxlength:120}});if(!s||s.confirmed===!1)return;const i=((s.text!=null?s.text:s.value)||"").trim();if(_at2CloseSheet(),!(!i||i===a))try{await api("PUT",`/api/campaigns/${t}/entities/${e}`,{name:i});const o=(allEntities||[]).find(r=>r.id===e);o&&(o.name=i),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof showToast=="function"&&showToast(`Renamed to ${i}.`)}catch(o){typeof showToast=="function"&&showToast("Rename failed: "+(o&&o.message||"try again"))}}async function _at2DeletePin(e){const t=currentCampaign&&currentCampaign.id;if(!t||!e)return;const n=(allEntities||[]).find(r=>r.id===e),a=n?n.name:"this place",s=(_at2Placements[e]||[]).slice(),i=s.length>1;let o;if(i){const r=await confirmDialog({title:`Delete ${a}?`,message:`${a} is pinned on ${s.length} maps. Choose how far this delete reaches.`,confirmLabel:"Delete",cancelLabel:"Cancel",danger:!0,selectInput:{label:"Remove from:",defaultValue:"this",options:[{value:"this",label:"This map only, keep it on the other maps"},{value:"all",label:`All ${s.length} maps, keep its Codex entry`},{value:"codex",label:"Everything, all maps and the Codex entry"}]}});if(!r||r.confirmed===!1)return;o=r.select||"this"}else{const r=await confirmDialog({title:"Delete this place?",message:`Remove ${a} from the map. Its codex entry is kept unless you tick the box below.`,confirmLabel:"Delete",cancelLabel:"Cancel",danger:!0,checkbox:{label:"Also delete its codex entry (delete from map AND codex)",defaultChecked:!1}});if(!r||r.confirmed===!1)return;o=r.checked?"codex":"this"}try{if(o==="codex")await api("DELETE",`/api/campaigns/${t}/entities/${e}`),allEntities=(allEntities||[]).filter(r=>r.id!==e);else if(o==="all")for(const r of s)await api("DELETE",`/api/campaigns/${t}/maps/${r}/placements/${e}`);else await api("DELETE",`/api/campaigns/${t}/maps/${_at2MapId}/placements/${e}`);if(_at2CloseSheet(),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof _at2RefreshInbox=="function")try{await _at2RefreshInbox()}catch{}if(typeof showToast=="function"){const r=o==="codex"?`Deleted ${a}.`:o==="all"?`Removed ${a} from all maps.`:`Removed ${a} from the map.`;showToast(r)}}catch(r){typeof showToast=="function"&&showToast("Delete failed: "+(r&&r.message||"try again"))}}async function _at2WpLabel(e){const t=currentCampaign&&currentCampaign.id,n=await confirmDialog({title:"Label this leg",message:"Give this stop a short name (e.g. \u201CMade camp at the ford\u201D).",confirmLabel:"Save",cancelLabel:"Cancel",danger:!1,textInput:{label:"Label",placeholder:"A short name for this stop"}});if(!n||n.confirmed===!1)return;const a=(n.text||n.value||"").trim();try{await api("PATCH",`/api/campaigns/${t}/party-positions/${e}`,{label:a||null}),_at2Rerender()}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s.message||"try again"))}}async function _at2WpExclude(e){const t=currentCampaign&&currentCampaign.id,n=await confirmDialog({title:"Remove this leg?",message:"Take this stop off the road? It stays in the record; you can restore it from Surveyor\u2019s Tools.",confirmLabel:"Remove",cancelLabel:"Keep"});if(!(!n||n.confirmed===!1))try{await api("PATCH",`/api/campaigns/${t}/party-positions/${e}`,{excluded:!0}),_at2Rerender()}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a.message||"try again"))}}let _at2Inbox=null;function _at2EnsureDrawer(){let e=document.getElementById("at2-gm-drawer");return e||(e=document.createElement("div"),e.id="at2-gm-drawer",e.className="
3487chr2-gm-drawer hidden",e.innerHTML=`<div class="chr2-gm-scrim" data-at2-dclose></div>
3488    <aside class="chr2-gm-panel" role="dialog" aria-modal="true" aria-label="Maps">
3489      <header class="chr2-gm-hdr"><span class="chr2-gm-title">Maps</span>
3490        <button type="button" class="chr2-gm-close" data-at2-dclose aria-label="Close">&times;</button></header>
3491      <nav class="chr2-gm-tabs" id="at2-d-tabs"></nav>
3492      <div class="chr2-gm-body" id="at2-d-body"></div>
3493    </aside>`,document.body.appendChild(e),e.querySelectorAll("[data-at2-dclose]").forEach(t=>t.addEventListener("click",_at2CloseDrawer)),document.addEventListener("keydown",t=>{t.key==="Escape"&&!t.__tfModalEsc&&!_tfTopModal()&&!e.classList.contains("hidden")&&_at2CloseDrawer()}),e.addEventListener("click",_at2DrawerClick),e)}function _at2CloseDrawer(){const e=document.getElementById("at2-gm-drawer");e&&(e.classList.remove("open"),setTimeout(()=>e.classList.add("hidden"),240))}function _at2OpenDrawer(e){const t=_at2EnsureDrawer(),n=t.querySelector("#at2-d-tabs");n.innerHTML='<button type="button" class="chr2-gm-tab on" data-at2-dtab="plates">Maps</button>',t.classList.remove("hidden"),requestAnimationFrame(()=>t.classList.add("open")),_at2DrawerTab("plates")}function _at2DrawerTab(e){const t=document.getElementById("at2-gm-drawer");if(!t)return;const n=t.querySelector("#at2-d-body");n.innerHTML=_at2DrawerPlates()}function _at2DrawerPlates(){const e=(allEntities||[]).filter(o=>o.type==="location").sort((o,r)=>(o.name||"").localeCompare(r.name||"")),t=o=>'<option value="">(a place)</option>'+e.map(r=>`<option value="${r.id}"${o===r.id?" selected":""}>${escHtml(r.name)}</option>`).join(""),n=_at2Maps.find(o=>!o.location_entity_id)||_at2Maps[0]||null,a=o=>{const r=_at2ParentsOf(o.id),c=o.location_entity_id?(allEntities||[]).find(f=>f.id===o.location_entity_id):null;let l;if(!o.location_entity_id)l='<span class="at2-d-nest at2-d-nest--none">Standalone, not nested under any map.</span>';else if(r.length)l=`<span class="at2-d-nest at2-d-nest--ok">&#8627; Nests under ${_at2JoinNames(r.map(g=>`<b>${escHtml(g.map.title||_at2PlateLabel(g.map))}</b>`))}, opens from its ${escHtml(c&&c.name||"pin")} pin.</span>`;else{const f=n&&n.id!==o.id&&c;l=`<span class="at2-d-nest at2-d-nest--warn">&#9888; ${escHtml(c&&c.name||"That place")} isn&rsquo;t on a wider map yet, so this won&rsquo;t nest.${f?` <button type="button" class="at2-d-placebtn" data-at2-placeon="${c.id}" data-pmap="${n.id}" data-name="${escHtml(c.name)}">Place it on ${escHtml(n.title||_at2PlateLabel(n))} &#9656;</button>`:""}</span>`}const d=_at2LevelGroup(o.id),u=o.location_entity_id?`<div class="at2-d-level">
3494        <span class="at2-d-level-lbl">${d.length?`${escHtml(_at2LevelLabel(o,d))} of ${d.length}`:"Level"}</span>
3495        <input class="form-input at2-d-levelno" type="number" min="1" max="99" value="${o.level_ordinal!=null?o.level_ordinal:""}" data-map-id="${o.id}" placeholder="#" title="Level number (1 = first)" aria-label="Level number" />
3496        <input class="form-input at2-d-levellabel" type="text" maxlength="40" value="${escHtml(o.level_label||"")}" data-map-id="${o.id}" placeholder="Label (Cellar, B1, West Wing)" aria-label="Level label" />
3497        <button type="button" class="btn-ghost btn-xs" data-at2-levelsave="${o.id}">Save</button>
3498        ${c?`<button type="button" class="btn-ghost btn-xs" data-at2-addlevel="${c.id}" data-name="${escHtml(c.name)}" title="Upload another map for ${escHtml(c.name)} as its next level">&#43; Add a level</button>`:""}
3499      </div>`:"",p=_at2Maps.findIndex(f=>f.id===o.id);return`<div class="at2-d-plate-row">
3500      <div class="at2-d-plate-top">${`<span class="at2-d-plate-reorder"><button type="button" class="at2-d-move" data-at2-moveup="${o.id}"${p<=0?" disabled":""} title="Move up" aria-label="Move up">&#9650;</button><button type="button" class="at2-d-move" data-at2-movedn="${o.id}"${p>=_at2Maps.length-1?" disabled":""} title="Move down" aria-label="Move down">&#9660;</button></span>`}<span class="at2-d-plate-no">${escHtml(_at2PlateLabel(o).toUpperCase())}</span>
3501        <input class="form-input at2-d-maptitle" type="text" value="${escHtml(o.title||"")}" maxlength="80" data-map-id="${o.id}" placeholder="Map title\u2026" />
3502        <button type="button" class="btn-ghost btn-xs" data-at2-maprename="${o.id}">Save</button></div>
3503      <label class="at2-d-shows">This map is a close-up of:
3504        <select class="form-input at2-d-link" data-at2-link="${o.id}">${t(o.location_entity_id)}</select>
3505      </label>
3506      ${l}
3507      ${u}
3508      <div class="at2-d-map-meta">
3509        <label class="at2-d-vis" title="Show on the Atlas for players"><input type="checkbox" data-at2-mapvis="${o.id}" data-field="visible_atlas" ${o.visible_atlas!==!1?"checked":""}> Atlas</label>
3510        <label class="at2-d-vis" title="Show on the Live session screen"><input type="checkbox" data-at2-mapvis="${o.id}" data-field="visible_live" ${o.visible_live!==!1?"checked":""}> Live</label>
3511        <button type="button" class="btn-ghost btn-xs btn-ghost--danger at2-d-mapdel" data-at2-mapdel="${o.id}">Delete map</button>
3512      </div>
3513    </div>`};return`<p class="at2-d-intro">Each area map is a <b>close-up of one place</b>. Link it to that place, then pin the place on a wider map, and that wider map becomes its parent (its pin gains a door inward, and the Directory nests it).</p><div class="at2-d-maps-head">
3514    <label class="at2-pill-cta at2-d-uploadbtn">&#43; Upload map<input type="file" id="at2-d-upload-input" accept="image/*" hidden></label>
3515    <button type="button" class="btn-ghost btn-xs" id="at2-d-libbtn">From library</button>
3516    <span class="form-helper-text" id="at2-d-upload-status"></span>
3517  </div>${_at2Maps.map(a).join("")}<p class="at2-d-foot-tip">&#9432; A place can live on <b>more than one map</b>, open the other map and chart it from the <b>To-Chart</b> inbox. It keeps every pin and one Codex entry.</p>`}async function _at2MoveMap(e,t){const n=currentCampaign&&currentCampaign.id;if(!n)return;const a=[..._at2Maps||[]],s=a.findIndex(o=>o.id===e),i=s+t;if(!(s<0||i<0||i>=a.length)){[a[s],a[i]]=[a[i],a[s]];try{for(let o=0;o<a.length;o++)a[o].sort_order!==o&&(a[o].sort_order=o,await api("PUT",`/api/campaigns/${n}/maps/${a[o].id}`,{sort_order:o}));_at2Maps=a,_at2DrawerTab("plates"),await renderAtlas2({cid:n,mapId:_at2MapId})}catch(o){typeof showToast=="function"&&showToast("Reorder failed: "+(o&&o.message||"try again"))}}}async function _at2RenameMapFromInput(e){const t=currentCampaign&&currentCampaign.id,n=document.querySelector(`.at2-d-maptitle[data-map-id="${e}"]`),a=n?n.value.trim():"";if(!(!t||!a))try{await api("PUT",`/api/campaigns/${t}/maps/${e}`,{title:a}),_at2Maps=(_at2Maps||[]).map(s=>s.id===e?{...s,title:a}:s),_at2DrawerTab("plates"),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof showToast=="function"&&showToast("Map renamed.")}catch(s){typeof showToast=="function"&&showToast("Rename failed: "+(s&&s.message||"try again"))}}async function _at2SaveLevelFromInputs(e){const t=currentCampaign&&currentCampaign.id,n=document.querySelector(`.at2-d-levelno[data-map-id="${e}"]`),a=document.querySelector(`.at2-d-levellabel[data-map-id="${e}"]`);if(!t)return;const s=n?n.value.trim():"",i=s===""?null:parseInt(s,10);if(i!=null&&!(i>=1&&i<=99)){typeof showToast=="function"&&showToast("Level must be between 1 and 99.");return}try{const o=await api("PUT",`/api/campaigns/${t}/maps/${e}`,{level_ordinal:i,level_label:(a?a.value.trim():"")||null});_at2Maps=(_at2Maps||[]).map(r=>r.id===e?{...r,...o}:r),_at2DrawerTab("plates"),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof showToast=="function"&&showToast("Level saved.")}catch(o){typeof showToast=="function"&&showToast("Save failed: "+(o&&o.message||"try again"))}}async function _at2ToggleMapVis(e,t,n){const a=currentCampaign&&currentCampaign.id;if(a)try{const s=await api("PUT",`/api/campaigns/${a}/maps/${e}`,{[t]:!!n});_at2Maps=(_at2Maps||[]).map(i=>i.id===e?{...i,...s}:i)}catch(s){typeof showToast=="function"&&showToast("Update failed: "+(s&&s.message||"try again"))}}async function _at2DeleteMap(e){const t=currentCampaign&&currentCampaign.id;if(!t)return;const n=_at2MapById(e),a=await confirmDialog({title:"Delete this map?",message:`Delete "${escHtml(n?n.title||_at2PlateLabel(n):"this map")}"? Its pins are detached (coordinates kept) but not deleted.`,confirmLabel:"Delete map",cancelLabel:"Cancel",danger:!0});if(!(!a||a.confirmed===!1))try{await api("DELETE",`/api/campaigns/${t}/maps/${e}`),_at2Maps=(_at2Maps||[]).filter(s=>s.id!==e),_at2MapId===e&&(_at2MapId=_at2Maps[0]&&_at2Maps[0].id||null),_at2LoadedCid=null,_at2DrawerTab("plates"),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof showToast=="function"&&showToast("Map deleted.")}catch(s){typeof showToast=="function"&&showToast("Delete failed: "+(s&&s.message||"try again"))}}async function _at2UploadMap(e){const t=currentCampaign&&currentCampaign.id;if(!t||!e)return;const n=document.getElementById("at2-d-upload-status");n&&(n.textContent="Uploading\u2026");try{const a=await _tfUploadMapDirect(t,e,{registerPath:"/maps/register",fields:{title:"Map"}});_at2Maps=[..._at2Maps||[],a],_at2LoadedCid=null,n&&(n.textContent=""),_at2DrawerTab("plates"),await renderAtlas2({cid:t,mapId:a.id}),typeof showToast=="function"&&showToast(`"${a.title}" uploaded.`)}catch(a){n&&(n.textContent="Upload failed: "+(a&&a.message||"try again"))}}
3517function _at2GiveOwnMap(e,t,n){const a=currentCampaign&&currentCampaign.id;if(!a||!e)return;const s=!!(n&&n.level),i=document.createElement("input");i.type="file",i.accept="image/*",i.style.display="none",document.body.appendChild(i),i.addEventListener("change",async()=>{const o=i.files&&i.files[0];if(i.remove(),!!o){if(o.size>TF_MAP_MAX_BYTES){typeof showToast=="function"&&showToast("Map image must be 10 MB or smaller.");return}_at2CloseSheet(),typeof showToast=="function"&&showToast(`Uploading ${t||"the"} map\u2026`);try{const r=await _tfUploadMapDirect(a,o,{registerPath:"/maps/register",fields:{title:t||"Map",location_entity_id:e}});_at2Maps=[..._at2Maps||[],r],_at2LoadedCid=null,_at2DrillAnchor={dir:"in"},await renderAtlas2({cid:a,mapId:r.id}),typeof showToast=="function"&&showToast(s?`${_at2LevelLabel(r)} added to ${t||"this place"}.`:`${t||"This place"} now has its own map.`)}catch(r){typeof showToast=="function"&&showToast("Failed: "+(r&&r.message||"try again"))}}}),i.click()}async function _at2PartyToPin(e,t,n){const a=currentCampaign&&currentCampaign.id;if(!a||!e)return;_at2CloseSheet();const s=(allEntities||[]).find(i=>i.id===e);try{await api("POST",`/api/campaigns/${a}/party-positions`,{map_x:t,map_y:n,map_id:_at2MapId,location_entity_id:e,source:"manual"}),typeof showToast=="function"&&showToast(`The party is at ${s?s.name:"this place"}.`),await renderAtlas2({cid:a,mapId:_at2MapId})}catch(i){typeof showToast=="function"&&showToast("Failed: "+(i&&i.message||"try again"))}}async function _at2EditGmNote(e){const t=currentCampaign&&currentCampaign.id;if(!t||!e)return;const n=(allEntities||[]).find(o=>o.id===e),a=n&&n.ledger&&n.ledger.gm_note||"",s=await confirmDialog({title:"GM note",message:"A private note for this place, only GMs see it, never players.",confirmLabel:"Save",cancelLabel:"Cancel",danger:!1,textInput:{label:"Note",placeholder:"e.g. secret door behind the bookcase; Mordenkainen is here but they don\u2019t know yet",defaultValue:a,rows:3,maxlength:600}});if(!s||s.confirmed===!1)return;const i=((s.text!=null?s.text:s.value)||"").trim();_at2CloseSheet();try{await api("PUT",`/api/campaigns/${t}/entities/${e}`,{ledger:{gm_note:i||null}}),n&&(n.ledger=Object.assign({},n.ledger,{gm_note:i||null})),await renderAtlas2({cid:t,mapId:_at2MapId}),typeof showToast=="function"&&showToast(i?"Note saved.":"Note cleared.")}catch(o){typeof showToast=="function"&&showToast("Failed: "+(o&&o.message||"try again"))}}function _at2AddMapFromLibrary(){!currentCampaign||typeof _campLibOpen!="function"||_campLibOpen({target:"map",filter:"map",title:"Add a map from your library",subtitle:"Pick a loose campaign image to add as a new map.",onPick:async()=>{const e=currentCampaign.id;try{_at2LoadedCid=null,await renderAtlas2({cid:e,mapId:_at2MapId}),document.getElementById("at2-gm-drawer")&&_at2DrawerTab("plates"),typeof showToast=="function"&&showToast("Map added from your library.")}catch(t){typeof showToast=="function"&&showToast("Could not refresh maps: "+(t&&t.message||t))}}})}function _at2DrawerClick(e){const t=e.target.closest("[data-at2-dtab]");if(t){_at2DrawerTab(t.getAttribute("data-at2-dtab"));return}const n=e.target.closest("[data-at2-place]");if(n){_at2StartPlace({kind:"pin",entityId:parseInt(n.getAttribute("data-at2-place")),name:n.getAttribute("data-name")});return}const a=e.target.closest("[data-at2-placeon]");if(a){_at2PlaceOnMap(parseInt(a.getAttribute("data-at2-placeon")),parseInt(a.getAttribute("data-pmap")),a.getAttribute("data-name"));return}const s=e.target.closest("[data-at2-moveup]");if(s){_at2MoveMap(parseInt(s.getAttribute("data-at2-moveup")),-1);return}const i=e.target.closest("[data-at2-movedn]");if(i){_at2MoveMap(parseInt(i.getAttribute("data-at2-movedn")),1);return}const o=e.target.closest("[data-at2-world]");if(o){_at2InboxSetWorld(parseInt(o.getAttribute("data-at2-world")),o.getAttribute("data-name"));return}const r=e.target.closest("[data-at2-offmap]");if(r){_at2InboxSetFlag(parseInt(r.getAttribute("data-at2-offmap")),"off_the_map",!0,`${r.getAttribute("data-name")} is off the map.`);return}const c=e.target.closest("[data-at2-unmap]");if(c){_at2InboxSetFlag(parseInt(c.getAttribute("data-at2-unmap")),"off_the_map",!1,"Back in the inbox.");return}if(e.target.closest("[data-at2-moveparty]")){_at2StartPlace({kind:"party"});return}if(e.target.closest("[data-at2-extract]")){_at2CloseDrawer(),_openReextractModal(_at2MapId);return}const l=e.target.closest("[data-at2-maprename]");if(l){_at2RenameMapFromInput(parseInt(l.getAttribute("data-at2-maprename")));return}const d=e.target.closest("[data-at2-levelsave]");
3517if(d){_at2SaveLevelFromInputs(parseInt(d.getAttribute("data-at2-levelsave")));return}const u=e.target.closest("[data-at2-addlevel]");if(u){_at2CloseDrawer(),_at2GiveOwnMap(parseInt(u.getAttribute("data-at2-addlevel")),u.getAttribute("data-name"),{level:!0});return}const p=e.target.closest("[data-at2-mapdel]");if(p){_at2DeleteMap(parseInt(p.getAttribute("data-at2-mapdel")));return}if(e.target.closest("#at2-d-libbtn")){_at2AddMapFromLibrary();return}}async function _at2RefreshInbox(){const e=currentCampaign&&currentCampaign.id;try{const n=await api("GET",`/api/campaigns/${e}/atlas/pin-inbox?map_id=${_at2MapId}`);n&&(_at2Inbox=n,_at2InboxCount=(n.items||[]).filter(a=>a.session_id).length)}catch{}const t=document.getElementById("at2-tray");t&&(t.outerHTML=_at2TrayHtml())}async function _at2InboxSetFlag(e,t,n,a){const s=currentCampaign&&currentCampaign.id;try{await api("PUT",`/api/campaigns/${s}/entities/${e}`,{ledger:{[t]:n}});const i=(allEntities||[]).find(o=>o.id===e);i&&(i.ledger=Object.assign({},i.ledger,{[t]:n})),typeof showToast=="function"&&showToast(a),await _at2RefreshInbox()}catch(i){typeof showToast=="function"&&showToast("Failed: "+(i&&i.message||"try again"))}}async function _at2InboxSetWorld(e,t){const n=currentCampaign&&currentCampaign.id;try{await api("PUT",`/api/campaigns/${n}/entities/${e}`,{ledger:{world_root:!0}});const a=(_at2Maps||[]).find(s=>!s.location_entity_id);if(a){await api("PUT",`/api/campaigns/${n}/maps/${a.id}`,{location_entity_id:e});const s=_at2MapById(a.id);s&&(s.location_entity_id=e)}typeof showToast=="function"&&showToast(`${t} is now the campaign\u2019s world${a?"; it anchors the Directory":""}.`),await _at2RefreshInbox(),_at2Rerender()}catch(a){typeof showToast=="function"&&showToast("Failed: "+(a&&a.message||"try again"))}}async function _at2PlaceOnMap(e,t,n){_at2CloseDrawer();const a=currentCampaign&&currentCampaign.id;_at2MapId!==t&&(_at2Go(t),await new Promise(s=>setTimeout(s,480))),_at2StartPlace({kind:"pin",entityId:e,name:n})}async function _at2LinkMap(e){const t=currentCampaign&&currentCampaign.id,n=parseInt(e.getAttribute("data-at2-link")),a=e.value?parseInt(e.value):null;try{await api("PUT",`/api/campaigns/${t}/maps/${n}`,{location_entity_id:a});const s=_at2MapById(n);s&&(s.location_entity_id=a),typeof showToast=="function"&&showToast("Map link updated."),await _at2Rerender(),_at2OpenDrawer("plates")}catch(s){typeof showToast=="function"&&showToast("Failed: "+(s.message||"try again")),_at2Rerender()}}function _at2Wire(e,t,n,a,s,i){_at2InitLeaflet(e,t,n,a,s,i);const o=l=>{const d=_at2Maps.findIndex(p=>p.id===_at2MapId),u=_at2Maps[d+l];d<0||!u||_at2Go(u.id)};e.querySelector("#at2-map-prev")?.addEventListener("click",l=>{l.stopPropagation(),o(-1)}),e.querySelector("#at2-map-next")?.addEventListener("click",l=>{l.stopPropagation(),o(1)});const r=e.querySelector("#at2-maps-jump");r&&r.addEventListener("click",l=>{l.stopPropagation();const d=e.querySelector("#at2-maps-pop");if(d){d.remove();return}const u=document.createElement("div");u.id="at2-maps-pop",u.className="at2-maps-pop",u.innerHTML=_at2Maps.map(g=>{const h=g.id===_at2MapId,v=_at2PlateLabel(g),b=_at2LevelGroup(g.id),y=b.length?` <span class="at2-maps-pop-level">${escHtml(_at2LevelLabel(g,b))}</span>`:"";return`<button type="button" class="at2-maps-pop-row${h?" is-current":""}"${h?"":` data-at2-go="${g.id}"`}>
3518          <span class="at2-maps-pop-no">${escHtml(v.toUpperCase())}</span>
3519          <span class="at2-maps-pop-title">${escHtml(g.title||v)}${y}</span>
3520        </button>`}).join(""),r.parentNode.appendChild(u);const p=g=>{!u.contains(g.target)&&g.target!==r&&f()},m=g=>{g.key==="Escape"&&f()},f=()=>{u.remove(),document.removeEventListener("click",p,!0),document.removeEventListener("keydown",m)};setTimeout(()=>{document.addEventListener("click",p,!0),document.addEventListener("keydown",m)},0)}),e.querySelector("#at2-managemaps-btn")?.addEventListener("click",()=>_at2OpenDrawer("plates")),e.classList.toggle("at2-editing",_at2Editing),e.classList.toggle("at2-preplens",_at2PrepLens),e.querySelector("#at2-wb-toggle")?.addEventListener("click",()=>_at2WbToggle()),_at2WbRestore(),e.querySelector("#at2-journey-rebuild")?.addEventListener("click",_at2RebuildJourney),e.querySelector("#at2-fit-btn")?.addEventListener("click",()=>{if(!_at2Leaflet)return;const l=_at2Leaflet.__at2RawBounds||L.latLngBounds([[0,0],[(_at2MapById(_at2MapId)||{}).height||1,(_at2MapById(_at2MapId)||{}).width||1]]);_at2Leaflet.flyToBounds(l,{duration:.6,padding:[20,20]})}),e.querySelector("#at2-zoom-btn")?.addEventListener("click",()=>{const l=_at2Journey&&_at2Journey.beacon;l&&_at2Leaflet&&_at2Leaflet.__at2ToLL?_at2Leaflet.flyTo(_at2Leaflet.__at2ToLL(l.x,l.y),_at2Leaflet.getMaxZoom(),{duration:.6}):typeof showToast=="function"&&showToast("The party isn\u2019t placed on this map yet.")}),e.querySelector("#at2-party-btn")?.addEventListener("click",()=>_at2StartPlace({kind:"party"})),e.querySelector("#at2-addloc-btn")?.addEventListener("click",async()=>{const l=await confirmDialog({title:"Add a place",message:"Name this place, then tap the map to chart it (it reuses an existing place if the name already matches one).",confirmLabel:"Next: tap the map",cancelLabel:"Cancel",danger:!1,textInput:{label:"Name",placeholder:"Place name",rows:1,maxlength:120}});if(!l||l.confirmed===!1)return;const d=((l.text!=null?l.text:l.value)||"").trim();d&&_at2StartPlace({kind:"new",name:d})}),_at2PlaceMode&&_at2ShowPlaceBanner(),e.querySelector("#at2-viewcycle-btn")?.addEventListener("click",()=>_at2SetView(_at2ViewMode+1)),e.querySelector("#at2-drawroute-btn")?.addEventListener("click",()=>_at2ToggleDrawRoute()),e.querySelector("#at2-encounter-btn")?.addEventListener("click",()=>{_at2DrawMode&&_at2DrawCancel(),_at2StartPlace({kind:"encounter"})}),e.querySelector("#at2-portal-entry-btn")?.addEventListener("click",()=>{_at2DrawMode&&_at2DrawCancel(),_at2StartPlace({kind:"portal",pkind:"entry"})}),e.querySelector("#at2-portal-exit-btn")?.addEventListener("click",()=>{_at2DrawMode&&_at2DrawCancel(),_at2StartPlace({kind:"portal",pkind:"exit"})}),e.querySelectorAll(".at2-levels [data-at2-level], .at2-levelnav [data-at2-level]").forEach(l=>l.addEventListener("click",d=>{d.stopPropagation(),_at2LevelGo(parseInt(l.getAttribute("data-at2-level")))})),e.querySelector("#at2-setscale-btn")?.addEventListener("click",()=>_at2ToggleScale()),e.querySelector("#at2-travel-btn")?.addEventListener("click",()=>_at2OpenTravelModal());const c=e.querySelector(".at2-dir-rail .cx2-rail-list");if(c){const l=()=>c.classList.toggle("at2-dir-scrolled-end",c.scrollHeight-c.scrollTop-c.clientHeight<8);c.addEventListener("scroll",l,{passive:!0}),l()}e.querySelector(".at2-dir-search")?.addEventListener("input",l=>{const d=(l.target.value||"").trim().toLowerCase();e.querySelectorAll(".at2-dir-row").forEach(u=>{const p=(u.querySelector(".cx2-ri-name")?.textContent||"").toLowerCase(),m=(u.querySelector(".cx2-ri-meta")?.textContent||"").toLowerCase();u.classList.toggle("at2-dir-searchhide",!!d&&!p.includes(d)&&!m.includes(d))})}),!e.__at2Wired&&(e.__at2Wired=!0,document.addEventListener("keydown",l=>{const d=l.target,u=d&&(d.tagName==="INPUT"||d.tagName==="TEXTAREA"||d.isContentEditable),p=document.querySelector(".at2-float-menu, #at2-gm-drawer.open, .modal
3520-overlay:not(.hidden)");if((l.key==="e"||l.key==="E")&&!u&&!p&&_at2IsGm()){const m=_atlas2Panel();if(m&&!m.classList.contains("hidden")&&m.offsetParent!==null){l.preventDefault(),_at2SetEditing(!0);return}}if(l.key==="Enter"&&_at2DrawMode&&!u){l.preventDefault(),_at2DrawFinish();return}if(l.key==="Backspace"&&_at2DrawMode&&!u){l.preventDefault(),_at2DrawMode.points.pop(),_at2DrawRedraw(_at2DrawMode._hoverSnap),_at2DrawBanner();return}if(_at2RouteEdit&&!u&&!p){if(l.key==="Escape"){l.preventDefault(),_at2RouteEdit.extend?(_at2RouteEdit.extend=null,_at2RouteEdit._hoverSnap=null,_at2RouteEditRedraw(),_at2RouteEditBanner()):_at2RouteEditCancel();return}if(l.key==="Backspace"&&_at2RouteEdit.extend&&(_at2RouteEdit._extAdded||0)>0){l.preventDefault(),_at2RouteEdit.extend==="start"?_at2RouteEdit.wps.shift():_at2RouteEdit.wps.pop(),_at2RouteEdit._extAdded--,_at2RouteEdit.dirty=!0,_at2RouteEditRedraw(),_at2RouteEditBanner();return}}if(l.key==="Escape"&&!p){if(_at2ScaleMode){_at2ScaleCancel();return}if(_at2DiffSecMode){_at2DiffSecCancel();return}if(_at2PortMode){_at2PortCancel();return}if(_at2DrawMode){_at2DrawCancel();return}if(_at2PlaceMode){_at2CancelPlace();return}_at2Editing&&_at2SetEditing(!1)}}),e.addEventListener("click",l=>{const d=l.target.closest("[data-at2-chipplace]");if(d){l.stopPropagation(),_at2StartPlace({kind:"pin",entityId:parseInt(d.getAttribute("data-at2-chipplace")),name:d.getAttribute("data-name")});return}const u=l.target.closest("[data-at2-chipmenu]");if(u){l.stopPropagation(),_at2ChipMenu(parseInt(u.getAttribute("data-at2-chipmenu")),u.getAttribute("data-name"),u);return}const p=l.target.closest("[data-at2-traypop]");if(p){l.stopPropagation(),_at2AuxPop(p.getAttribute("data-at2-traypop"),p);return}if(l.target.closest("[data-at2-extract]")){l.stopPropagation(),_openReextractModal(_at2MapId);return}const m=l.target.closest("[data-at2-nudgex]");if(m){l.stopPropagation();try{localStorage.setItem("tf_at2_scalenudge_"+m.getAttribute("data-at2-nudgex"),"1")}catch{}m.closest(".at2-wb-nudge")?.remove();return}if(l.target.closest("[data-at2-editenter]")){l.stopPropagation(),_at2SetEditing(!0);return}}),e.addEventListener("input",l=>{if(!l.target.classList||!l.target.classList.contains("at2-tray-search"))return;const d=e.querySelector("#at2-tray");if(!d)return;const u=(l.target.value||"").trim().toLowerCase();let p=0;d.querySelectorAll(".at2-tray-chip").forEach(f=>{const g=!!u&&!(f.dataset.name||"").toLowerCase().includes(u);f.classList.toggle("at2-tray-searchhide",g),g||p++});let m=d.querySelector(".at2-tray-nomatch");u&&p===0?(m||(m=document.createElement("span"),m.className="at2-tray-empty at2-tray-nomatch",d.querySelector(".at2-tray-scroll")?.appendChild(m)),m.textContent=`No place matches \u201C${l.target.value.trim()}\u201D`):m&&m.remove()}),e.addEventListener("dragstart",l=>{const d=l.target.closest("[data-at2-chip]");if(d){_at2DragChip={entityId:parseInt(d.getAttribute("data-at2-chip")),name:d.getAttribute("data-name")};try{l.dataTransfer.setData("text/plain",_at2DragChip.name||""),l.dataTransfer.effectAllowed="copy"}catch{}e.classList.add("at2-chip-dragging")}}),e.addEventListener("dragend",()=>{_at2DragChip=null,e.classList.remove("at2-chip-dragging")}),e.addEventListener("dragover",l=>{if(_at2DragChip&&l.target.closest("#at2-map")){l.preventDefault();try{l.dataTransfer.dropEffect="copy"}catch{}}}),e.addEventListener("drop",l=>{if(!_at2DragChip||!l.target.closest("#at2-map"))return;l.preventDefault();const d=_at2DragChip;if(_at2DragChip=null,e.classList.remove("at2-chip-dragging"),!_at2Leaflet||!_at2Leaflet.__at2FromLL)return;let u;try{u=_at2Leaflet.mouseEventToLatLng(l)}catch{return}_at2ChartChipAt(d.entityId,d.name,_at2Leaflet.__at2FromLL(u))}),e.addEventListener("mouseover",l=>{const d=l.target.closest(".at2-dir-row[id], .at2-dir-yah[data-at2-fly]");if(!d)return;if(d.classList.contains("at2-dir-row--route")){const p=+(d.id||"").replace(/[^0-9]/g,"");if(!p||p===_at2RailHoverRid)return;_at2RailHoverRid&&_at2HighlightRoad(_at2RailHoverRid,!1,!1),_at2RailHoverRid=p,_at2HighlightRoad(p,!0,!1);return}const u=+(d.id||d.getAttribute("data-at2-fly")||"").replace(/[^0-9]/g,"");!u||u===_at2RailHoverEid||(_at2RailHoverEid&&_at2Highlight(_at2RailHoverEid,!1,!1),_at2RailHoverEid=u,_at2Highlight(u,!0,!1))}),e.addEventListener("mouseout",l=>{const d=l.target.closest(".at2-dir-row[id], .at2-dir-yah[data-at2-fly]");if(!d||l.relatedTarget&&d.contains(l.relatedTarget))return;if(d.classList.contains("at2-dir-row--route")){const p=+(d.id||"").replace(/[^0-9]/g,"");p&&p===_at2RailHoverRid&&(_at2HighlightRoad(p,!1,!1),_at2RailHoverRid=null);return}const u=+(d.id||d.getAttribute("data-at2-fly")||"").replace(/[^0-9]/g,"");u&&u===_at2RailHoverEid&&(_at2Highlight(u,!1,!1),_at2RailHoverEid=null)}),e.addEventListener("click",l=>{if(l.target.closest("a"))return;const d=l.target.closest("[data-at2-toggle]");if(d){l.stopPropagation();const b=e.querySelector(`#at2-sub-${d.getAttribute("data-at2-toggle")}`);
3520if(b){const y=b.classList.toggle("at2-sub-collapsed");d.setAttribute("aria-expanded",String(!y)),d.closest(".at2-dir-row")?.classList.toggle("at2-dir-row--collapsed",y)}return}const u=l.target.closest("[data-at2-edit]");if(u){l.stopPropagation(),_at2EditField(u.getAttribute("data-at2-edit"));return}const p=l.target.closest("[data-at2-drawer]");if(p){l.stopPropagation(),_at2OpenDrawer(p.getAttribute("data-at2-drawer"));return}const m=l.target.closest("[data-at2-go]");if(m){l.stopPropagation();const b=m.getAttribute("data-at2-focus");_at2Go(parseInt(m.getAttribute("data-at2-go")),b?parseInt(b):null);return}if(l.target.closest("[data-at2-drawroute]")){l.stopPropagation(),_at2ToggleDrawRoute();return}const f=l.target.closest("[data-at2-routefocus]");if(f){l.stopPropagation(),_at2FocusRoute(parseInt(f.getAttribute("data-at2-routefocus")));return}const g=l.target.closest("[data-at2-wplabel]");if(g){l.stopPropagation(),_at2WpLabel(parseInt(g.getAttribute("data-at2-wplabel")));return}const h=l.target.closest("[data-at2-wexcl]");if(h){l.stopPropagation(),_at2WpExclude(parseInt(h.getAttribute("data-at2-wexcl")));return}const v=l.target.closest("[data-at2-fly]");if(v&&_at2Leaflet&&_at2Leaflet.__at2ToLL){const b=parseFloat(v.getAttribute("data-x")),y=parseFloat(v.getAttribute("data-y"));if(isNaN(b)||isNaN(y))return;const _=_at2Leaflet,w=_.__at2ToLL(b,y);_.getBounds().contains(w)?_.panTo(w,{duration:.3,easeLinearity:.25}):_.flyTo(w,Math.max(_.getZoom(),Math.min(_.getMinZoom()+1.5,0)),{duration:.7,easeLinearity:.25});const E=v.getAttribute("data-at2-fly")||"",k=E.startsWith("g")?(_.__at2Pins||{})[+E.slice(1)]:E.startsWith("w")?(_.__at2Wps||{})[+E.slice(1)]:null;k&&!_at2Coarse&&_.once("moveend",()=>{try{k.openPopup()}catch{}})}}),_at2WireGlobalDelegation())}function _at2WireGlobalDelegation(){window.__at2PopWired||(window.__at2PopWired=!0,document.addEventListener("click",e=>{const t=e.target.closest(".leaflet-popup [data-at2-rename-inline-trigger], .at2-sheet [data-at2-rename-inline-trigger]");if(t){const B=t.parentElement,H=B&&B.querySelector("[data-at2-rename-inline]");
3520H&&(e.preventDefault(),_at2ToolbarInlineRename(H));return}const n=e.target.closest(".leaflet-popup [data-at2-rename-inline], .at2-sheet [data-at2-rename-inline]");if(n){e.preventDefault(),_at2ToolbarInlineRename(n);return}const a=e.target.closest("[data-at2-hide]");if(a&&a.closest(".leaflet-popup")){e.preventDefault(),_at2ToggleHide(a);return}const s=e.target.closest(".leaflet-popup [data-at2-portpin]");if(s){e.preventDefault(),_at2TogglePort(parseInt(s.getAttribute("data-at2-portpin")));return}const i=e.target.closest(".leaflet-popup [data-at2-partyhere]");if(i){e.preventDefault(),_at2PartyToPin(parseInt(i.getAttribute("data-at2-partyhere")),parseFloat(i.getAttribute("data-x")),parseFloat(i.getAttribute("data-y")));return}const o=e.target.closest(".leaflet-popup [data-at2-gmnote]");if(o){e.preventDefault(),_at2EditGmNote(parseInt(o.getAttribute("data-at2-gmnote")));return}const r=e.target.closest(".leaflet-popup [data-at2-rename]");if(r){e.preventDefault(),_at2RenamePin(parseInt(r.getAttribute("data-at2-rename")));return}const c=e.target.closest(".leaflet-popup [data-at2-ownmap]");if(c){e.preventDefault(),_at2GiveOwnMap(parseInt(c.getAttribute("data-at2-ownmap")),c.getAttribute("data-name"));return}const l=e.target.closest(".leaflet-popup [data-at2-del]");if(l){e.preventDefault(),_at2DeletePin(parseInt(l.getAttribute("data-at2-del")));return}const d=e.target.closest(".leaflet-popup [data-at2-go]");if(d){e.preventDefault(),_at2Go(parseInt(d.getAttribute("data-at2-go")));return}if(e.target.closest(".leaflet-popup [data-at2-setscale]")){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}typeof _at2ToggleScale=="function"&&_at2ToggleScale();return}const p=e.target.closest(".leaflet-popup [data-at2-roadterrain]");if(p){e.preventDefault(),_at2ToggleRoadTerrain(parseInt(p.getAttribute("data-at2-roadterrain")));return}const m=e.target.closest(".leaflet-popup [data-at2-roadname]");if(m){e.preventDefault(),_at2NameRoad(parseInt(m.getAttribute("data-at2-roadname")));return}const f=e.target.closest(".leaflet-popup [data-at2-roadshape]");if(f){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2StartRouteEdit(parseInt(f.getAttribute("data-at2-roadshape")));return}const g=e.target.closest(".leaflet-popup [data-at2-roadcenter]");if(g){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2CenterRoadOnArt(parseInt(g.getAttribute("data-at2-roadcenter")));return}const h=e.target.closest(".leaflet-popup [data-at2-roadtype]");if(h){e.preventDefault(),_at2ToggleRoadType(parseInt(h.getAttribute("data-at2-roadtype")));return}const v=e.target.closest(".leaflet-popup [data-at2-roadhide]");if(v){e.preventDefault(),_at2ToggleRoadHidden(parseInt(v.getAttribute("data-at2-roadhide")));return}const b=e.target.closest(".leaflet-popup [data-at2-roaddel]");if(b){e.preventDefault(),_at2DeleteRoad(parseInt(b.getAttribute("data-at2-roaddel")));return}const y=e.target.closest(".leaflet-popup [data-at2-roaddiffsec]");if(y){e.preventDefault(),_at2StartDiffSection(parseInt(y.getAttribute("data-at2-roaddiffsec")));return}const _=e.target.closest(".leaflet-popup [data-at2-roaddiffclear]");if(_){e.preventDefault(),_at2ClearDiffSections(parseInt(_.getAttribute("data-at2-roaddiffclear")));return}const w=e.target.closest(".leaflet-popup [data-at2-roadport]");if(w){e.preventDefault(),_at2StartPort(parseInt(w.getAttribute("data-at2-roadport")));return}const $=e.target.closest(".leaflet-popup [data-at2-roadportclear]");if($){e.preventDefault(),_at2ClearPorts(parseInt($.getAttribute("data-at2-roadportclear")));return}const E=e.target.closest(".leaflet-popup [data-at2-encedit]");if(E){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2OpenEncModal({enc:_at2EncById(parseInt(E.getAttribute("data-at2-encedit")))});return}const k=e.target.closest(".leaflet-popup [data-at2-portalgo]");if(k){e.preventDefault(),_at2PortalGo(parseInt(k.getAttribute("data-at2-portalgo")));return}const C=e.target.closest(".leaflet-popup [data-at2-portalmove]");if(C){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2StartPlace({kind:"portal-move",portalId:parseInt(C.getAttribute("data-at2-portalmove"))});return}const S=e.target.closest(".leaflet-popup [data-at2-portaldel]");if(S){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2DeletePortal(parseInt(S.getAttribute("data-at2-portaldel")));return}const q=e.target.closest(".leaflet-popup [data-at2-addlevel]");if(q){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2GiveOwnMap(parseInt(q.getAttribute("data-at2-addlevel")),q.getAttribute("data-name"),{level:!0});return}const I=e.target.closest(".leaflet-popup [data-at2-encmove]");if(I){e.preventDefault();try{_at2Leaflet&&_at2Leaflet.closePopup()}catch{}_at2StartPlace({kind:"enc-move",encId:parseInt(I.getAttribute("data-at2-encmove"))});return}const T=e.target.closest(".leaflet-popup [data-at2-enchide]");if(T){e.preventDefault(),_at2ToggleEncHidden(parseInt(T.getAttribute("data-at2-enchide")));return}const P=e.target.closest(".leaflet-popup [data-at2-encdel]");if(P){e.preventDefault(),_at2DeleteEnc(parseInt(P.getAttribute("data-at2-encdel")));return}}),document.addEventListener("change",e=>{const t=e.target.closest("[data-at2-link]");if(t){_at2LinkMap(t);return}const n=e.target.closest("[data-at2-mapvis]");if(n){_at2ToggleMapVis(parseInt(n.getAttribute("data-at2-mapvis")),n.getAttribute("data-field"),n.checked);return}if(e.target.id==="at2-d-upload-input"){const a=e.target.files&&e.target.files[0];a&&_at2UploadMap(a)}}))}let _at2HoverRaf=null,_at2LitEid=null,_at2RailHoverEid=null,_at2LitRid=null,_at2RailHoverRid=null,_at2RoadHoverRaf=null;function _at2Highlight(e,t,n){if(!(t&&_at2LitEid===e)){if(t&&_at2LitEid!=null&&_at2LitEid!==e&&_at2HighlightClearPin(_at2LitEid),!t&&_at2LitEid!==e){_at2HighlightClearPin(e);return}_at2LitEid=t?e:null,_at2HoverRaf&&cancelAnimationFrame(_at2HoverRaf),_at2HoverRaf=requestAnimationFrame(()=>{const a=_at2Leaflet&&_at2Leaflet.__at2Pins&&_at2Leaflet.__at2Pins[e],s=a&&a.getElement();s&&s.classList.toggle("at2-pin--lit",!!t);const i=document.getElementById(`at2-gaz-${e}`);i&&(i.classList.toggle("at2-dir-hover",!!t),t&&n&&i.scrollIntoView({block:"nearest"}))})}}function _at2HighlightClearPin(e){const t=_at2Leaflet&&_at2Leaflet.__at2Pins&&_at2Leaflet.__at2Pins[e],n=t&&t.getElement&&t.getElement();n&&n.classList.remove("at2-pin--lit");const a=_at2Leaflet&&_at2Leaflet.getContainer&&_at2Leaflet.getContainer();a&&a.querySelectorAll(".at2-pin--lit").forEach(i=>
3520i.classList.remove("at2-pin--lit"));const s=document.getElementById(`at2-gaz-${e}`);s&&s.classList.remove("at2-dir-hover")}function _at2HighlightRoad(e,t,n){if(!(t&&_at2LitRid===e)){if(t&&_at2LitRid!=null&&_at2LitRid!==e&&_at2HighlightClearRoad(_at2LitRid),!t&&_at2LitRid!==e){_at2HighlightClearRoad(e);return}_at2LitRid=t?e:null,_at2RoadHoverRaf&&cancelAnimationFrame(_at2RoadHoverRaf),_at2RoadHoverRaf=requestAnimationFrame(()=>{const a=_at2Leaflet&&_at2Leaflet.__at2RoadHits&&_at2Leaflet.__at2RoadHits[e],s=a&&a.getElement&&a.getElement();s&&s.classList.toggle("at2-road-hit--lit",!!t);const i=document.getElementById(`at2-road-${e}`);i&&(i.classList.toggle("at2-dir-hover",!!t),t&&n&&i.scrollIntoView({block:"nearest"}))})}}function _at2HighlightClearRoad(e){const t=_at2Leaflet&&_at2Leaflet.__at2RoadHits&&_at2Leaflet.__at2RoadHits[e],n=t&&t.getElement&&t.getElement();n&&n.classList.remove("at2-road-hit--lit");const a=_at2Leaflet&&_at2Leaflet.getContainer&&_at2Leaflet.getContainer();a&&a.querySelectorAll(".at2-road-hit--lit").forEach(i=>i.classList.remove("at2-road-hit--lit"));const s=document.getElementById(`at2-road-${e}`);s&&s.classList.remove("at2-dir-hover")}async function _atlasHandleHash(){const e=(location.hash||"").match(/^#atlas\/(\d+)(?:\/(\d+))?(?:\/(\d+))?$/);if(!e)return!1;const t=+e[1],n=e[2]?+e[2]:null,a=e[3]?+e[3]:null;if(typeof getStoredToken!="function"||!getStoredToken())return!1;const s=++_tfNavSeq;_tfPendingLanding="atlas";try{if((!currentCampaign||currentCampaign.id!==t)&&await bootWithCampaignId(t),_tfNavStale(s))return!0;const i=_atlas2Panel();return _atlas2Show(i,location.hash,!1),await renderAtlas2({cid:t,mapId:n,focusEntityId:a}),!0}catch(i){return console.warn("[atlas2] deep-link failed",i),!1}finally{_tfPendingLanding=null}}window.addEventListener("hashchange",_atlasHandleHash),window.openAtlas2=function(e){const t=e||currentCampaign&&currentCampaign.id;if(t){if(location.hash===`#atlas/${t}`){_atlasHandleHash();return}location.hash=`#atlas/${t}`}};let _ftr=null,_ftrWired=!1,_ftrPhaseTimer=null;const _FTR_SCAN_PHASES=["Reading session summaries\u2026","Checking codex entries\u2026","Reviewing quest text\u2026","Searching cached recaps\u2026","Reading source notes\u2026","Weighing each passage\u2026"];function openForTheRecord(e={}){const t=e.entityId?(allEntities||[]).find(n=>n&&n.id===e.entityId):null;_ftr={step:"mode",mode:null,entityId:t?t.id:null,entityName:t?t.name:null,statement:"",renameOld:"",renameNew:t&&t.name||"",scan:null,renamePreview:null,choices:{},confirmRoot:!1,receipt:null,busy:!1,error:null},_ftrWire(),document.getElementById("ftr-modal")?.classList.remove("hidden"),_ftrRender()}function _ftrWire(){if(_ftrWired)return;_ftrWired=!0;const e=document.getElementById("ftr-modal");e&&(e.querySelector("[data-ftr-close]")?.addEventListener("click",()=>_ftrClose(!1)),e.addEventListener("click",t=>{t.target===e&&_ftrClose(!1)}),document.addEventListener("keydown",t=>{if(t.key!=="Escape"||!_ftr||e.classList.contains("hidden"))return;const n=document.getElementById("confirm-modal");n&&!n.classList.contains("hidden")||(t.stopImmediatePropagation(),t.preventDefault(),_ftrClose(!1))},!0))}function _ftrClose(e){if(!(_ftr&&_ftr.busy&&_ftr.step!=="scanning")){if(!e&&_ftr&&_ftr.step==="review"){const t=Object.keys(_ftr.choices).length>0;confirmDialog({title:"Discard this review?",message:t?"Nothing has been written yet, closing discards the passages you reviewed.":"Nothing has been written yet. Close and discard this scan?",confirmLabel:"Discard",danger:!0}).then(n=>{n&&_ftrClose(!0)});return}_ftrPhaseTimer&&(clearInterval(_ftrPhaseTimer),_ftrPhaseTimer=null),document.getElementById("ftr-modal")?.classList.add("hidden"),_ftr=null}}function _ftrBox(){return document.querySelector("#ftr-modal .ftr-box")}function _ftrBody(){return document.getElementById("ftr-modal-body")}function _ftrRender(){if(!_ftr)return;const e=_ftrBody();if(!e)return;const t=_ftr.step==="review"&&_ftr.mode==="correct";_ftrBox()?.classList.toggle("modal-box--xl",t),_ftrBox()?.classList.toggle("modal-box--lg",!t);const n={mode:_ftrRenderMode,input:_ftrRenderInput,scanning:_ftrRenderScanning,review:_ftrRenderReview,applying:_ftrRenderScanning,receipt:_ftrRenderReceipt,canon:_ftrRenderCanon}[_ftr.step];n&&n(e)}function _ftrRenderMode(e){e.innerHTML=`
3521    <div class="ftr-pane">
3522      <span class="form-section-label">What needs fixing?</span>
3523      <div class="ftr-mode-tiles">
3524        <div class="ftr-mode-tile" role="button" tabindex="0" data-ftr-mode="rename">
3525          <div class="ftr-mode-title">A name changed</div>
3526          <div class="ftr-mode-desc">Fix a spelling or rename something everywhere: "Stoneguard" becomes "Stone Guard" across every entry, summary, and note. Exact swap, no AI.</div>
3527        </div>
3528        <div class="ftr-mode-tile" role="button" tabindex="0" data-ftr-mode="correct">
3529          <div class="ftr-mode-title">The record is wrong</div>
3530          <div class="ftr-mode-desc">State what actually happened in plain language. Threadfall finds every passage that contradicts it and proposes minimal rewrites for your review.</div>
3531        </div>
3532      </div>
3533      <div class="ftr-canon-row"><button class="btn-new btn-xs" data-ftr-canon>Campaign canon</button></div>
3534    </div>`,e.querySelector("[data-ftr-canon]")?.addEventListener("click",()=>{_ftr.canonReturn="mode",_ftr.step="canon",_ftrRender()}),e.querySelectorAll("[data-ftr-mode]").forEach(t=>{const n=()=>{_ftr.mode=t.dataset.ftrMode,_ftr.step="input",_ftrRender()};t.addEventListener("click",n),t.addEventListener("keydown",a=>{(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),n())})})}function _ftrRenderInput(e){const t=_ftr.error?`<p class="ftr-error">${escHtml(_ftr.error)}</p>`:"";if(_ftr.mode==="rename")e.innerHTML=`
3535      <div class="ftr-pane">
3536        <span class="form-section-label">Current name</span>
3537        <input type="text" id="ftr-old" class="ftr-input" value="${escHtml(_ftr.renameOld)}" placeholder="Stoneguard" autocomplete="off">
3538        <span class="form-section-label">New name</span>
3539        <input type="text" id="ftr-new" class="ftr-input" value="${escHtml(_ftr.renameNew)}" placeholder="Stone Guard" autocomplete="off">
3540        <p class="form-helper-text">Whole-word swap across the entire campaign record: entries, summaries, quotes, tallies, transcripts. You'll see grouped counts before anything is written.</p>
3541        ${t}
3542        <div class="ftr-actions">
3543          <button class="btn-new" data-ftr-back>Back</button>
3544          <button class="btn-new" data-ftr-preview>Preview changes</button>
3545        </div>
3546      </div>`,e.querySelector("#ftr-old")?.addEventListener("input",n=>{_ftr.renameOld=n.target.value}),e.querySelector("#ftr-new")?.addEventListener("input",n=>{_ftr.renameNew=n.target.value}),e.querySelector("[data-ftr-preview]")?.addEventListener("click",()=>_ftrRunRename(!1));else{const n=_ftr.entityName?`
3547      <div class="ftr-chip-row"><span class="ftr-chip">Scoped to: <b>${escHtml(_ftr.entityName)}</b>
3548        <button class="ftr-chip-x" data-ftr-unscope title="Search the whole campaign instead" aria-label="Clear scope">\u2715</button></span></div>`:"";e.innerHTML=`
3549      <div class="ftr-pane">
3550        <span class="form-section-label">What actually happened</span>
3551        ${n}
3552        <textarea id="ftr-statement" class="ftr-textarea" rows="5"
3553          placeholder="Thalen never devoured Buppido, the drow fed Buppido to Lolth's spiders after Twig vanished. Thalen's light spell gave Twig her escape moment; he had no direct role.">${escHtml(_ftr.statement)}</textarea>
3554        <p class="form-helper-text">Plain language. Every passage that contradicts this gets found and a minimal rewrite proposed, nothing is written until you approve it. The statement also becomes standing canon so future recaps can't reintroduce the error.</p>
3555        ${t}
3556        <div class="ftr-actions">
3557          <button class="btn-new" data-ftr-back>Back</button>
3558          <button class="btn-new" data-ftr-scan>Scan the record</button>
3559        </div>
3560      </div>`,e.querySelector("#ftr-statement")?.addEventListener("input",a=>{_ftr.statement=a.target.value}),e.querySelector("[data-ftr-unscope]")?.addEventListener("click",()=>{_ftr.entityId=null,_ftr.entityName=null,_ftrRender()}),e.querySelector("[data-ftr-scan]")?.addEventListener("click",_ftrRunScan)}e.querySelector("[data-ftr-back]")?.addEventListener("click",()=>{_ftr.error=null,_ftr.step="mode",_ftrRender()})}function _ftrRenderScanning(e){const t=_ftr.step==="applying";if(e.innerHTML=`
3561    <div class="ftr-pane ftr-scanning">
3562      <div class="ftr-progress"><i></i></div>
3563      <p class="ftr-phase" id="ftr-phase">${t?"Updating the record\u2026":"Scanning the record\u2026"}</p>
3564      <p class="form-helper-text">${t?"Writing your approved changes.":"This reads the whole campaign, give it up to a minute."}</p>
3565    </div>`,!t){let n=0;_ftrPhaseTimer&&clearInterval(_ftrPhaseTimer),_ftrPhaseTimer=setInterval(()=>{const a=document.getElementById("ftr-phase");if(!a||!_ftr||_ftr.step!=="scanning"){clearInterval(_ftrPhaseTimer),_ftrPhaseTimer=null;return}a.textContent=_FTR_SCAN_PHASES[n++%_FTR_SCAN_PHASES.length]},2600)}}
3565function _ftrGroupsHtml(e,t){return`<div class="ftr-groups">${(e||[]).map(a=>{const s=(a.samples||[]).map(o=>o.new?`<div class="ftr-sample"><span class="ftr-old">${escHtml(o.old)}</span><span class="ftr-arrow">\u2192</span><span class="ftr-new">${escHtml(o.new)}</span></div>`:"").join("");return`<div class="ftr-group"><div class="ftr-group-hdr"><b>${a.occurrences!=null?a.occurrences:a.count}</b> ${escHtml(a.label)}</div>${s}</div>`}).join("")}</div>${t?`<p class="ftr-total">${escHtml(t)}</p>`:""}`}async function _ftrRunRename(e){if(!_ftr||_ftr.busy)return;_ftr.error=null,_ftr.busy=!0,_ftr.step=e?"applying":"scanning",_ftrRender();const t=_ftr;try{const n=await api("POST",`/api/campaigns/${currentCampaign.id}/consistency/rename`,{old:_ftr.renameOld,new:_ftr.renameNew,apply:!!e});if(_ftr!==t)return;_ftr.busy=!1,e?(_ftr.receipt=n,_ftr.step="receipt",_ftrRefreshSurfaces()):(_ftr.renamePreview=n,_ftr.step="review")}catch(n){if(_ftr!==t)return;_ftr.busy=!1,_ftr.error=n&&n.message||"That did not work, try again.",_ftr.step="input"}_ftrRender()}function _ftrRenderRenameReview(e){const t=_ftr.renamePreview;if(!t||!t.total_occurrences){e.innerHTML=`
3566      <div class="ftr-pane"><p class="placeholder--padded">No occurrences of \u201C${escHtml(t?t.old:"")}\u201D found in this campaign's record.</p>
3567      <p class="form-helper-text">Check the spelling, the search is whole-word.</p>
3568      <div class="ftr-actions"><button class="btn-new" data-ftr-again>Back</button></div></div>`,e.querySelector("[data-ftr-again]")?.addEventListener("click",()=>{_ftr.step="input",_ftrRender()});return}e.innerHTML=`
3569    <div class="ftr-pane">
3570      <span class="form-section-label">${t.total_occurrences} occurrence${t.total_occurrences!==1?"s":""} \xB7 ${t.total_rows} record${t.total_rows!==1?"s":""}</span>
3571      ${_ftrGroupsHtml(t.groups,`Total: ${t.total_occurrences} changes across ${t.total_rows} records.`)}
3572      <p class="form-helper-text form-helper-text--warn">This will update ${t.total_occurrences} occurrence${t.total_occurrences!==1?"s":""} of \u201C${escHtml(t.old)}\u201D \u2192 \u201C${escHtml(t.new)}\u201D across ${t.total_rows} record${t.total_rows!==1?"s":""}.</p>
3573      <div class="ftr-actions">
3574        <button class="btn-new" data-ftr-again>Back</button>
3575        <button class="btn-new" data-ftr-apply>Apply to all ${t.total_occurrences}</button>
3576      </div>
3577    </div>`,e.querySelector("[data-ftr-again]")?.addEventListener("click",()=>{_ftr.step="input",_ftrRender()}),e.querySelector("[data-ftr-apply]")?.addEventListener("click",()=>_ftrRunRename(!0))}async function _ftrRunScan(){if(!_ftr||_ftr.busy)return;_ftr.error=null,_ftr.busy=!0,_ftr.step="scanning",_ftrRender();const e=_ftr;try{const t=await api("POST",`/api/campaigns/${currentCampaign.id}/consistency/scan`,{statement:_ftr.statement,entity_id:_ftr.entityId});if(_ftr!==e)return;_ftr.busy=!1,_ftr.scan=t,_ftr.choices={},(t.proposals||[]).forEach(n=>{n.editable&&n.verdict==="rewrite"&&(_ftr.choices[n.id]=!0)}),_ftr.step="review"}catch(t){if(_ftr!==e)return;_ftr.busy=!1,_ftr.error=t&&t.message||"The scan failed, try again.",_ftr.step="input"}_ftrRender()}function _ftrRenderReview(e){if(_ftr.mode==="rename"){_ftrRenderRenameReview(e);return}const t=_ftr.scan||{},n=t.proposals||[],a=n.filter(l=>l.editable&&(l.verdict==="rewrite"||l.verdict==="unclear")),s=n.filter(l=>!l.editable||l.verdict==="readonly");if(!a.length&&!s.length){e.innerHTML=`
3578      <div class="ftr-pane"><p class="placeholder--padded">Nothing in the record contradicts that. No changes needed.</p>
3579      <div class="ftr-actions"><button class="btn-new" data-ftr-again>Back</button><button class="btn-new" data-ftr-done>Done</button></div></div>`,e.querySelector("[data-ftr-again]")?.addEventListener("click",()=>{_ftr.step="input",_ftrRender()}),e.querySelector("[data-ftr-done]")?.addEventListener("click",()=>_ftrClose(!0));return}const i=a.filter(l=>_ftr.choices[l.id]).length,o=a.some(l=>_ftr.choices[l.id]&&l.needs_confirm),r=a.map(l=>{const d=!!_ftr.choices[l.id],u=l.verdict==="unclear",p=l.needs_confirm?'<div class="ftr-root-strip form-helper-text--warn">Source notes / transcript: only edit if this session was imported or written up by hand, not live-recorded.</div>':"",m=(l.old_text||"").length>420;return`
3580      <div class="ftr-card${d?" ftr-card--on":""}${u?" ftr-card--unclear":""}" data-ftr-card="${l.id}">
3581        <div class="ftr-card-hdr">
3582          <span class="ftr-card-label">${escHtml(l.label)}</span>
3583          ${u?'<span class="ftr-flag">low confidence</span>':""}
3584          <span class="ftr-card-actions">
3585            <button class="btn-new btn-xs${d?"":" ftr-btn-active"}" data-ftr-reject="${l.id}">Skip</button>
3586            <button class="btn-new btn-xs${d?" ftr-btn-active":""}" data-ftr-approve="${l.id}">${d?"Approved":"Approve"}</button>
3587          </span>
3588        </div>
3589        ${p}
3590        <div class="ftr-diff${m?" ftr-diff--clamp":""}" data-ftr-diff="${l.id}">
3591          <div class="ftr-old">${escHtml(l.old_text||"")}</div>
3592          ${l.new_text?`<div class="ftr-new"><span class="ftr-arrow">\u2192</span>${escHtml(l.new_text)}</div>`:'<div class="form-helper-text">No rewrite proposed, needs your judgment.</div>'}
3593        </div>
3594        ${m?`<button class="btn-new btn-xs" data-ftr-expand="${l.id}">Show full passage</button>`:""}
3595      </div>`}).join(""),c=s.length?`
3596    <span class="form-section-label ftr-ro-hdr">Found, not editable (${s.length})</span>
3597    ${s.map(l=>`
3598      <div class="ftr-card ftr-card--ro">
3599        <div class="ftr-card-hdr"><span class="ftr-card-label">${escHtml(l.label)}</span>
3600          <span class="ftr-card-actions"><button class="btn-new btn-xs" data-ftr-copy="${l.id}">Copy</button></span></div>
3601        <div class="ftr-old-ro">${escHtml((l.old_text||"").slice(0,420))}</div>
3602      </div>`).join("")}`:"";e.innerHTML=`
3603    <div class="ftr-pane">
3604      <div class="ftr-review-top">
3605        <span class="form-section-label">${a.length} proposed change${a.length!==1?"s":""}${s.length?` \xB7 ${s.length} read-only`:""}${t.truncated?" \xB7 capped at 120":""}</span>
3606        <span class="ftr-bulk">
3607          <button class="btn-new btn-xs" data-ftr-all="0">Skip all</button>
3608          <button class="btn-new btn-xs" data-ftr-all="1">Approve all</button>
3609        </span>
3610      </div>
3611      ${_ftrGroupsHtml(t.groups,"")}
3612      <div class="ftr-cards">${r}${c}</div>
3613      ${o?`<label class="ftr-confirm-row"><input type="checkbox" id="ftr-root-ok" ${_ftr.confirmRoot?"checked":""}> I confirm the flagged source notes are safe to edit (imported / hand-written, not a live recording).</label>`:""}
3614      <p class="form-helper-text form-helper-text--warn">This will update ${i} passage${i!==1?"s":""}. \u201C${escHtml((t.statement||"").slice(0,120))}\u201D becomes standing canon for future recaps.</p>
3615      <div class="ftr-actions">
3616        <button class="btn-new" data-ftr-again>Back</button>
3617        <button class="btn-new" data-ftr-apply ${i?"":"disabled"}>Apply ${i} change${i!==1?"s":""}</button>
3618      </div>
3619    </div>`,e.querySelectorAll("[data-ftr-approve]").forEach(l=>l.addEventListener("click",()=>{_ftr.choices[+l.dataset.ftrApprove]=!0,_ftrRender()})),e.querySelectorAll("[data-ftr-reject]").forEach(l=>l.addEventListener("click",()=>{delete _ftr.choices[+l.dataset.ftrReject],_ftrRender()})),e.querySelectorAll("[data-ftr-all]").forEach(l=>l.addEventListener("click",()=>{_ftr.choices={},l.dataset.ftrAll==="1"&&a.forEach(d=>{_ftr.choices[d.id]=!0}),_ftrRender()})),e.querySelectorAll("[data-ftr-expand]").forEach(l=>l.addEventListener("click",()=>{e.querySelector(`[data-ftr-diff="${l.dataset.ftrExpand}"]`)?.classList.remove("ftr-diff--clamp"),l.remove()})),e.querySelectorAll("[data-ftr-copy]").forEach(l=>l.addEventListener("click",()=>{const d=s.find(u=>u.id===+l.dataset.ftrCopy);d&&typeof _tfCopyText=="function"&&_tfCopyText(d.old_text||"")})),e.querySelector("#ftr-root-ok")?.addEventListener("change",l=>{_ftr.confirmRoot=l.target.checked}),e.querySelector("[data-ftr-again]")?.addEventListener("click",()=>{_ftr.step="input",_ftrRender()}),e.querySelector("[data-ftr-apply]")?.addEventListener("click",_ftrRunApply)}async function _ftrRunApply(){if(!_ftr||_ftr.busy)return;const e=Object.keys(_ftr.choices).map(Number);if(e.length){_ftr.busy=!0,_ftr.step="applying",_ftrRender();try{const t=await api("POST",`/api/campaigns/${currentCampaign.id}/consistency/apply`,{scan_id:_ftr.scan.scan_id,approved_ids:e,confirm_transcript_edit:!!_ftr.confirmRoot});_ftr.busy=!1,_ftr.receipt=t,_ftr.step="receipt",_ftrRefreshSurfaces()}catch(t){_ftr.busy=!1,_ftr.error=t&&t.message||"Applying failed, nothing was changed.",_ftr.step="review"}_ftrRender()}}function _ftrRenderReceipt(e){const t=_ftr.receipt||{},n=_ftr.mode==="rename",a=n?`${t.total_occurrences||0} occurrence${(t.total_occurrences||0)!==1?"s":""} updated across ${t.total_rows||0} record${(t.total_rows||0)!==1?"s":""}.`:`${t.applied||0} passage${(t.applied||0)!==1?"s":""} updated.`,s=!n&&(t.skipped_stale||t.skipped_unconfirmed)?`<p class="form-helper-text">${t.skipped_stale?`${t.skipped_stale} skipped (changed since the scan). `:""}${t.skipped_unconfirmed?`${t.skipped_unconfirmed} skipped (source-note edit not confirmed).`:""}</p>`:"";e.innerHTML=`
3620    <div class="ftr-pane ftr-receipt">
3621      <p class="ftr-receipt-line">${escHtml(a)}</p>
3622      ${n?_ftrGroupsHtml(t.groups||[],""):""}
3623      ${s}
3624      ${!n&&t.applied?'<p class="form-helper-text">The correction is now standing canon, future recaps and codex rebuilds will respect it. Undo stays available for 14 days.</p>':""}
3625      <div class="ftr-actions">
3626        ${!n&&t.applied?'<button class="btn-new" data-ftr-undo>Undo this correction</button>':""}
3627        ${!n&&t.applied?'<button class="btn-new" data-ftr-canon>View canon</button>':""}
3628        <button class="btn-new" data-ftr-done>Done</button>
3629      </div>
3630    </div>`,e.querySelector("[data-ftr-canon]")?.addEventListener("click",()=>{_ftr.canonReturn="receipt",_ftr.step="canon",_ftrRender()}),e.querySelector("[data-ftr-done]")?.addEventListener("click",()=>_ftrClose(!0)),e.querySelector("[data-ftr-undo]")?.addEventListener("click",async()=>{if(!(!_ftr||_ftr.busy||!await confirmDialog({title:"Undo this correction?",message:"Every passage this correction changed is restored to its previous text.",confirmLabel:"Undo",danger:!0})||!_ftr)){_ftr.busy=!0;try{const o=await api("POST",`/api/campaigns/${currentCampaign.id}/consistency/undo`,{scan_id:_ftr.scan.scan_id});_ftr.busy=!1,showToast(`Restored ${o.undone} passage${o.undone!==1?"s":""}${o.stale?` (${o.stale} changed since and were left alone)`:""}.`),_ftrClose(!0),_ftrRefreshSurfaces()}catch{_ftr.busy=!1,showToast("Undo failed, nothing was changed.")}}})}function _ftrRefreshSurfaces(){try{typeof loadEntities=="function"&&loadEntities()}catch{}try{typeof _codex2RerenderIfOpen=="function"&&_codex2RerenderIfOpen()}catch{}}async function _ftrRenderCanon(e){e.innerHTML='<div class="ftr-pane ftr-scanning"><div class="ftr-progress"><i></i></div><p class="ftr-phase">Loading campaign canon\u2026</p></div>';let t=[];try{const a=await api("GET",`/api/campaigns/${currentCampaign.id}/consistency/canon`);t=a&&a.notes||[]}catch{e.innerHTML=`<div class="ftr-pane"><p class="ftr-error">Couldn't load the canon list, try again.</p>
3631      <div class="ftr-actions"><button class="btn-new" data-ftr-canon-back>Back</button></div></div>`,e.querySelector("[data-ftr-canon-back]")?.addEventListener("click",()=>{_ftr.step=_ftr.canonReturn||"mode",_ftrRender()});return}if(!_ftr||_ftr.step!=="canon")return;const n=t.length?t.map(a=>`
3632    <div class="ftr-card${a.active?"":" ftr-card--ro"}" data-canon-id="${a.id}">
3633      <div class="ftr-card-hdr">
3634        <span class="ftr-card-label">${a.active?"Active":"Retired"}</span>
3635        <span class="ftr-card-actions">
3636          <button class="btn-new btn-xs" data-canon-toggle="${a.id}" data-canon-active="${a.active?1:0}">${a.active?"Retire":"Restore"}</button>
3637          <button class="btn-new btn-xs btn-new--danger" data-canon-delete="${a.id}">Delete</button>
3638        </span>
3639      </div>
3640      <div class="ftr-diff">${escHtml(a.note)}</div>
3641    </div>`).join(""):'<p class="placeholder--padded">No standing canon yet. Applying a correction records its statement here.</p>';e.innerHTML=`
3642    <div class="ftr-pane">
3643      <span class="form-section-label">Campaign canon \xB7 ${t.length} fact${t.length!==1?"s":""}</span>
3644      <p class="form-helper-text">These facts are injected into every future recap, codex rebuild, and campaign summary so a corrected error can't regenerate. Retire a fact when later play supersedes it.</p>
3645      <div class="ftr-cards">${n}</div>
3646      <div class="ftr-actions"><button class="btn-new" data-ftr-canon-back>Back</button></div>
3647    </div>`,e.querySelector("[data-ftr-canon-back]")?.addEventListener("click",()=>{_ftr.step=_ftr.canonReturn||"mode",_ftrRender()}),e.querySelectorAll("[data-canon-toggle]").forEach(a=>a.addEventListener("click",async()=>{if(!_ftr.busy){_ftr.busy=!0;try{await api("PATCH",`/api/campaigns/${currentCampaign.id}/consistency/canon/${a.dataset.canonToggle}`,{active:a.dataset.canonActive!=="1"}),_ftr.busy=!1,_ftrRender()}catch{_ftr.busy=!1,showToast("Couldn't update that canon fact.")}}})),e.querySelectorAll("[data-canon-delete]").forEach(a=>a.addEventListener("click",async()=>{if(!(_ftr.busy||!await confirmDialog({title:"Delete this canon fact?",message:"Future AI generation will no longer be held to it. (Retiring keeps it on the list, just inactive.)",confirmLabel:"Delete",danger:!0})||!_ftr)){_ftr.busy=!0;try{await api("DELETE",`/api/campaigns/${currentCampaign.id}/consistency/canon/${a.dataset.canonDelete}`),_ftr.busy=!1,_ftrRender()}catch{_ftr.busy=!1,showToast("Couldn't delete that canon fact.")}}}))}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".js-discord-login").forEach(t=>{t.addEventListener("click",()=>{try{window._tfDoorCode&&localStorage.setItem("tf_door_stash",JSON.stringify({code:window._tfDoorCode,consent:!!window._tfDoorConsent,ts:Date.now()}))}catch{}location.href="/api/auth/discord/login"})});const e=window._tfAuthNotice;if(e){const t=document.getElementById("auth-login-error");t&&(t.textContent=e,t.classList.remove("hidden"))}});function _tfRenderPasswordNudge(e){const t=document.getElementById("tf-password-nudge");if(t&&t.remove(),!e||!e.needs_password||sessionStorage.getItem("tf_pw_nudge_dism
3647issed"))return;const n=document.querySelector("#main-app .tab-panel:not(.hidden)")||document.getElementById("main-app");if(!n)return;const a=document.createElement("div");a.id="tf-password-nudge",a.innerHTML='<span>You signed in with Discord. Add a password too, so losing Discord never locks you out of your campaigns.</span><span class="tf-nudge-actions"><button class="btn-new btn-sm" id="tf-pw-nudge-go">Email me a link</button><button class="btn-ghost btn-sm" id="tf-pw-nudge-dismiss">Later</button></span>',n.prepend(a),document.getElementById("tf-pw-nudge-go").addEventListener("click",async()=>{try{await api("POST","/api/auth/forgot-password",{email:e.email}),showToast("Check your email for a set-password link."),a.remove()}catch{showToast("Could not send the link, try again.")}}),document.getElementById("tf-pw-nudge-dismiss").addEventListener("click",()=>{sessionStorage.setItem("tf_pw_nudge_dismissed","1"),a.remove()})}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.