const state = {
  conversations: [], modelAccess: null, conversationId: null, bundle: null, currentView: 'home',
  discoverScenarios: [], discoverTags: [], selectedTags: new Set(), creatorContent: [], creatorCharacters: [], scenarioAutofillResearch:null,
  tagsExpanded: false, canonDrafts: {}, canonSearchResults: {}, home:null, favorites:[], favoriteIds:new Set(), myProfile:null, personas:null, profileCreator:null, appliedPromo:null, billingCatalog:null, checkoutPending:false, rpNavExpanded: localStorage.getItem('melumiyachat_rp_nav_expanded') === 'true', imageDraft:null, importFilename:'', importPreview:null, notifications:{items:[],unread:0}, supportTickets:[],
};
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];

async function api(path, opts={}) {
  try{return await MelumiyaAuth.fetch(path, opts);}
  catch(e){throw new Error(`${path}: ${e?.message||e}`);}
}

function escapeHtml(v=''){ return String(v).replace(/[&<>'"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c])); }
function rpRegexEscape(v=''){return String(v).replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}
function rpKnownSpeakerNames(){const b=state.bundle||{},r=b.conversation||{},st=b.state||{},hc=st.hard_canon||{},wb=st.world_bible||{},sp=hc.scenario_payload||{},names=[];const add=v=>{v=String(v||'').trim();if(v&&v.length<=80&&!names.some(x=>x.toLowerCase()===v.toLowerCase()))names.push(v)};add(hc.base_character);add(hc.character_name);add(r.character_name);add(r.persona_name);add(r.user_name);add(sp.character_name);add(sp.parent_character_snapshot?.title);add(sp.parent_character_snapshot?.name);for(const x of (Array.isArray(wb.characters)?wb.characters:[])){add(x?.name);add(x?.title)}for(const x of (Array.isArray(sp.linked_characters)?sp.linked_characters:[])){add(x?.name);add(x?.title);add(x?.snapshot?.name);add(x?.snapshot?.title)}return names}
function stripRpSpeakerLabels(v=''){let out=String(v??'').replace(/\r\n?/g,'\n');for(const name of rpKnownSpeakerNames()){const re=new RegExp('(^|\n)([\t ]*)\['+rpRegexEscape(name)+'\][\t ]*(?::[\t ]*)?','gi');out=out.replace(re,'$1$2')}out=out.replace(/(^|\n)([\t ]*)\[([A-ZÀ-ÖØ-Þ][A-Za-zÀ-ÖØ-öø-ÿ'’.-]+(?:[\t ]+[A-ZÀ-ÖØ-Þ][A-Za-zÀ-ÖØ-öø-ÿ'’.-]+){1,4})\][\t ]*(?::[\t ]*)?/g,'$1$2');return out}
function renderRpMarkup(v=''){const raw=stripRpSpeakerLabels(v).replace(/\r\n?/g,'\n');let out='',last=0,m;const rx=/(\*[^*\n]+\*|"[^"\n]*")/g;const loose=part=>{if(!part)return;const e=escapeHtml(part);out+=/[\p{L}\p{N}]/u.test(part)?`<span class="rp-narration">${e}</span>`:e};while((m=rx.exec(raw))){loose(raw.slice(last,m.index));const token=m[0];out+=token[0]==='*'?`<span class="rp-narration">${escapeHtml(token.slice(1,-1))}</span>`:escapeHtml(token);last=rx.lastIndex}loose(raw.slice(last));return out}
async function uploadLocalImage(file, outputId){
  if(!file)return;
  if(!['image/png','image/jpeg','image/webp'].includes(file.type)){alert('Choose a PNG, JPG/JPEG, or WEBP image.');return;}
  if(file.size>8*1024*1024){alert('Keep cover images under 8 MB.');return;}
  const dataUrl=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsDataURL(file);});
  const result=await api('/api/images/upload',{method:'POST',body:JSON.stringify({filename:file.name,data_url:dataUrl,kind:outputId==='creatorImage'?'creation':'persona'})});
  const out=$('#'+outputId);if(out){out.value=result.url;out.dispatchEvent(new Event('change'));}
  return result;
}
function chooseLocalImage(outputId){const input=document.createElement('input');input.type='file';input.accept='image/png,image/jpeg,image/webp';input.onchange=async()=>{try{await uploadLocalImage(input.files?.[0],outputId)}catch(e){alert(e.message)}};input.click();}
function pretty(obj){ return JSON.stringify(obj ?? {}, null, 2); }
function imagePromptFromFields(target){
  if(target==='persona') return [$('#personaFormName')?.value,$('#personaFormAppearance')?.value,$('#personaFormBackstory')?.value].filter(Boolean).join('. ');
  if(target==='scenario') return [$('#creatorTitle')?.value,$('#creatorSummary')?.value,'Cover image for the scenario'].filter(Boolean).join('. ');
  return [$('#creatorTitle')?.value,$('#creatorSummary')?.value,'Character portrait'].filter(Boolean).join('. ');
}
function openImageGenerator(target, outputId, initialPrompt=''){
  const aspectDefault=target==='scenario'?'16:9':'1:1';
  openModal('2D Image Generator',`<div class="advanced-explain"><strong>Small artwork generator</strong><span>Made for persona avatars, character portraits and scenario covers. You can edit the description before generating.</span></div><label>What should it show?<textarea id="imgPrompt" rows="6" placeholder="Describe the person or scene...">${escapeHtml(initialPrompt||'')}</textarea></label><div class="form-row triple"><label>Style<select id="imgStyle"><option>Cinematic</option><option>Realistic</option><option>Anime</option><option>Painterly</option><option>Graphic</option></select></label><label>Shape<select id="imgAspect"><option ${aspectDefault==='1:1'?'selected':''}>1:1</option><option>4:5</option><option ${aspectDefault==='16:9'?'selected':''}>16:9</option></select></label><label>Use for<input value="${escapeHtml(target)}" disabled></label></div><div id="imgGenerateNote" class="muted compact">MelumiyaChat uses the cloud image service when you are online. A local mock preview is only used in offline/development fallback mode.</div><div id="imgPreview" class="image-generator-preview"></div><div class="modal-actions"><button id="generate2dBtn" class="primary">✦ Generate image</button><button id="use2dBtn" class="secondary hidden">Use this image</button></div>`);
  let result=null;
  $('#generate2dBtn').onclick=async()=>{const b=$('#generate2dBtn');b.disabled=true;b.textContent='Generating…';try{result=await api('/api/images/generate',{method:'POST',body:JSON.stringify({target,prompt:$('#imgPrompt').value,style:$('#imgStyle').value,aspect:$('#imgAspect').value})});$('#imgPreview').innerHTML=`<img src="${escapeHtml(result.url)}" alt="Generated preview"><div class="muted compact">${escapeHtml(result.plain||'')}</div>`;$('#use2dBtn').classList.remove('hidden')}catch(e){$('#imgGenerateNote').textContent=e.message}finally{b.disabled=false;b.textContent='✦ Generate image'}};
  $('#use2dBtn').onclick=()=>{if(!result)return; const out=$('#'+outputId); if(out){out.value=result.url; out.dispatchEvent(new Event('change'));} closeModal();};
}
function localDate(){ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }
function localTime(){ const d=new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; }

async function init(){
  const params=new URLSearchParams(location.search);
  await MelumiyaAuth.ensure();
  bindNav(); bindActions();
  const requestedConversation=params.get('test')||null;
  const requestedView=params.get('view')||'home';
  if(requestedConversation) state.conversationId=requestedConversation;
  // Activate the requested page before any network/data loading. A failed optional loader
  // must never visually dump a stale embedded page back onto Home.
  showView(requestedView);
  await loadCustomerModelAccess();
  await loadConversations();
  await Promise.all([loadDiscover(), loadPlatformShell()]);
  showView(requestedView);
}

function bindNav(){
  $$('#nav .nav-btn').forEach(btn=>btn.addEventListener('click',()=>showView(btn.dataset.view)));
}
function supportStatusText(v){return ({open:'Open',waiting_on_customer:'Waiting on you',resolved:'Resolved',closed:'Closed'}[String(v)]||String(v||'Open'));}
async function loadHelpTickets(){try{const d=await api('/api/help/tickets');state.supportTickets=d.items||[];renderHelpTickets()}catch(e){const x=$('#myHelpTickets');if(x)x.innerHTML=`<div class="empty-state">${escapeHtml(e.message)}</div>`}}
function renderHelpTickets(){const e=$('#myHelpTickets');if(!e)return;e.innerHTML=(state.supportTickets||[]).length?state.supportTickets.map(t=>`<button class="community-card help-ticket-open" data-id="${escapeHtml(t.id)}" style="text-align:left;color:inherit;width:100%"><div class="community-card-top"><span class="pill">${escapeHtml(supportStatusText(t.status))}</span><span class="muted compact">${escapeHtml(t.reference||'')}</span></div><h3>${escapeHtml(t.subject||'Support request')}</h3><p>${escapeHtml(t.last_message?.body||'Open to read the conversation.')}</p><div class="muted compact">${escapeHtml(t.category||'general')} · Updated ${escapeHtml(new Date(t.updated_at||Date.now()).toLocaleString())}</div></button>`).join(''):'<div class="empty-state">You have no help requests yet.</div>';$$('.help-ticket-open').forEach(b=>b.onclick=()=>openHelpTicket(b.dataset.id));}
async function openHelpTicket(id){try{const d=await api(`/api/help/tickets/${encodeURIComponent(id)}`),c=d.case||{},msgs=d.messages||[];const thread=msgs.map(m=>`<div class="detail-box"><b>${m.author_role==='staff'?'MelumiyaChat Support':'You'}</b><span class="muted compact">${escapeHtml(new Date(m.created_at||Date.now()).toLocaleString())}</span><div style="margin-top:7px;white-space:pre-wrap">${escapeHtml(m.body||'')}</div></div>`).join('')||'<div class="empty-state">No messages.</div>';openModal(c.subject||'Support request',`<div class="advanced-explain"><strong>${escapeHtml(c.reference||'')}</strong><span>${escapeHtml(supportStatusText(c.status))} · ${escapeHtml(c.category||'general')}</span></div><div style="display:grid;gap:9px;margin-top:12px">${thread}</div>${c.status==='closed'?'':'<h4>Reply</h4><textarea id="helpThreadReply" rows="5" placeholder="Reply to MelumiyaChat Support…"></textarea><div class="modal-actions"><button id="sendHelpThreadReply" class="primary">Send reply</button></div>'}`);const btn=$('#sendHelpThreadReply');if(btn)btn.onclick=async()=>{const body=$('#helpThreadReply').value.trim();if(!body)return;btn.disabled=true;try{await api(`/api/help/tickets/${encodeURIComponent(id)}/reply`,{method:'POST',body:JSON.stringify({body})});await loadHelpTickets();await openHelpTicket(id)}catch(e){alert(e.message)}finally{btn.disabled=false}}}catch(e){alert(e.message)}}
function showView(view){
  state.currentView=view;
  document.body.classList.toggle('rp-immersive',view==='chat');
  if(view!=='chat') document.body.classList.remove('rp-settings-open');
  $$('.nav-btn').forEach(b=>b.classList.toggle('active',b.dataset.view===view));
  $$('.view').forEach(v=>v.classList.toggle('active',v.id===`view-${view}`));
  if(view==='home') renderHome();
  if(view==='discover') renderDiscover();
  if(view==='rps') renderMyRps();
  if(view==='create'){}
  if(view==='favorites') renderFavorites();
  if(view==='creations') loadCreatorContent();
  if(view==='personas') renderPersonas();
  if(view==='profile') renderProfile();
  if(view==='plans') renderPlans();
  if(view==='dashboard') renderDashboard();
  if(view==='world') renderWorldEditors();
  if(view==='help') loadHelpTickets();
}
window.addEventListener('focus',async()=>{if(!state.checkoutPending)return;try{await loadPlatformShell();await loadCustomerModelAccess();}catch(e){console.warn('checkout refresh',e)}});
window.addEventListener('message',e=>{
  if(e.origin!==location.origin)return;
  if(e.data?.type!=='melumiyachat:set-view')return;
  const view=String(e.data.view||'home');
  const allowed=new Set(['home','discover','rps','favorites','creations','personas','create','plans','profile','help','chat','dashboard','world']);
  if(allowed.has(view))showView(view);
});

function updateCurrentRpNav(){
  const wrap=$('#currentRpNav'), title=$('#currentRpNavTitle'), toggle=$('#currentRpNavToggle');
  if(!wrap||!title)return;
  const c=state.bundle?.conversation || state.conversations.find(x=>String(x.id)===String(state.conversationId));
  wrap.classList.toggle('hidden',!c);
  wrap.classList.toggle('collapsed',!state.rpNavExpanded);
  if(toggle)toggle.setAttribute('aria-expanded',String(state.rpNavExpanded));
  title.textContent=c?.title || 'No RP selected';
  title.title=c?.scenario_name ? `${c.title} · ${c.scenario_name}` : (c?.title||'');
}
function toggleCurrentRpNav(){state.rpNavExpanded=!state.rpNavExpanded;localStorage.setItem('melumiyachat_rp_nav_expanded',String(state.rpNavExpanded));updateCurrentRpNav();}
async function openRpView(rpId, view='chat'){
  state.conversationId=String(rpId);
  const sel=$('#conversationSelect'); if(sel) sel.value=String(state.conversationId);
  await loadBundle();
  updateCurrentRpNav();
  showView(view);
}

function renderCustomerModelAccess(){
  const x=state.modelAccess||{},sel=$('#modelChoiceSelect');
  if(sel){const cur=sel.value;sel.innerHTML=(x.models||[]).map(m=>`<option value="${escapeHtml(m.key)}">${escapeHtml(m.label)}</option>`).join('');if(cur&&(x.models||[]).some(m=>m.key===cur))sel.value=cur;sel.onchange=renderCustomerModelAccess}
  const u=x.usage||{},p=$('#modelAccessStatus');if(!p)return;
  p.textContent=u.total_remaining==null?`${x.plan_tier||'Free'} plan`:`${x.plan_tier||'Free'} · ${Number(u.total_remaining).toLocaleString()} / ${Number(u.total_limit||0).toLocaleString()} replies left today`;p.className=`status-pill ${u.total_remaining===0?'warn':'good'}`
}
async function loadCustomerModelAccess(){try{state.modelAccess=await api('/api/models/available');renderCustomerModelAccess()}catch(e){console.warn('model access',e)}}

async function loadConversations(){
  state.conversations = await api('/api/conversations');
  const sel=$('#conversationSelect'); sel.innerHTML=state.conversations.map(c=>`<option value="${c.id}">${escapeHtml(c.title)}</option>`).join('');
  if(!state.conversationId && state.conversations.length) state.conversationId=state.conversations[0].id;
  sel.value=state.conversationId; sel.onchange=async()=>{state.conversationId=String(sel.value); await loadBundle();};
  await loadBundle();
  updateCurrentRpNav(); renderHome();
}
async function loadBundle(){
  if(!state.conversationId)return;
  state.bundle=await api(`/api/conversations/${state.conversationId}/bundle`);
  syncSettings(); renderRpImmersionHeader(); renderMessages(); renderSceneStrip(); updateCurrentRpNav(); if(state.currentView==='dashboard')renderDashboard(); if(state.currentView==='world')renderWorldEditors(); if(state.currentView==='rps')renderMyRps();
}


async function loadPlatformShell(){
  try{
    const [home,favs,profile,personas,notifications]=await Promise.all([api('/api/home'),api('/api/favorites'),api('/api/profile/me'),api('/api/personas'),api('/api/notifications').catch(()=>({items:[],unread:0}))]);
    state.home=home; state.favorites=favs; state.favoriteIds=new Set(favs.map(x=>String(x.id))); state.myProfile=profile; state.personas=personas; state.notifications=notifications||{items:[],unread:0};
    renderHome(); renderFavorites(); renderPersonas(); renderProfile(); renderPlans(); renderNotificationBell();
  }catch(e){console.error(e)}
}
function renderNotificationBell(){
  const badge=$('#notificationBadge'); if(!badge)return;
  const n=Number(state.notifications?.unread||0); badge.textContent=n>99?'99+':String(n); badge.classList.toggle('hidden',n<=0);
}
async function refreshNotifications(){
  try{state.notifications=await api('/api/notifications');renderNotificationBell();return state.notifications}catch(e){console.warn('Notifications unavailable',e);return state.notifications||{items:[],unread:0}}
}
function notificationPriorityClass(p){return p==='Critical'?'notification-priority-critical':p==='High'?'notification-priority-high':''}
async function openNotifications(){
  await refreshNotifications(); const data=state.notifications||{items:[],unread:0};
  openModal('Notifications',`<div class="notification-inbox-actions"><button id="markAllNotificationsRead" class="secondary small" ${data.unread?'':'disabled'}>Mark all read</button></div><div id="notificationInbox" class="notification-inbox">${(data.items||[]).length?(data.items||[]).map(n=>`<article class="notification-item ${n.read?'':'unread'}" data-notification-id="${escapeHtml(n.id)}"><div class="notification-item-head"><div><div class="notification-meta"><span class="tag">${escapeHtml(n.kind||'Update')}</span><span class="${notificationPriorityClass(n.priority)}">${escapeHtml(n.priority||'Normal')}</span>${n.read?'':'<span class="tag">New</span>'}</div><h4>${escapeHtml(n.title)}</h4></div><span class="muted compact">${escapeHtml(String(n.sent_at||n.created_at||'').slice(0,10))}</span></div><p>${escapeHtml(n.body||'')}</p>${n.cta_text&&n.cta_url?`<div><a class="secondary button-link small" href="${escapeHtml(n.cta_url)}" target="_blank" rel="noopener">${escapeHtml(n.cta_text)}</a></div>`:''}</article>`).join(''):'<div class="empty-state">No notifications yet.</div>'}</div>`);
  const markAll=$('#markAllNotificationsRead'); if(markAll)markAll.onclick=async()=>{await api('/api/notifications/read-all',{method:'POST'});await refreshNotifications();openNotifications()};
  $$('.notification-item[data-notification-id]').forEach(el=>el.onclick=async e=>{if(e.target.closest('a'))return;if(!el.classList.contains('unread'))return;await api(`/api/notifications/${el.dataset.notificationId}/read`,{method:'POST'});el.classList.remove('unread');await refreshNotifications();});
}

function scenarioFallbackUrl(x){const text=[x?.title,x?.summary,x?.source_name,x?.payload?.card_hook,...(Array.isArray(x?.tags)?x.tags:[])].filter(Boolean).join(' ').toLowerCase();if(/bike|motor|rider|motorcycle|biker/.test(text))return '/static/fallbacks/scenario-bike.jpg';if(/mask|masked|assassin|secret identity|vigilante|spy/.test(text))return '/static/fallbacks/scenario-masked.jpg';if(/dark|mafia|crime|vampire|goth|thriller|danger|war|enemy|military|rival/.test(text))return '/static/fallbacks/scenario-dark.jpg';return '/static/fallbacks/scenario-fantasy.jpg'}
function scenarioActualCoverUrl(x){const p=x?.payload||{},m=x?.metadata||p?.metadata||{};return p.image_url||p.cover_url||p.cover_image_url||p.cover_image||p.scenario_image_url||p.thumbnail_url||p.hero_image_url||p.generated_image_url||m.image_url||m.cover_url||x?.image_url||x?.cover_url||x?.cover_image_url||x?.thumbnail_url||''}
function scenarioCoverUrl(x){return scenarioActualCoverUrl(x)||scenarioFallbackUrl(x)}
function scenarioCardHtml(x,{favoriteButton=true}={}){
  const fav=state.favoriteIds.has(String(x.id)),fallback=scenarioFallbackUrl(x),img=scenarioCoverUrl(x),teaser=x.payload?.card_hook || x.summary || '';
  const subline=x.parent_character?`With <strong>${escapeHtml(x.parent_character)}</strong>`:'Character-flexible scenario';
  return `<article class="scenario-card"><div class="scenario-cover"><img src="${escapeHtml(img)}" data-fallback="${escapeHtml(fallback)}" alt="" loading="lazy" onerror="if(this.src!==this.dataset.fallback){this.src=this.dataset.fallback;this.onerror=null}"></div>
    <div class="scenario-card-top"><div><span class="scenario-rating ${x.content_rating==='NSFW'?'nsfw':''}">${escapeHtml(x.content_rating)}</span><span class="scenario-canon">${escapeHtml(x.canon_mode)}</span></div><div class="scenario-card-icons">${favoriteButton?`<button class="favorite-btn ${fav?'active':''}" data-favorite-id="${x.id}" title="${fav?'Remove from favorites':'Add to favorites'}">${fav?'♥':'♡'}</button>`:''}</div></div>
    <h2>${escapeHtml(x.title)}</h2><div class="scenario-character">${subline}</div><p class="scenario-teaser">${escapeHtml(teaser)}</p>
    <div class="scenario-tags">${(x.tags||[]).slice(0,4).map(t=>`<button class="tag-filter mini" data-card-tag="${escapeHtml(t)}">${escapeHtml(t)}</button>`).join('')}</div>
    <div class="scenario-card-bottom"><button class="creator-link" data-creator="${escapeHtml(x.creator_name||'Creator')}">by ${escapeHtml(x.creator_name||'Creator')}</button><button class="primary start-scenario" data-scenario-id="${x.id}">Start RP</button></div></article>`;
}
function wireScenarioCards(){
  $$('.start-scenario').forEach(b=>b.onclick=()=>openStartScenario(b.dataset.scenarioId));
  $$('[data-card-tag]').forEach(b=>b.onclick=()=>{state.selectedTags.add(b.dataset.cardTag);showView('discover');renderDiscoverTagChips();renderDiscover();});
  $$('.favorite-btn').forEach(b=>b.onclick=async()=>{const id=b.dataset.favoriteId; if(state.favoriteIds.has(String(id))){await api(`/api/favorites/${id}`,{method:'DELETE'});}else{await api(`/api/favorites/${id}`,{method:'POST'});} await loadPlatformShell(); renderDiscover();});
  $$('.creator-link').forEach(b=>b.onclick=()=>openCreatorProfile(b.dataset.creator));
}
function homeProfileAvatar(){const p=state.myProfile||{},name=p.display_name||'there',url=p.avatar_url||p.profile_image_url||p.photo_url||p.image_url||'',box=$('#homeGreetingAvatar');if(!box)return;box.innerHTML=url?`<img src="${escapeHtml(url)}" alt="${escapeHtml(name)}" onerror="this.parentElement.innerHTML='<span>${escapeHtml((name||'?').trim().slice(0,1).toUpperCase())}</span>'">`:`<span>${escapeHtml((name||'?').trim().slice(0,1).toUpperCase())}</span>`;$('#homeGreetingName').textContent=name}
function homeScenarioMatch(c){const key=String(c?.scenario_name||'').trim().toLowerCase(),all=[...(state.discoverScenarios||[]),...(state.home?.featured_scenarios||[]),...(state.favorites||[])];return all.find(x=>String(x?.title||'').trim().toLowerCase()===key)||null}
function conversationCover(c){const x=homeScenarioMatch(c);return x?scenarioCoverUrl(x):scenarioFallbackUrl({title:c?.scenario_name||c?.title||'Story'})}
function conversationHook(c){const x=homeScenarioMatch(c);return x?.payload?.card_hook||x?.summary||'Pick up the story exactly where you left it.'}
function homeRpCard(c){const img=conversationCover(c),fallback=scenarioFallbackUrl({title:c?.scenario_name||c?.title||'Story'});return `<article class="home-rp-card" data-home-rp="${escapeHtml(c.id)}"><img src="${escapeHtml(img)}" data-fallback="${escapeHtml(fallback)}" onerror="if(this.src!==this.dataset.fallback){this.src=this.dataset.fallback;this.onerror=null}" alt=""><div><strong>${escapeHtml(c.title||c.scenario_name||'RP')}</strong><span>${escapeHtml(c.persona_name||'Persona')} · ${escapeHtml(c.scenario_name||'Scenario')}</span></div></article>`}
function wireHomeRpCards(){$$('[data-home-rp]').forEach(x=>x.onclick=()=>openRpView(x.dataset.homeRp,'chat'))}
function renderHome(){
  if(!state.home)return;homeProfileAvatar();const posts=state.home.posts||[],feed=$('#communityFeed');
  if(feed)feed.innerHTML=posts.length?posts.slice(0,1).map(p=>`<article class="community-card home-update-card"><div class="community-card-top"><span class="tag">${escapeHtml(p.post_type||'Update')}</span><span class="muted compact">${escapeHtml((p.published_at||p.created_at||'').slice(0,10))}</span></div><h3>${escapeHtml(p.title)}</h3><p>${escapeHtml(p.body)}</p>${p.cta_text?`<a href="${escapeHtml(p.cta_url||'#')}" class="creator-link">${escapeHtml(p.cta_text)} →</a>`:'<span class="home-update-link">See what’s new →</span>'}</article>`).join(''):'<div class="muted compact">No new updates.</div>';
  const first=state.conversations[0],cont=$('#homeContinue');if(cont){if(first){const img=conversationCover(first),fb=scenarioFallbackUrl({title:first.scenario_name||first.title||'Story'});cont.innerHTML=`<article class="home-continue-card" data-home-rp="${escapeHtml(first.id)}"><img src="${escapeHtml(img)}" data-fallback="${escapeHtml(fb)}" onerror="if(this.src!==this.dataset.fallback){this.src=this.dataset.fallback;this.onerror=null}" alt=""><div class="home-continue-copy"><span>Continue your story</span><h2>${escapeHtml(first.title||first.scenario_name||'Your RP')}</h2><small>${escapeHtml(first.persona_name||'Persona')} · ${escapeHtml(first.scenario_name||'Scenario')}</small><p>${escapeHtml(conversationHook(first))}</p><button class="primary">Continue →</button></div></article>`}else cont.innerHTML='<div class="home-empty"><h2>Your first story begins here.</h2><p>Choose a scenario and start an RP.</p><button class="primary" id="homeEmptyDiscover">Discover scenarios</button></div>'}
  const recent=$('#homeRecentRps');if(recent)recent.innerHTML=state.conversations.slice(0,6).map(homeRpCard).join('')||'<div class="muted compact">No RPs yet.</div>';
  $('#homeStatRps').textContent=state.conversations.length;$('#homeStatPersonas').textContent=state.personas?.items?.length??state.personas?.count??0;const creations=state.myProfile?.creations||[];$('#homeStatCharacters').textContent=creations.filter(x=>x.item_type==='character').length;$('#homeStatScenarios').textContent=creations.filter(x=>x.item_type==='scenario').length;
  const f=$('#homeFeatured');if(f){f.innerHTML=(state.home.featured_scenarios||[]).map(x=>scenarioCardHtml(x)).join('');wireScenarioCards()}wireHomeRpCards();const empty=$('#homeEmptyDiscover');if(empty)empty.onclick=()=>showView('discover');
}
function renderFavorites(){const g=$('#favoritesGrid');if(!g)return;g.innerHTML=state.favorites.length?state.favorites.map(x=>scenarioCardHtml(x)).join(''):'<div class="empty-state discover-empty"><div><h2>No favorites yet</h2><p>Tap ♡ on a scenario to save it here.</p></div></div>';wireScenarioCards();}
function personaLimitText(plan_tier,limit,count){
  if(limit==null)return `<strong>${escapeHtml(plan_tier)}:</strong> ${count} saved · Unlimited persona slots.`;
  const left=Math.max(0,limit-count);
  return `<strong>${escapeHtml(plan_tier)}:</strong> ${count}/${limit} persona slots used · ${left} remaining.${left===0?' Upgrade or delete a persona to create another.':''}`;
}
function personaOptions(selectedId=''){
  const items=state.personas?.items||[];
  return items.map(x=>`<option value="${x.id}" ${String(x.id)===String(selectedId)?'selected':''}>${escapeHtml(x.name)}${x.is_default?' · Default':''}</option>`).join('');
}
function renderPersonas(){
  if(!state.personas)return; const {items=[],plan_tier,limit,count}=state.personas;
  const banner=$('#personaLimit'); if(banner)banner.innerHTML=personaLimitText(plan_tier,limit,count);
  const g=$('#personaGrid'); if(!g)return; g.innerHTML=items.length?items.map(x=>`<article class="persona-card panel">${x.image_url?`<img class="persona-card-avatar" src="${escapeHtml(x.image_url)}" alt="">`:''}<div class="panel-title-row"><div><h2>${escapeHtml(x.name)}</h2>${x.is_default?'<span class="tag">Default</span>':''}</div><div><button class="ghost small edit-persona" data-id="${x.id}">Edit</button><button class="ghost small delete-persona" data-id="${x.id}">Delete</button></div></div><p class="muted">${escapeHtml([x.age_text,x.pronouns,x.occupation].filter(Boolean).join(' · ')||'No details yet.')}</p><p>${escapeHtml(x.backstory||x.notes||'')}</p></article>`).join(''):'<div class="empty-state"><div><h2>Create who you want to play as</h2><p>A persona is your reusable identity inside an RP.</p><button class="primary empty-create-persona">+ Create Persona</button></div></div>';
  $$('.edit-persona').forEach(b=>b.onclick=()=>openPersonaForm(items.find(x=>String(x.id)===String(b.dataset.id))));
  $$('.delete-persona').forEach(b=>b.onclick=async()=>{if(confirm('Delete this persona?')){await api(`/api/personas/${b.dataset.id}`,{method:'DELETE'});await loadPlatformShell();renderPersonas();}});
  $$('.empty-create-persona').forEach(b=>b.onclick=()=>openPersonaForm());
  const btn=$('#newPersonaBtn'); if(btn){const full=limit!=null&&count>=limit;btn.disabled=full;btn.textContent=full?'Persona limit reached':'+ Create Persona';btn.title=full?`${plan_tier} allows ${limit} personas.`:'';}
}
function openPersonaForm(x=null){
  const slot=state.personas?personaLimitText(state.personas.plan_tier,state.personas.limit,state.personas.count):'';
  if(!x && state.personas?.limit!=null && state.personas.count>=state.personas.limit){openModal('Persona limit reached',`<div class="advanced-explain"><strong>No persona slots left on ${escapeHtml(state.personas.plan_tier)}.</strong><span>${escapeHtml(String(state.personas.count))}/${escapeHtml(String(state.personas.limit))} used. You can edit/delete an existing persona or upgrade your plan.</span></div><div class="modal-actions"><button class="primary" data-go-plans>See plans</button></div>`);$$('[data-go-plans]').forEach(b=>b.onclick=()=>{closeModal();showView('plans')});return;}
  openModal(x?'Edit persona':'Create Persona',`<div class="persona-slot-note">${slot}</div><div class="advanced-explain"><strong>This is who you play as.</strong><span>Characters and scenarios can react to this persona, but the AI still must not decide this persona's dialogue, thoughts, consent or major actions for you.</span></div><label>Name*<input id="personaFormName" value="${escapeHtml(x?.name||'')}" placeholder="Mariana, Kyra, Alex…"></label><div class="form-row double"><label>Age<input id="personaFormAge" value="${escapeHtml(x?.age_text||'')}"></label><label>Pronouns<input id="personaFormPronouns" value="${escapeHtml(x?.pronouns||'')}"></label></div><label>Occupation / role<input id="personaFormOccupation" value="${escapeHtml(x?.occupation||'')}"></label><label>Appearance<textarea id="personaFormAppearance" rows="3">${escapeHtml(x?.appearance||'')}</textarea></label><div class="image-field-row"><label>Persona image<input id="personaFormImage" value="${escapeHtml(x?.image_url||'')}" placeholder="Upload or generate a portrait"></label><div class="persona-image-actions"><button type="button" id="personaUploadImage" class="ghost">Upload</button><button type="button" id="personaGenerateImage" class="secondary">✦ Generate portrait</button></div></div><input id="personaUploadFile" type="file" accept="image/png,image/jpeg,image/webp" class="hidden"><label>Backstory<textarea id="personaFormBackstory" rows="5" placeholder="Anything characters may reasonably know about your persona at the start.">${escapeHtml(x?.backstory||'')}</textarea></label><label>Private notes<textarea id="personaFormNotes" rows="3">${escapeHtml(x?.notes||'')}</textarea></label><label class="check-row"><input id="personaFormDefault" type="checkbox" ${x?.is_default?'checked':''}> Make this my default persona</label><div class="modal-actions"><button id="savePersonaForm" class="primary" data-id="${x?.id||''}">Save Persona</button></div>`);
  $('#personaGenerateImage').onclick=()=>openImageGenerator('persona','personaFormImage',imagePromptFromFields('persona'));
  $('#personaUploadImage').onclick=()=>$('#personaUploadFile').click();
  $('#personaUploadFile').onchange=async()=>{const f=$('#personaUploadFile').files?.[0];if(!f)return;const dataUrl=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsDataURL(f)});const up=await api('/api/images/upload',{method:'POST',body:JSON.stringify({filename:f.name,data_url:dataUrl})});$('#personaFormImage').value=up.url;};
}
async function savePersonaForm(id){const body={name:$('#personaFormName').value,age_text:$('#personaFormAge').value,pronouns:$('#personaFormPronouns').value,appearance:$('#personaFormAppearance').value,backstory:$('#personaFormBackstory').value,occupation:$('#personaFormOccupation').value,notes:$('#personaFormNotes').value,image_url:$('#personaFormImage').value,is_default:$('#personaFormDefault').checked};if(!body.name.trim()){alert('Give your persona a name first.');return;}try{await api(id?`/api/personas/${id}`:'/api/personas',{method:id?'PATCH':'POST',body:JSON.stringify(body)});closeModal();await loadPlatformShell();showView('personas');}catch(e){alert(e.message)}}

function profileCreationImage(x){const p=x?.payload||{};return p.image_url||p.cover_url||p.cover_image_url||p.character_image_url||p.scenario_image_url||x.image_url||x.cover_url||x.cover_image_url||'';}
function profileAvatarMarkup(p){const name=p?.display_name||'User',official=String(name).trim().toLowerCase()==='melumiyachat'||String(p?.handle||'').replace(/^@/,'').trim().toLowerCase()==='melumiyachat',url=p?.avatar_url||(official?'/static/branding/melumiyachat_app_icon.png':'');return url?`<img class="profile-avatar profile-avatar-image" src="${escapeHtml(url)}" alt="${escapeHtml(name)}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'profile-avatar',textContent:'${escapeHtml((name||'?')[0].toUpperCase())}'}))">`:`<div class="profile-avatar">${escapeHtml((name||'?')[0].toUpperCase())}</div>`;}
function profileCreationCard(x){const img=profileCreationImage(x);return `<article class="creator-content-card profile-creation-card">${img?`<img class="profile-creation-cover" src="${escapeHtml(img)}" alt="${escapeHtml(x.title||'Creation')}" onerror="this.remove()">`:''}<div class="profile-creation-copy"><span class="tag">${escapeHtml(x.item_type||'creation')}</span><h2>${escapeHtml(x.title||'Untitled')}</h2><p>${escapeHtml(x.summary||'')}</p></div></article>`;}
function renderProfile(){
  const p=state.myProfile;if(!p)return;const card=$('#profileCard');if(card)card.innerHTML=`<div class="profile-main">${profileAvatarMarkup(p)}<div><h2>${escapeHtml(p.display_name)}</h2><p class="muted">@${escapeHtml(p.handle)} · ${escapeHtml(p.plan_tier)} plan${p.location?` · ${escapeHtml(p.location)}`:''}</p><p>${escapeHtml(p.bio||'No bio yet.')}</p><div class="profile-stats"><span><b>${p.creation_count||0}</b> public creations</span><span><b>${p.favorite_count||0}</b> favorites received</span></div><button class="secondary small profile-plan-btn" data-go-plans>Explore ${p.plan_tier==='Free'?'paid plans':'plans'}</button></div></div>`;
  const g=$('#profileCreations');if(g)g.innerHTML=(p.creations||[]).map(profileCreationCard).join('')||'<div class="empty-state">No public creations yet.</div>';
}

function renderPlans(){
  const p=state.myProfile||{plan_tier:'Free'}; const current=p.plan_tier||'Free';
  const pill=$('#plansCurrentTier'); if(pill)pill.textContent=`Current: ${current}`;
  const side=$('#sidebarPlanPill'); if(side)side.textContent=`${current} plan`;
  const tiers=[
    {name:'Free',monthly:'€0',annual:'',kicker:'Start here',tagline:'A generous local-model allowance for discovering persistent RP.',features:['Up to 500 AI replies/day','One shared daily pool across Free models','Qwen3.5 4B','Core MelumiyaChat memory & continuity','2 persona slots']},
    {name:'Plus',monthly:'€9.99/month',annual:'€49.99 / 6 months · €99 / year',kicker:'More choice',tagline:'A much larger daily pool plus both DeepSeek styles.',features:['Up to 2,000 AI replies/day','One shared pool across every included model','DeepSeek V4 Flash + DeepSeek V3.1 + Free model','Everything in Free','5 persona slots']},
    {name:'Pro',monthly:'€16.99/month',annual:'€84.99 / 6 months · €169 / year',kicker:'Most popular',tagline:'Premium persistent RP with a huge shared daily allowance.',features:['Up to 4,000 AI replies/day','One shared pool across every included model','Qwen3.6 35B + Plus models + Free model','Everything in Plus','15 persona slots']},
    {name:'Ultra',monthly:'€29.99/month',annual:'€149.99 / 6 months · €299 / year',kicker:'Best available',tagline:'The largest model and the largest daily allowance.',features:['Up to 5,000 AI replies/day','One shared pool across every included model','Qwen3.5 122B + every lower model','Use the full pool on any included model','Unlimited persona slots']},
  ];
  const grid=$('#plansGrid'); if(!grid)return;
  const promo=state.appliedPromo;
  grid.innerHTML=tiers.map(t=>{const isCurrent=t.name===current;const discountPromo=promo&&promo.kind!=='tier_reward';const tierReward=promo?.kind==='tier_reward'&&promo.target_tier===t.name;return `<article class="plan-card ${isCurrent?'current-plan':''} ${tierReward?'reward-plan':''}"><div class="plan-card-top"><div><p class="eyebrow">${escapeHtml(t.kicker)}</p><h2>${escapeHtml(t.name)}</h2></div>${isCurrent?'<span class="status-pill good">Current plan</span>':''}</div><div class="plan-price"><strong>${t.monthly}</strong>${t.annual?`<span>${t.annual}</span>`:''}</div>${discountPromo&&t.name!=='Free'?`<div class="plan-promo-badge">${escapeHtml(promo.code)} · ${Number(promo.discount_percent||0)}% off</div>`:''}${tierReward?`<div class="plan-promo-badge reward">🎁 ${escapeHtml(promo.code)} · ${promo.duration_days?promo.duration_days+' days':'no expiry'} prize</div>`:''}<p class="plan-tagline">${escapeHtml(t.tagline)}</p><div class="plan-features">${t.features.map(x=>`<div><span>✓</span>${escapeHtml(x)}</div>`).join('')}</div>${tierReward?`<button class="primary full redeem-tier-reward" data-code="${escapeHtml(promo.code)}">Redeem ${escapeHtml(t.name)} reward</button>`:(isCurrent?'<button class="ghost full" disabled>Current plan</button>':`<button class="primary full plan-cta" data-plan-tier="${t.name}">Choose ${t.name}</button>`)}<div class="plan-model-note">Model access is cumulative: higher tiers keep access to approved lower-tier models too.</div></article>`}).join('');
  $$('.plan-cta').forEach(b=>b.onclick=()=>openPlanCheckoutPreview(b.dataset.planTier));
  $$('.redeem-tier-reward').forEach(b=>b.onclick=()=>redeemTierReward(b.dataset.code));
  const input=$('#planPromoCode'), notice=$('#planPromoNotice');
  if(input && promo) input.value=promo.code;
  if(notice){
    if(promo){
      notice.className='promo-notice good';
      if(promo.kind==='tier_reward') notice.innerHTML=`<strong>${escapeHtml(promo.code)}</strong> is a ${escapeHtml(promo.target_tier)} gift. <button id="promoRedeemNow" class="primary small">Redeem now</button>`;
      else notice.textContent=`${promo.code} applied — ${Number(promo.discount_percent||0)}% off one eligible paid checkout.${promo.creator_name?` Creator: ${promo.creator_name}.`:''}`;
    } else {notice.className='promo-notice muted';notice.textContent='No code applied.';}
  }
  const perk=$('#planPerksPanel');
  if(perk){
    const currentGift=(p.plan_source||'').startsWith('reward:') || (p.plan_source||'').startsWith('owner:');
    const discount=p.personal_discount_offer;
    const giftNote=currentGift?`<div class="perk-card"><p class="eyebrow">Current gifted / manual access</p><h3>${escapeHtml(p.plan_tier)} active</h3><p class="muted compact">Source: ${escapeHtml((p.plan_source||'Account grant').replace('reward:','Reward code ').replace('owner:','Account grant '))}${p.plan_expires_at?` · Expires ${escapeHtml(String(p.plan_expires_at).slice(0,10))}`:' · No expiry set'}</p></div>`:'';
    const discountNote=discount?`<div class="perk-card"><p class="eyebrow">Personal offer for your account</p><h3>${Number(discount.discount_percent||0)}% off</h3><p class="muted compact">This offer is assigned directly to your account${discount.expires_at?` until ${escapeHtml(String(discount.expires_at).slice(0,10))}`:''}. You do not need a separate code for this one.</p></div>`:'';
    const help=`<div class="perk-card"><p class="eyebrow">Where do I enter a code?</p><h3>Right above the plans</h3><p class="muted compact">Paste the code in the input above and choose “Apply / check code”. If the code is a free-tier gift, you will get a redeem button instantly.</p></div>`;
    perk.innerHTML=`<div class="plan-perks-grid">${giftNote}${discountNote}${help}</div>`;
  }
  const redeem=$('#promoRedeemNow'); if(redeem)redeem.onclick=()=>redeemTierReward(promo.code);
}
async function applyPlanPromoCode(code){
  const cleaned=(code||'').trim();
  if(!cleaned){state.appliedPromo=null;renderPlans();return null;}
  const r=await api(`/api/promos/validate?code=${encodeURIComponent(cleaned)}`);
  if(!r.valid){state.appliedPromo=null;renderPlans();throw new Error(r.message||'That code is not valid.');}
  state.appliedPromo=r;renderPlans();return r;
}
async function redeemTierReward(code){
  try{const r=await api('/api/rewards/redeem',{method:'POST',body:JSON.stringify({code})});state.appliedPromo=null;await loadPlatformShell();showView('plans');openModal('Reward activated 🎁',`<div class="advanced-explain"><strong>${escapeHtml(r.plan_tier)} is active.</strong><span>${escapeHtml(r.message)}</span></div><div class="modal-actions"><button id="rewardDone" class="primary">Done</button></div>`);setTimeout(()=>{if($('#rewardDone'))$('#rewardDone').onclick=closeModal},0)}catch(e){openModal('Could not redeem reward',`<p>${escapeHtml(e.message)}</p>`)}
}
async function loadBillingCatalog(){
  if(state.billingCatalog)return state.billingCatalog;
  state.billingCatalog=await api('/api/billing/catalog');
  return state.billingCatalog;
}
function billingPeriodLabel(period){return ({monthly:'Monthly',six_months:'6 months',annual:'Yearly'})[period]||period;}
function billingPriceLabel(item){
  const amount=Number(item?.unit_amount||0)/100;
  const money=new Intl.NumberFormat('en-IE',{style:'currency',currency:String(item?.currency||'eur').toUpperCase()}).format(amount);
  if(item?.billing_period==='monthly')return `${money} / month`;
  if(item?.billing_period==='six_months')return `${money} / 6 months`;
  if(item?.billing_period==='annual')return `${money} / year`;
  return money;
}
async function startPlanCheckout(tier,period,button){
  // Open the checkout window immediately so normal browser popup protection does not block it
  // while we wait for the authenticated server to create the Stripe Checkout Session.
  const checkoutWindow=window.open('about:blank','melumiyachat_checkout');
  if(checkoutWindow){checkoutWindow.document.title='Opening MelumiyaChat checkout…';checkoutWindow.document.body.innerHTML='<p style="font-family:system-ui;padding:24px">Opening secure MelumiyaChat checkout…</p>';}
  const old=button?.textContent;if(button){button.disabled=true;button.textContent='Opening checkout…';}
  try{
    const promo=state.appliedPromo;
    const referral=(promo?.kind==='influencer_discount')?(promo.code||''):'';
    if(promo && promo.kind!=='influencer_discount' && promo.kind!=='tier_reward'){
      const claimed=await api('/api/rewards/redeem-discount',{method:'POST',body:JSON.stringify({code:promo.code})});
      state.appliedPromo={...promo,kind:'account_discount',discount_percent:Number(claimed.discount_percent||promo.discount_percent||0),message:claimed.message||promo.message};
    }
    const r=await api('/api/billing/checkout',{method:'POST',body:JSON.stringify({tier,billing_period:period,referral_code:referral})});
    if(!r?.url)throw new Error('Checkout did not return a secure payment URL.');
    state.checkoutPending=true;
    if(checkoutWindow)checkoutWindow.location.href=r.url;else window.open(r.url,'_blank','noopener');
    openModal('Checkout opened',`<div class="advanced-explain"><strong>Stripe Checkout opened in your browser.</strong><span>Complete or cancel the checkout there, then come back to MelumiyaChat.</span></div><div class="friendly-item"><strong>${escapeHtml(tier)} · ${escapeHtml(billingPeriodLabel(period))}</strong><div class="muted compact">Your plan is granted by the server after Stripe confirms the payment. The desktop app never stores card details.</div></div><div class="modal-actions"><button id="refreshPlanAfterCheckout" class="primary">I completed checkout — refresh access</button><button id="closeCheckoutOpened" class="secondary">Close</button></div>`);
    setTimeout(()=>{
      const refresh=$('#refreshPlanAfterCheckout'),close=$('#closeCheckoutOpened');
      if(refresh)refresh.onclick=async()=>{refresh.disabled=true;refresh.textContent='Refreshing…';try{await api('/api/billing/refresh',{method:'POST',body:'{}'});await loadPlatformShell();await loadCustomerModelAccess();state.checkoutPending=false;closeModal();showView('plans');}catch(e){refresh.disabled=false;refresh.textContent='Try refresh again';alert(e.message)}};
      if(close)close.onclick=closeModal;
    },0);
  }catch(e){if(checkoutWindow)checkoutWindow.close();openModal('Could not open checkout',`<p>${escapeHtml(e.message)}</p>`);}
  finally{if(button){button.disabled=false;button.textContent=old||'Buy';}}
}
async function openPlanCheckoutPreview(tier){
  const promo=state.appliedPromo;
  const personal=state.myProfile?.personal_discount_offer;
  try{
    const catalog=await loadBillingCatalog();
    const choices=(catalog.items||[]).filter(x=>x.tier===tier).sort((a,b)=>['monthly','six_months','annual'].indexOf(a.billing_period)-['monthly','six_months','annual'].indexOf(b.billing_period));
    if(!choices.length)throw new Error(`${tier} is not currently available for checkout.`);
    openModal(`Choose ${tier}`,`<div class="advanced-explain"><strong>Choose how you want to pay.</strong><span>Checkout is handled securely by Stripe. MelumiyaChat receives the resulting entitlement from the server; card details never enter the desktop app.</span></div>${personal?`<div class="friendly-item"><strong>Personal account offer</strong><div class="muted compact">${Number(personal.discount_percent||0)}% off is attached to your account${personal.expires_at?` until ${escapeHtml(String(personal.expires_at).slice(0,10))}`:''} and will be applied server-side when eligible.</div></div>`:''}${promo?.kind==='influencer_discount'?`<div class="friendly-item"><strong>Creator code: ${escapeHtml(promo.code)}</strong><div class="muted compact">${Number(promo.discount_percent||0)}% off the eligible initial purchase will be passed to checkout.</div></div>`:''}<div class="friendly-list billing-choice-list">${choices.map(x=>`<button class="community-card billing-choice" data-billing-tier="${escapeHtml(tier)}" data-billing-period="${escapeHtml(x.billing_period)}" style="text-align:left;color:inherit;width:100%"><div class="community-card-top"><strong>${escapeHtml(billingPeriodLabel(x.billing_period))}</strong><span class="status-pill good">${escapeHtml(billingPriceLabel(x))}</span></div><p>${x.billing_period==='six_months'?'One 6-month fixed-term purchase.':'Recurring subscription managed through Stripe.'}</p><span class="primary small">Continue to secure checkout →</span></button>`).join('')}</div><div class="modal-actions"><button id="closePlanPreview" class="secondary">Close</button></div>`);
    setTimeout(()=>{
      $$('.billing-choice').forEach(b=>b.onclick=()=>startPlanCheckout(b.dataset.billingTier,b.dataset.billingPeriod,b));
      const close=$('#closePlanPreview');if(close)close.onclick=closeModal;
    },0);
  }catch(e){
    openModal(`Choose ${tier}`,`<div class="advanced-explain"><strong>Checkout is temporarily unavailable.</strong><span>${escapeHtml(e.message)}</span></div><div class="modal-actions"><a class="secondary" href="https://melumiyachat.pages.dev/plans/" target="_blank" rel="noopener">Open official Plans page</a><button id="closePlanPreview" class="secondary">Close</button></div>`);
    setTimeout(()=>{const close=$('#closePlanPreview');if(close)close.onclick=closeModal},0);
  }
}

function openProfileEditor(){const p=state.myProfile;openModal('Edit profile',`<label>Display name<input id="profileName" value="${escapeHtml(p.display_name||'')}"></label><label>Bio<textarea id="profileBio" rows="5">${escapeHtml(p.bio||'')}</textarea></label><label>Location<input id="profileLocation" value="${escapeHtml(p.location||'')}"></label><label>Avatar image URL<input id="profileAvatar" value="${escapeHtml(p.avatar_url||'')}"></label><label>Links (one per line)<textarea id="profileLinks" rows="4">${escapeHtml((p.links||[]).join('\n'))}</textarea></label><div class="modal-actions"><button id="saveProfileForm" class="primary">Save profile</button></div>`);}
async function openCreatorProfile(creator){try{const p=await api(`/api/profiles/${encodeURIComponent(creator)}`);openModal(`${p.display_name}'s profile`,`<div class="profile-main">${profileAvatarMarkup(p)}<div><h2>${escapeHtml(p.display_name)}</h2><p class="muted">${p.handle?'@'+escapeHtml(p.handle):''}</p><p>${escapeHtml(p.bio||'')}</p></div></div><h3>Creations</h3><div class="profile-creation-list">${(p.creations||[]).map(profileCreationCard).join('')||'<div class="empty-state">No public creations yet.</div>'}</div>`);}catch(e){openModal('Creator',`<p>${escapeHtml(e.message)}</p>`);}}

async function loadDiscover(){
  try{
    [state.discoverScenarios,state.discoverTags]=await Promise.all([api('/api/discover/scenarios'),api('/api/discover/tags')]);
    renderDiscoverTagChips(); renderDiscover(); renderMyRps();
  }catch(e){console.error(e)}
}
function updateDiscoverTagsToggle(){
  const box=$('#discoverTagChips'), btn=$('#discoverTagsToggle'), count=$('#selectedTagCount');
  if(count) count.textContent=String(state.selectedTags.size);
  if(!box||!btn)return;
  const mobile=window.innerWidth<=760;
  box.classList.toggle('mobile-collapsed',mobile&&!state.tagsExpanded);
  btn.setAttribute('aria-expanded',String(!mobile||state.tagsExpanded));
  btn.innerHTML=`Tags${state.selectedTags.size?` <span class="tag-count">${state.selectedTags.size}</span>`:''}<span class="tag-chevron">${state.tagsExpanded?'▲':'▼'}</span>`;
}
function renderDiscoverTagChips(){
  const box=$('#discoverTagChips'); if(!box)return;
  box.innerHTML=state.discoverTags.map(t=>`<button class="tag-filter ${state.selectedTags.has(t)?'active':''}" data-discover-tag="${escapeHtml(t)}">${escapeHtml(t)}</button>`).join('');
  $$('[data-discover-tag]').forEach(b=>b.onclick=()=>{const t=b.dataset.discoverTag;if(state.selectedTags.has(t))state.selectedTags.delete(t);else state.selectedTags.add(t);renderDiscoverTagChips();renderDiscover();});
  updateDiscoverTagsToggle();
}
function filteredDiscoverScenarios(){
  const q=($('#scenarioSearch')?.value||'').trim().toLowerCase();
  const rating=$('#discoverRating')?.value||''; const canon=$('#discoverCanon')?.value||''; const creator=($('#creatorSearch')?.value||'').trim().toLowerCase();
  return state.discoverScenarios.filter(x=>{
    const tags=(x.tags||[]).map(String); const hay=[x.title,x.summary,x.parent_character,x.creator_name,...tags].join(' ').toLowerCase();
    if(q && !hay.includes(q))return false;
    if(creator && !String(x.creator_name||'').toLowerCase().includes(creator))return false;
    if(rating && x.content_rating!==rating)return false;
    if(canon && x.canon_mode!==canon)return false;
    for(const t of state.selectedTags){if(t==='SFW'||t==='NSFW'){if(x.content_rating!==t)return false;}else if(!tags.some(z=>z.toLowerCase()===t.toLowerCase()))return false;}
    return true;
  });
}
function renderDiscover(){
  const grid=$('#scenarioDiscoverGrid'); if(!grid)return;
  const items=filteredDiscoverScenarios();
  $('#discoverResultCount').textContent=`${items.length} scenario${items.length===1?'':'s'} found`;
  grid.innerHTML=items.length?items.map(x=>scenarioCardHtml(x)).join(''):'<div class="empty-state discover-empty"><div><h2>No scenarios match those filters</h2><p>Try removing a tag or searching for something else.</p></div></div>';
  wireScenarioCards();
}
function openStartScenario(id,selectedPersonaId=''){
  const x=state.discoverScenarios.find(s=>String(s.id)===String(id)); if(!x)return;
  const opening=x.payload?.opening_prompt || x.payload?.opening || '';
  const teaser=x.payload?.card_hook || x.summary || '';
  const cover=x.payload?.image_url||x.payload?.cover_url||x.payload?.cover_image_url||x.image_url||x.cover_url||x.cover_image_url||'';
  const personas=state.personas?.items||[];
  const defaultPersona=personas.find(p=>p.is_default)||personas[0];
  const chosen=selectedPersonaId||defaultPersona?.id||'';
  const limit=state.personas?.limit, count=state.personas?.count||0, full=limit!=null&&count>=limit;
  const personaBlock=personas.length?`<div class="start-persona-box"><div class="panel-title-row"><div><strong>Who are you playing?</strong><div class="muted compact">Choose one of your saved personas for this RP.</div></div><button id="startCreatePersonaBtn" class="secondary small" ${full?'disabled':''}>+ Create Persona</button></div><select id="startRpPersona">${personas.map(p=>`<option value="${p.id}" data-name="${escapeHtml(p.name)}" ${String(p.id)===String(chosen)?'selected':''}>${escapeHtml(p.name)}${p.is_default?' · Default':''}</option>`).join('')}</select><div class="muted compact">${personaLimitText(state.personas.plan_tier,limit,count)}</div></div>`:`<div class="start-persona-box needs-persona"><strong>Create a persona before starting</strong><p class="muted">A persona is the identity you control in the RP.</p><button id="startCreatePersonaBtn" class="primary">+ Create Persona</button></div>`;
  openModal('Start a new RP',`<div class="start-scenario-summary">${cover?`<img class="start-scenario-cover" src="${escapeHtml(cover)}" alt="" onerror="this.remove()">`:''}<span class="tag">${escapeHtml(x.content_rating)}</span><h2>${escapeHtml(x.title)}</h2><p class="start-scenario-teaser">${escapeHtml(teaser)}</p><p>${escapeHtml(x.summary)}</p>${x.parent_character?`<p class="muted">Base character: <strong>${escapeHtml(x.parent_character)}</strong></p>`:''}${opening?`<div class="scenario-opening-preview"><div class="scenario-opening-label">Opening scene</div><div class="scenario-opening-copy">${escapeHtml(opening)}</div></div>`:''}</div><label>Your RP name<input id="startRpTitle" value="${escapeHtml(x.title)}"></label>${personaBlock}<div id="startPersonaInline" class="inline-persona-create hidden"><h3>Quick Create Persona</h3><div class="form-row double"><label>Name*<input id="startPersonaName"></label><label>Pronouns<input id="startPersonaPronouns"></label></div><div class="form-row double"><label>Age<input id="startPersonaAge"></label><label>Occupation / role<input id="startPersonaOccupation"></label></div><label>Appearance<textarea id="startPersonaAppearance" rows="3"></textarea></label><label>Backstory<textarea id="startPersonaBackstory" rows="4"></textarea></label><label class="check-row"><input id="startPersonaDefault" type="checkbox"> Make this my default persona</label><button id="saveStartPersona" class="primary">Save Persona & use it</button><div id="startPersonaError" class="muted compact"></div></div><div class="form-row double"><label>First-message date<input id="startRpDate" type="date" value="${localDate()}"></label><label>First-message time<input id="startRpTime" type="time" value="${localTime()}"></label></div><div class="advanced-explain"><strong>Separate universe:</strong><span>You can start this scenario again later. Each RP keeps its own memories, relationships, secrets and timeline. MelumiyaChat will begin with the authored opening scene automatically.</span></div><div class="modal-actions"><button id="confirmStartScenario" class="primary" data-id="${id}" ${personas.length?'':'disabled'}>Start this RP</button></div>`);
  if($('#startCreatePersonaBtn'))$('#startCreatePersonaBtn').onclick=()=>{$('#startPersonaInline').classList.toggle('hidden')};
  if($('#saveStartPersona'))$('#saveStartPersona').onclick=async()=>{const name=$('#startPersonaName').value.trim();if(!name){$('#startPersonaError').textContent='Give the persona a name first.';return;}try{const created=await api('/api/personas',{method:'POST',body:JSON.stringify({name,pronouns:$('#startPersonaPronouns').value,age_text:$('#startPersonaAge').value,occupation:$('#startPersonaOccupation').value,appearance:$('#startPersonaAppearance').value,backstory:$('#startPersonaBackstory').value,notes:'',image_url:'',is_default:$('#startPersonaDefault').checked})});state.personas=await api('/api/personas');openStartScenario(id,created.id);}catch(e){$('#startPersonaError').textContent=e.message}};
}
async function startScenario(id){
  const sel=$('#startRpPersona'); if(!sel){alert('Create a persona first.');return;}
  const opt=sel.selectedOptions?.[0]; const personaName=opt?.dataset?.name||opt?.textContent?.replace(' · Default','')||'Persona';
  const r=await api(`/api/discover/scenarios/${id}/start`,{method:'POST',body:JSON.stringify({title:$('#startRpTitle').value,persona_id:sel.value,persona_name:personaName,start_date:$('#startRpDate').value,start_time:$('#startRpTime').value})});
  closeModal(); state.conversationId=r.conversation_id; await loadConversations(); showView('chat');
}
function renderMyRps(){
  const grid=$('#myRpsGrid'); if(!grid)return;
  grid.innerHTML=state.conversations.length?state.conversations.map(c=>`<article class="rp-card ${String(c.id)===String(state.conversationId)?'current':''}">
    <div class="rp-card-head"><span class="tag">${escapeHtml(c.canon_mode)}</span>${String(c.id)===String(state.conversationId)?'<span class="status-pill good">Current RP</span>':''}</div>
    <h2>${escapeHtml(c.title)}</h2>
    <p><strong>${escapeHtml(c.scenario_name)}</strong></p>
    <div class="rp-meta"><span>Persona: ${escapeHtml(c.persona_name)}</span><span>Updated: ${escapeHtml(String(c.updated_at||'').slice(0,16))}</span></div>
    <div class="rp-inside-label">Inside this RP</div>
    <div class="rp-section-actions">
      <button class="secondary rp-open" data-rp-id="${c.id}" data-rp-view="chat">✦ Chat</button>
      <button class="ghost rp-open" data-rp-id="${c.id}" data-rp-view="dashboard">◫ Story Dashboard</button>
      <button class="ghost rp-open" data-rp-id="${c.id}" data-rp-view="world">⌘ World Bible</button>
    </div>
  </article>`).join(''):'<div class="empty-state"><div><h2>No RPs yet</h2><p>Choose a scenario and start your first story.</p></div></div>';
  $$('.rp-open').forEach(b=>b.onclick=()=>openRpView(b.dataset.rpId,b.dataset.rpView));
}


function importPlatformOptions(){
  return ['Other / plain text','Character.AI','SpicyChat','SillyTavern','JanitorAI','Chub / Venus','ChatGPT','Other JSON export'].map(x=>`<option>${escapeHtml(x)}</option>`).join('');
}
function openChatImport(){
  state.importFilename=''; state.importPreview=null;
  const current=state.bundle?.conversation;
  openModal('Import an old RP / chat',`<div class="import-intro"><strong>Bring the history with you.</strong><span>MelumiyaChat preserves the imported transcript as evidence, scans it to rebuild this RP's World Bible and Story Dashboard, reconstructs recurring characters' personality from repeated behaviour, and leaves every extracted fact editable.</span></div>
    <div class="form-row double"><label>Where is it from?<select id="importPlatform">${importPlatformOptions()}</select></label><label>Where should it go?<select id="importDestination"><option value="new">New separate RP (recommended)</option>${current?'<option value="current">Add to current RP</option>':''}</select></label></div>
    <div id="importNewRpFields"><div class="form-row double"><label>New RP name<input id="importRpTitle" value="Imported RP"></label><label>Your persona name<input id="importPersona" value="${escapeHtml(current?.persona_name||'Mariana')}"></label></div></div>
    <label class="import-file-label">Upload export file <input id="importFile" type="file" accept=".txt,.md,.json,.jsonl,.csv,.html,text/plain,application/json"><span class="muted compact">TXT, JSON, JSONL, Markdown and most text exports work. If a platform export is weird, paste the transcript below.</span></label>
    <label>Or paste the transcript<textarea id="importTranscript" rows="10" placeholder="You: ...\nCharacter: ..."></textarea></label>
    <div class="form-row double"><label>Analysis<select id="importAnalysisMode"><option value="quick">Quick local scan — free, scans the whole transcript</option><option value="deep">Deep AI enrichment — uses the selected generation model</option></select></label><label id="importUserSpeakerWrap" class="hidden">Which speaker is you?<select id="importUserSpeaker"></select></label></div>
    <div class="import-rule"><strong>Important:</strong> high-confidence structure is filled automatically. Personality is treated as an editable interpretation built from repeated evidence — one angry scene does not make a character aggressive. Uncertain findings are marked for review. Your edits always win, and imported chats are never training data by default.</div>
    <div id="importPreviewBox" class="import-preview-box"><div class="muted">Preview the transcript first so MelumiyaChat can detect speakers and format.</div></div>
    <div class="modal-actions"><button id="previewImportBtn" class="secondary">1. Preview transcript</button><button id="applyImportBtn" class="primary" disabled>2. Import & build this RP</button></div>`);
  $('#importDestination').onchange=()=>$('#importNewRpFields').classList.toggle('hidden',$('#importDestination').value==='current');
  $('#importFile').onchange=async e=>{const f=e.target.files?.[0];if(!f)return;state.importFilename=f.name;try{$('#importTranscript').value=await f.text();$('#importPreviewBox').innerHTML=`<div class="good-text">✓ Loaded ${escapeHtml(f.name)}. Now preview it.</div>`;}catch(err){$('#importPreviewBox').textContent=`Could not read that file: ${err.message}`;}};
  $('#previewImportBtn').onclick=previewChatImport;
  $('#applyImportBtn').onclick=applyChatImport;
}
async function previewChatImport(){
  const text=$('#importTranscript').value.trim(); if(!text){$('#importPreviewBox').innerHTML='<div class="bad-text">Paste a transcript or choose a file first.</div>';return;}
  const b=$('#previewImportBtn');b.disabled=true;b.textContent='Scanning…';
  try{
    const r=await api('/api/import/preview',{method:'POST',body:JSON.stringify({source_platform:$('#importPlatform').value,transcript:text})});state.importPreview=r;
    const options=(r.speakers||[]).map(x=>`<option value="${escapeHtml(x.name)}">${escapeHtml(x.name)} · ${x.count} messages</option>`).join('');
    $('#importUserSpeaker').innerHTML=options||'<option>Mariana</option>'; const persona=$('#importPersona')?.value?.trim().toLowerCase(); const match=(r.speakers||[]).find(x=>x.name.toLowerCase()===persona)|| (r.speakers||[]).find(x=>['you','user','me'].includes(x.name.toLowerCase())); if(match)$('#importUserSpeaker').value=match.name;
    $('#importUserSpeakerWrap').classList.remove('hidden');
    $('#importPreviewBox').innerHTML=`<div class="import-preview-head"><strong>${escapeHtml(r.format)}</strong><span class="status-pill good">${r.message_count||0} messages</span></div><div class="import-speakers">${(r.speakers||[]).slice(0,12).map(x=>`<span class="tag">${escapeHtml(x.name)} · ${x.count}</span>`).join('')}</div>${(r.warnings||[]).map(w=>`<div class="import-warning">▲ ${escapeHtml(w)}</div>`).join('')}<div class="import-preview-messages">${(r.preview||[]).slice(0,5).map(m=>`<div><b>${escapeHtml(m.speaker)}:</b> ${escapeHtml(String(m.text||'').slice(0,220))}</div>`).join('')}</div>`;
    $('#applyImportBtn').disabled=false;
  }catch(e){$('#importPreviewBox').innerHTML=`<div class="bad-text">${escapeHtml(e.message)}</div>`;}finally{b.disabled=false;b.textContent='1. Preview transcript';}
}
async function applyChatImport(){
  if(!state.importPreview){await previewChatImport();if(!state.importPreview)return;}
  const b=$('#applyImportBtn');b.disabled=true;b.textContent='Importing & rebuilding world…';
  try{
    const body={source_platform:$('#importPlatform').value,source_filename:state.importFilename,transcript:$('#importTranscript').value,destination:$('#importDestination').value,conversation_id:state.conversationId,title:$('#importRpTitle')?.value||'Imported RP',persona_name:$('#importPersona')?.value||state.bundle?.conversation?.persona_name||'Persona',user_speaker:$('#importUserSpeaker')?.value||'',analysis_mode:$('#importAnalysisMode').value,model_key:$('#modelChoiceSelect')?.value||null};
    const r=await api('/api/import/apply',{method:'POST',body:JSON.stringify(body)});closeModal();state.conversationId=r.conversation_id;await loadConversations();showView('world');toast(`Imported ${r.messages_imported} messages. World Bible + Dashboard rebuilt from the chat.`);
  }catch(e){alert(e.message);b.disabled=false;b.textContent='2. Import & build this RP';}
}


async function loadCreatorContent(){
  try{
    [state.creatorContent,state.creatorCharacters]=await Promise.all([api('/api/creator/content?creator_name=You'),api('/api/creator/characters?creator_name=You')]);
    renderCreatorContent();
  }catch(e){console.error(e)}
}
function creatorStatusPill(status){const cls=status==='Approved'?'good':status==='Pending Review'?'warn':'';return `<span class="status-pill ${cls}">${escapeHtml(status)}</span>`}
function renderCreatorContent(){
  const grid=$('#creatorContentGrid'); if(!grid)return;
  grid.innerHTML=state.creatorContent.length?state.creatorContent.map(x=>`<article class="creator-content-card"><div class="scenario-card-top"><span class="tag">${escapeHtml(x.item_type)}</span>${creatorStatusPill(x.status)}</div><h2>${escapeHtml(x.title)}</h2><p>${escapeHtml(x.summary||'No description yet.')}</p>${x.parent_character?`<div class="scenario-character">Character: <strong>${escapeHtml(x.parent_character)}</strong></div>`:''}<div class="scenario-tags">${(x.tags||[]).map(t=>`<span class="tag">${escapeHtml(t)}</span>`).join('')}</div><div class="creator-card-actions"><button class="ghost edit-creation" data-id="${x.id}">Edit</button>${['Draft','Changes Requested','Private Test','Rejected'].includes(x.status)?`<button class="secondary submit-creation" data-id="${x.id}">Submit for approval</button>`:''}</div></article>`).join(''):'<div class="empty-state"><div><h2>No creations yet</h2><p>Create a character or scenario when inspiration hits.</p></div></div>';
  $$('.edit-creation').forEach(b=>b.onclick=()=>openCreatorForm(state.creatorContent.find(x=>String(x.id)===String(b.dataset.id))));
  $$('.submit-creation').forEach(b=>b.onclick=async()=>{await api(`/api/creator/content/${b.dataset.id}/submit`,{method:'POST',body:'{}'});await loadCreatorContent();});
}
function presetTagButtons(target='creatorTags'){return `<div class="creator-tag-presets">${state.discoverTags.map(t=>`<button type="button" class="tag-filter mini public-tag-preset" data-target="${target}" data-tag="${escapeHtml(t)}">${escapeHtml(t)}</button>`).join('')}</div>`}
function creatorCharacterOptions(selected=''){return `<option value="">No fixed base character / flexible cast</option>`+state.creatorCharacters.map(c=>{const id=String(c.id||''),match=String(selected||'')===id||String(selected||'')===String(c.title||'');return `<option value="${escapeHtml(id)}" data-title="${escapeHtml(c.title||'')}" ${match?'selected':''}>${escapeHtml(c.title)} · ${escapeHtml(c.creator_name||'Library')}</option>`}).join('')}
function canonSearchBlock(scope, nameId, sourceId, existingPack=null){
  if(existingPack) state.canonDrafts[scope]=existingPack; else delete state.canonDrafts[scope];
  return `<div class="canon-search-box" data-canon-scope="${scope}"><div class="canon-search-head"><div><strong>Existing / canon character?</strong><div class="muted">Search the real internet for source material before you build the RP canon.</div></div><span class="status-pill">review first</span></div><div class="form-row double"><label>Character name comes from<input value="${nameId==='creatorTitle'?'the title above':'the name above'}" disabled></label><label>Franchise / source comes from<input value="the Source / franchise field" disabled></label></div><button type="button" class="secondary canon-search-btn" data-scope="${scope}" data-name-id="${nameId}" data-source-id="${sourceId}">🔎 Search web for canon</button><div class="canon-search-help">No API key is required for the Wikipedia + Wikidata fallback. If a broad-web search key is configured later, MelumiyaChat also searches other sites. Nothing is imported automatically.</div><div id="canonSearchResults-${scope}" class="canon-search-results">${existingPack?`<div class="canon-attached">✓ ${existingPack.sources?.length||0} source(s) already attached to this character.</div>`:''}</div></div>`;
}
function renderCanonSearchResults(scope, result){
  state.canonSearchResults[scope]=result;
  const box=$(`#canonSearchResults-${scope}`); if(!box)return;
  const warnings=(result.warnings||[]).map(w=>`<div class="canon-warning">${escapeHtml(w)}</div>`).join('');
  const sources=(result.sources||[]).map((x,i)=>`<label class="canon-source-card"><input type="checkbox" class="canon-source-check" data-scope="${scope}" data-index="${i}" ${i<5?'checked':''}><div><div class="canon-source-top"><strong>${escapeHtml(x.title)}</strong><span class="tag">${escapeHtml(x.authority||x.source_label||'source')}</span></div><div class="muted">${escapeHtml(x.domain||x.source_label||'')}</div><p>${escapeHtml(x.snippet||'No summary returned.')}</p><a href="${escapeHtml(x.url)}" target="_blank" rel="noopener">Open source ↗</a></div></label>`).join('');
  box.innerHTML=`<div class="canon-search-summary"><strong>${escapeHtml(result.mode||'Canon search')}</strong><span>${(result.sources||[]).length} source result(s)</span></div>${warnings}${sources||'<div class="canon-warning">No sources found. Try adding the franchise name or checking the spelling.</div>'}<div class="modal-actions"><button type="button" class="secondary canon-attach-btn" data-scope="${scope}">Attach selected sources</button></div>`;
  const attach=box.querySelector('.canon-attach-btn'); if(attach)attach.onclick=()=>attachCanonSources(scope);
}
async function runCanonSearch(scope,nameId,sourceId){
  const name=$('#'+nameId)?.value.trim()||''; const franchise=$('#'+sourceId)?.value.trim()||'';
  if(!name){alert('Add the character name first.');return;}
  const box=$(`#canonSearchResults-${scope}`); if(box)box.innerHTML='<div class="canon-searching">Searching Wikipedia, Wikidata and any configured broad-web source…</div>';
  try{const result=await api(`/api/canon/search?name=${encodeURIComponent(name)}&franchise=${encodeURIComponent(franchise==='Original'?'':franchise)}`);renderCanonSearchResults(scope,result);}catch(e){if(box)box.innerHTML=`<div class="canon-warning">Could not search right now: ${escapeHtml(e.message)}</div>`;}
}
function attachCanonSources(scope){
  const result=state.canonSearchResults[scope]; if(!result)return;
  const checks=[...document.querySelectorAll(`.canon-source-check[data-scope="${scope}"]`)];
  const picked=checks.filter(c=>c.checked).map(c=>result.sources[Number(c.dataset.index)]).filter(Boolean);
  state.canonDrafts[scope]={searched_at:new Date().toISOString(),query:result.query,character_name:result.character_name,franchise:result.franchise,search_mode:result.mode,sources:picked,fact_candidates:result.fact_candidates||[],rule:result.import_rule};
  const box=$(`#canonSearchResults-${scope}`); if(box)box.insertAdjacentHTML('afterbegin',`<div class="canon-attached">✓ Attached ${picked.length} source(s). They will be saved as research/provenance, not forced into active AU canon.</div>`);
}
function bindCanonSearchButtons(){
  $$('.canon-search-btn').forEach(b=>b.onclick=()=>runCanonSearch(b.dataset.scope,b.dataset.nameId,b.dataset.sourceId));
}
function researchButton(label,target,fieldId,hint=''){
  return `<button type="button" class="secondary small creation-web-btn" data-research-target="${escapeHtml(target)}" data-field-id="${escapeHtml(fieldId||'')}" data-hint="${escapeHtml(hint)}">🌐 ${escapeHtml(label)}</button>`;
}
function appendToField(fieldId,text){const el=$('#'+fieldId);if(!el)return;const old=(el.value||'').trim();el.value=old?`${old}\n${text}`:text;el.dispatchEvent(new Event('input',{bubbles:true}));}
function closeCreationResearchOverlay(){document.querySelector('.creation-research-overlay')?.remove();}
function openCreationWebSearch(target,fieldId='',hint=''){
  closeCreationResearchOverlay();
  const overlay=document.createElement('div');overlay.className='creation-research-overlay';
  overlay.innerHTML=`<div class="creation-research-sheet"><div class="panel-title-row"><div><h3>🌐 Research the web</h3><p class="muted compact">Nothing is added until you choose it, edit it and press Add draft.</p></div><button type="button" class="icon-btn" id="closeCreationResearch">✕</button></div><div class="web-search-bar"><label>What do you want to research?<input id="creationWebQuery" value="${escapeHtml(hint||'')}" placeholder="character, location, historical event, world detail…"></label><button id="runCreationWebSearch" class="primary">Search</button></div><div id="creationWebResults" class="web-result-list"></div><div id="creationResearchEdit" class="hidden"></div></div>`;
  document.querySelector('.modal-card')?.appendChild(overlay);
  $('#closeCreationResearch').onclick=closeCreationResearchOverlay;
  const run=async()=>{const q=$('#creationWebQuery').value.trim();if(!q)return;$('#runCreationWebSearch').disabled=true;$('#creationWebResults').innerHTML='<div class="canon-searching">Searching…</div>';try{const r=await api(`/api/web/search?q=${encodeURIComponent(q)}`);const results=r.results||[];$('#creationWebResults').innerHTML=results.length?results.map((x,i)=>`<div class="web-result"><h4>${escapeHtml(x.title||'Result')}</h4><p>${escapeHtml(x.summary||'')}</p><div class="web-result-meta"><span class="tag">${escapeHtml(x.source_label||x.domain||'Web')}</span></div>${x.url?`<a href="${escapeHtml(x.url)}" target="_blank" rel="noopener">Open source ↗</a>`:''}<div class="web-result-actions"><button class="primary small use-creation-research" data-index="${i}">Use / edit this</button></div></div>`).join(''):friendlyEmpty('No results found.');$$('.use-creation-research').forEach(b=>b.onclick=()=>showCreationResearchDraft(target,fieldId,results[Number(b.dataset.index)]));}catch(e){$('#creationWebResults').innerHTML=`<div class="canon-warning">${escapeHtml(e.message)}</div>`}finally{$('#runCreationWebSearch').disabled=false}};
  $('#runCreationWebSearch').onclick=run;$('#creationWebQuery').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();run();}});setTimeout(()=>$('#creationWebQuery')?.focus(),50);
}
function showCreationResearchDraft(target,fieldId,x){
  const box=$('#creationResearchEdit');box.classList.remove('hidden');box.innerHTML=`<div class="research-edit-card"><div class="bible-draft-source">Source: ${escapeHtml(x.source_label||x.domain||'Web')} ${x.url?`· ${escapeHtml(x.url)}`:''}</div><label>Edit before adding<textarea id="creationResearchText" rows="7">${escapeHtml([x.title,x.summary].filter(Boolean).join(' — '))}</textarea></label><div class="modal-actions"><button id="cancelCreationResearchDraft" class="ghost">Cancel</button><button id="useCreationResearchDraft" class="primary">Add edited draft</button></div></div>`;
  $('#cancelCreationResearchDraft').onclick=()=>box.classList.add('hidden');
  $('#useCreationResearchDraft').onclick=()=>{const text=$('#creationResearchText').value.trim();if(fieldId)appendToField(fieldId,text);if(!state.creationResearchDrafts[target])state.creationResearchDrafts[target]=[];state.creationResearchDrafts[target].push({title:x.title||'',content:text,url:x.url||'',source_label:x.source_label||x.domain||'Web',saved_at:new Date().toISOString()});closeCreationResearchOverlay();toast('Research added as an editable draft.');};
}

function bindCreationResearchButtons(){ $$('.creation-web-btn').forEach(b=>b.onclick=()=>openCreationWebSearch(b.dataset.researchTarget,b.dataset.fieldId,b.dataset.hint||'')); }
function lines(v){return String(v||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean)}
function scenarioAdvancedJson(payloadObj){return `<details class="creator-advanced"><summary>Advanced structured data (optional)</summary><div class="advanced-explain"><strong>Most people never need this.</strong><span>The normal fields above are authoritative. Use JSON only for unusual structured rules.</span></div><textarea id="creatorPayload" rows="9">${escapeHtml(pretty(payloadObj))}</textarea></details>`}
function scenarioDetailText(v){if(v==null)return'';if(typeof v==='string')return v;if(Array.isArray(v))return v.map(scenarioDetailText).filter(Boolean).join('\n');if(typeof v==='object'){const base=v.details||v.fact||v.event||v.rule||v.name||v.title||v.value||v.description||v.notes||'';const known=Array.isArray(v.known_by)?v.known_by.filter(Boolean):[],hidden=Array.isArray(v.hidden_from)?v.hidden_from.filter(Boolean):[];if(base&&(known.length||hidden.length))return `${base}${known.length?` — known by: ${known.join(', ')}`:''}${hidden.length?` — hidden from: ${hidden.join(', ')}`:''}`;return base||JSON.stringify(v)}return String(v)}
function scenarioListText(v){return (Array.isArray(v)?v:[]).map(scenarioDetailText).filter(Boolean).join('\n')}
function scenarioSetField(id,value,replace=false,mode='text'){const el=$('#'+id);if(!el||value==null)return 0;let v;if(mode==='lines')v=scenarioListText(value);else if(mode==='csv')v=(Array.isArray(value)?value:[]).map(scenarioDetailText).filter(Boolean).join(', ');else v=scenarioDetailText(value);if(!v.trim())return 0;if(!replace&&String(el.value||'').trim())return 0;el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));return 1}
const SCENARIO_FIELD_ALIASES={
  starting_situation:['scenario_setup','setup','initial_situation'],history_backstory:['history','backstory','history_notes','history_anchors'],relationship_setup:['relationship_history','relationships'],
  key_characters:['characters','side_characters','cast'],groups_factions:['groups','factions','organisations','organizations'],important_objects:['objects','artifacts','artefacts'],
  secrets_knowledge:['secrets','knowledge'],world_rules:['rules','lore_rules'],current_state:['present_state','current_status'],physical_continuity:['physical_state','physical_continuity_notes','continuity.physical','continuity.physical_state'],
  story_timeline:['timeline','timeline_seed','prior_events'],goals_hooks:['goals','hooks','story_hooks'],hard_scenario_rules:['hard_rules','scenario_rules','core_canon'],
  'starting_scene.location':['scene.location','starting_location'],'starting_scene.date_or_era':['scene.date_or_era','scene.era','starting_era'],'starting_scene.time':['scene.time','starting_time'],
  'starting_scene.present_cast':['scene.present_cast','present_cast','present_characters'],'starting_scene.situation':['scene.situation','starting_scene_situation'],
  'tone_genre.genre':['genre','tone_and_genre.genre'],'tone_genre.tone':['tone','tone_and_genre.tone']
};
function scenarioField(o,path,fallback=null){if(!o||typeof o!=='object')return fallback;const candidates=[path,...(SCENARIO_FIELD_ALIASES[path]||[])];for(const candidate of candidates){if(Object.prototype.hasOwnProperty.call(o,candidate)&&o[candidate]!=null)return o[candidate];const v=candidate.split('.').reduce((a,k)=>a?.[k],o);if(v!=null)return v}return fallback}
function scenarioCurrentResearchInput(){return {premise:fieldValue('creatorPremise'),starting_situation:fieldValue('creatorScenarioSetup'),history_backstory:lines(fieldValue('creatorHistory')),relationship_setup:lines(fieldValue('creatorRelationshipHistory')),key_characters:lines(fieldValue('creatorSideCharacters')),locations:lines(fieldValue('creatorLocations')),groups_factions:lines(fieldValue('creatorGroups')),important_objects:lines(fieldValue('creatorObjects')),secrets_knowledge:lines(fieldValue('creatorSecrets')),world_rules:lines(fieldValue('creatorWorldRules')),current_state:lines(fieldValue('creatorCurrentState')),physical_continuity:lines(fieldValue('creatorPhysicalContinuity')),story_timeline:lines(fieldValue('creatorTimeline')),goals_hooks:lines(fieldValue('creatorHooks')),hard_scenario_rules:lines(fieldValue('creatorHardRules')),starting_scene:{location:fieldValue('creatorSceneLocation'),date_or_era:fieldValue('creatorSceneDate'),time:fieldValue('creatorSceneTime'),present_cast:csv(fieldValue('creatorPresentCast')),situation:fieldValue('creatorSceneSituation')},tone_genre:{genre:fieldValue('creatorGenre'),tone:fieldValue('creatorTone')}}}
function scenarioResearchMatches(suggestions=[]){const chars=state.creatorCharacters||[];const matches=[];for(const s of suggestions||[]){const n=String(s.name||'').trim().toLowerCase();if(!n)continue;const c=chars.find(x=>String(x.title||'').trim().toLowerCase()===n);if(c&&!matches.some(m=>String(m.content_id)===String(c.id)))matches.push({content_id:String(c.id),name:c.title,role:s.role||'scenario character',source:'character-library-match'})}return matches}
function applyScenarioAutofill(result,replace=false){const f=result.fields||{};let n=0;n+=scenarioSetField('creatorSummary',result.description||scenarioField(f,'description',''),replace);n+=scenarioSetField('creatorCardHook',result.card_hook||scenarioField(f,'card_hook',''),replace);const map=[['creatorPremise','premise'],['creatorScenarioSetup','starting_situation'],['creatorHistory','history_backstory','lines'],['creatorRelationshipHistory','relationship_setup','lines'],['creatorSideCharacters','key_characters','lines'],['creatorLocations','locations','lines'],['creatorGroups','groups_factions','lines'],['creatorObjects','important_objects','lines'],['creatorSecrets','secrets_knowledge','lines'],['creatorWorldRules','world_rules','lines'],['creatorCurrentState','current_state','lines'],['creatorPhysicalContinuity','physical_continuity','lines'],['creatorTimeline','story_timeline','lines'],['creatorHooks','goals_hooks','lines'],['creatorHardRules','hard_scenario_rules','lines'],['creatorSceneLocation','starting_scene.location'],['creatorSceneDate','starting_scene.date_or_era'],['creatorSceneTime','starting_scene.time'],['creatorPresentCast','starting_scene.present_cast','csv'],['creatorSceneSituation','starting_scene.situation'],['creatorGenre','tone_genre.genre'],['creatorTone','tone_genre.tone']];for(const [id,path,mode='text'] of map)n+=scenarioSetField(id,scenarioField(f,path,mode==='text'?'':[]),replace,mode);
  const suggestions=result.character_suggestions||[],matched=scenarioResearchMatches(suggestions);state.scenarioAutofillResearch={searched_at:new Date().toISOString(),query:result.query||'',sources:result.sources||[],field_sources:result.field_sources||{},warnings:result.warnings||[],character_suggestions:suggestions,linked_character_matches:matched,source_strategy:result.source_strategy||[],rich_pages_fetched:Number(result.rich_pages_fetched||0)};scenarioParticipantWinImportAutofill();
  const status=$('#scenarioAutofillStatus');if(status)status.innerHTML=`<strong>✓ ${n} field${n===1?'':'s'} filled</strong><span>${(result.sources||[]).length} sources · ${result.rich_pages_fetched||0} detailed pages read · ${suggestions.length} character suggestions${matched.length?` · ${matched.length} matched your Character Library`:''}</span>${(result.warnings||[]).length?`<small>${escapeHtml(result.warnings.join(' · '))}</small>`:''}`;
}
async function runScenarioAutofill(){const title=fieldValue('scenarioAutofillQuery')||fieldValue('creatorTitle'),source=fieldValue('creatorSource'),main=scenarioParticipantWinCompat().main,parent=main?.linked_entity_id?state.creatorCharacters.find(c=>String(c.id)===String(main.linked_entity_id)):null;if(!title&&!source){alert('Add a scenario title or source / franchise first.');return}const b=$('#scenarioAutofillBtn'),replace=Boolean($('#scenarioAutofillReplace')?.checked);b.disabled=true;b.textContent='Researching setting…';const status=$('#scenarioAutofillStatus');if(status)status.innerHTML='<span>Opening detailed setting, franchise, guide and reference pages…</span>';try{const r=await api('/api/creator/scenario-autofill',{method:'POST',body:JSON.stringify({title,source,main_character:main?.name||parent?.title||'',existing:scenarioCurrentResearchInput()})});applyScenarioAutofill(r,replace)}catch(e){if(status)status.innerHTML=`<span class="canon-warning">${escapeHtml(e.message)}</span>`}finally{b.disabled=false;b.textContent='🌐 Search the web & autofill scenario'}}
function scenarioSlug(v=''){return String(v||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,100)||'item'}
function scenarioSeedItems(values,section,extra={}){return (values||[]).map((value,i)=>{const details=scenarioDetailText(value);return details?{stable_key:`scenario:${section}:${scenarioSlug(details)||i}`,details,source:'scenario-template',...extra}:null}).filter(Boolean)}
function parseScenarioSecrets(values){return (values||[]).map((raw,i)=>{if(raw&&typeof raw==='object'){const fact=scenarioDetailText({...raw,known_by:[],hidden_from:[]}).trim();if(!fact)return null;return {...raw,stable_key:raw.stable_key||`scenario:secrets_knowledge:${scenarioSlug(fact)||i}`,fact:raw.fact||raw.details||fact,details:raw.details||raw.fact||fact,known_by:Array.isArray(raw.known_by)?raw.known_by.filter(Boolean):[],hidden_from:Array.isArray(raw.hidden_from)?raw.hidden_from.filter(Boolean):[],source:raw.source||'scenario-template'}}const text=String(raw||'').trim();if(!text)return null;const parts=text.split(/\s+[—-]\s+/),fact=String(parts.shift()||'').trim(),known=[],hidden=[];for(const part of parts){const km=part.match(/^known by:\s*(.+)$/i),hm=part.match(/^hidden from:\s*(.+)$/i);if(km)known.push(...km[1].split(',').map(x=>x.trim()).filter(Boolean));else if(hm)hidden.push(...hm[1].split(',').map(x=>x.trim()).filter(Boolean))}return {stable_key:`scenario:secrets_knowledge:${scenarioSlug(fact)||i}`,fact,details:fact,known_by:[...new Set(known)],hidden_from:[...new Set(hidden)],source:'scenario-template'}}).filter(Boolean)}
function scenarioParticipantWinId(type,name,linked=''){return `${type||'participant'}:${linked||scenarioSlug(name)}:${Math.random().toString(36).slice(2,8)}`}
function scenarioParticipantWinSnapshot(c){return c?{id:c.id,title:c.title||'',summary:c.summary||'',tags:Array.isArray(c.tags)?c.tags:[],payload:c.payload||{},image_url:profileCreationImage(c)||''}:null}
function scenarioParticipantWinNormalize(raw,i=0){const x={...(raw||{})},type=['character','bot','persona','npc'].includes(String(x.type))?String(x.type):'character',name=String(x.name||x.display_name||x.title||'').trim();if(!name)return null;let role=String(x.story_role||x.role||'').toLowerCase().replace(/\s+/g,'_');if(type==='character'||type==='npc')role=role.includes('main')?'main':'secondary';else if(type==='persona')role='user';else if(!['narrator','writer','editor','game_master','custom'].includes(role))role='custom';return{participant_id:String(x.participant_id||x.id||scenarioParticipantWinId(type,name,x.linked_entity_id||x.content_id||'')),type,linked_entity_id:x.linked_entity_id||x.content_id||null,name,story_role:role,active:x.active!==false,speaking_enabled:type==='persona'?false:x.speaking_enabled!==false,present_at_start:type==='bot'?Boolean(x.present_at_start&&x.diegetic):x.present_at_start!==false,diegetic:type==='bot'?Boolean(x.diegetic):true,instructions:String(x.instructions||''),memory_scope:String(x.memory_scope||'auto'),knowledge_scope:String(x.knowledge_scope||'auto'),sort_order:Number.isFinite(Number(x.sort_order))?Number(x.sort_order):i,snapshot:x.snapshot||null}}
function scenarioParticipantWinInitial(payload={},creation={}){const out=[],seen=new Set(),all=state.creatorCharacters||[],add=r=>{const x=scenarioParticipantWinNormalize(r,out.length);if(!x)return;const k=x.linked_entity_id?`${x.type}:${x.linked_entity_id}`:`${x.type}:${x.name.toLowerCase()}`;if(seen.has(k))return;seen.add(k);out.push(x)};if(Array.isArray(payload.participants)&&payload.participants.length)payload.participants.forEach(add);else{add({type:'persona',name:'User Persona',story_role:'user',present_at_start:true,speaking_enabled:false});const pid=String(payload.parent_character_id||''),pc=all.find(c=>String(c.id)===pid);if(pc)add({type:'character',linked_entity_id:String(pc.id),name:pc.title,story_role:'main',snapshot:scenarioParticipantWinSnapshot(pc),present_at_start:true});else if(payload.parent_character_snapshot)add({type:'character',linked_entity_id:payload.parent_character_snapshot.id||null,name:payload.parent_character_snapshot.title||creation.parent_character||'Character',story_role:'main',snapshot:payload.parent_character_snapshot,present_at_start:true});else if(creation.parent_character)add({type:'npc',name:creation.parent_character,story_role:'main',present_at_start:true});for(const y of (payload.linked_characters||[])){const id=String(y.content_id||y.id||''),c=all.find(v=>String(v.id)===id),name=y.name||y.title||c?.title;if(name)add({type:'character',linked_entity_id:id||null,name,story_role:String(y.role||'').toLowerCase().includes('main')?'main':'secondary',snapshot:y.snapshot||scenarioParticipantWinSnapshot(c),present_at_start:true})}for(const line of (payload.key_characters||payload.side_characters||[])){const s=scenarioDetailText(line),name=s.split(/\s+[—–-]\s+/,1)[0].trim();if(name)add({type:'npc',name,story_role:'secondary',present_at_start:true,instructions:s})}}if(!out.some(x=>x.type==='persona'))add({type:'persona',name:'User Persona',story_role:'user',present_at_start:true,speaking_enabled:false});if(!out.some(x=>(x.type==='character'||x.type==='npc')&&x.story_role==='main')){const f=out.find(x=>x.type==='character'||x.type==='npc');if(f)f.story_role='main'}return out.map((x,i)=>({...x,sort_order:i}))}
function scenarioParticipantWinCompat(){const ps=(state.scenarioParticipantsDraft||[]).map((x,i)=>({...x,sort_order:i})),chars=ps.filter(x=>x.type==='character'||x.type==='npc'),main=chars.find(x=>x.story_role==='main')||chars[0]||null,linked=chars.filter(x=>x.linked_entity_id).map(x=>({content_id:String(x.linked_entity_id),name:x.name,role:x.story_role==='main'?'main character':'secondary character',source:'scenario-participants',snapshot:x.snapshot||null})),key=chars.map(x=>`${x.name} — ${x.story_role==='main'?'main character':'secondary character'}${x.instructions?` — ${x.instructions}`:''}`),secondary=chars.filter(x=>x.story_role!=='main').map(x=>`${x.name} — secondary character${x.instructions?` — ${x.instructions}`:''}`),present=chars.filter(x=>x.present_at_start&&x.diegetic!==false).map(x=>x.name);return{participants:ps,chars,main,linked,key,secondary,present}}
function scenarioParticipantWinSystemEntry(ps=[]){return{stable_key:'scenario:participants',kind:'scenario_participants',canon_importance:'High',source:'scenario-participant-system',details:'Scenario participant orchestration. Main/secondary is narrative priority, not physical presence. Only present diegetic participants normally witness the scene. User Persona is controlled only by the user. Bots follow their bot role and instructions and are not fictional characters unless explicitly marked diegetic.',participants:ps.map(x=>({participant_id:x.participant_id,type:x.type,name:x.name,story_role:x.story_role,linked_entity_id:x.linked_entity_id||null,active:x.active!==false,speaking_enabled:x.speaking_enabled!==false,present_at_start:Boolean(x.present_at_start),diegetic:x.diegetic!==false,instructions:x.instructions||'',memory_scope:x.memory_scope||'auto',knowledge_scope:x.knowledge_scope||'auto'}))}}
function scenarioParticipantWinRender(){const box=$('#creatorParticipantList');if(!box)return;const ps=state.scenarioParticipantsDraft||[];box.innerHTML=ps.map((x,i)=>`<div class="scenario-participant" data-i="${i}"><div class="scenario-participant-avatar">${x.type==='bot'?'🤖':x.type==='persona'?'◉':x.story_role==='main'?'⭐':'👤'}</div><div class="scenario-participant-copy"><strong>${escapeHtml(x.name)}</strong><span>${x.type==='persona'?'User Persona':x.type==='bot'?({'narrator':'Narrator','writer':'Writer','editor':'Editor','game_master':'Game Master','custom':'Bot'}[x.story_role]||'Bot'):x.story_role==='main'?'⭐ Main':'Secondary'}${x.type!=='bot'&&x.type!=='persona'?` · ${x.present_at_start?'Present at start':'Starts offscreen'}`:''}</span></div><button type="button" class="ghost small scenario-participant-menu-btn" data-i="${i}">⋯</button><div class="scenario-participant-menu hidden" data-menu-i="${i}">${(x.type==='character'||x.type==='npc')?`<button type="button" data-act="${x.story_role==='main'?'secondary':'main'}" data-i="${i}">${x.story_role==='main'?'Make secondary':'⭐ Make main'}</button><button type="button" data-act="presence" data-i="${i}">${x.present_at_start?'Start offscreen':'Present at start'}</button>`:''}${x.type==='bot'?`<button type="button" data-act="bot" data-i="${i}">Edit instructions</button>`:''}<button type="button" data-act="remove" data-i="${i}">Remove</button></div></div>`).join('')||'<div class="muted compact">Nobody added yet.</div>';box.querySelectorAll('.scenario-participant-menu-btn').forEach(b=>b.onclick=()=>box.querySelector(`[data-menu-i="${b.dataset.i}"]`)?.classList.toggle('hidden'));box.querySelectorAll('.scenario-participant-menu button').forEach(b=>b.onclick=()=>{const i=Number(b.dataset.i),x=state.scenarioParticipantsDraft[i],a=b.dataset.act;if(!x)return;if(a==='main'||a==='secondary')x.story_role=a;else if(a==='presence')x.present_at_start=!x.present_at_start;else if(a==='bot'){const v=prompt('Bot instructions',x.instructions||'');if(v!==null)x.instructions=v.trim()}else if(a==='remove')state.scenarioParticipantsDraft.splice(i,1);if(!state.scenarioParticipantsDraft.some(p=>(p.type==='character'||p.type==='npc')&&p.story_role==='main')){const f=state.scenarioParticipantsDraft.find(p=>p.type==='character'||p.type==='npc');if(f)f.story_role='main'}scenarioParticipantWinRender()});const c=scenarioParticipantWinCompat();if($('#creatorParent'))$('#creatorParent').value=c.main?.linked_entity_id||'';if($('#creatorSideCharacters'))$('#creatorSideCharacters').value=c.key.join('\n');if($('#creatorPresentCast'))$('#creatorPresentCast').value=c.present.join(', ')}
function scenarioParticipantWinPicker(type){const body=$('#creatorParticipantAddBody');if(!body)return;if(type==='character'){body.innerHTML=`<label>Character<select id="creatorParticipantCharacter">${creatorCharacterOptions('')}</select></label><button id="creatorParticipantCharacterAdd" type="button" class="primary">Add character</button>`;$('#creatorParticipantCharacterAdd').onclick=()=>{const id=$('#creatorParticipantCharacter').value,c=(state.creatorCharacters||[]).find(x=>String(x.id)===String(id));if(!c)return alert('Choose a character.');if(state.scenarioParticipantsDraft.some(x=>String(x.linked_entity_id)===String(c.id)))return toast('That character is already here.');const has=state.scenarioParticipantsDraft.some(x=>(x.type==='character'||x.type==='npc')&&x.story_role==='main');state.scenarioParticipantsDraft.push(scenarioParticipantWinNormalize({type:'character',linked_entity_id:String(c.id),name:c.title,story_role:has?'secondary':'main',snapshot:scenarioParticipantWinSnapshot(c),present_at_start:true},state.scenarioParticipantsDraft.length));scenarioParticipantWinRender()}}else if(type==='bot'){body.innerHTML=`<label>Bot type<select id="creatorParticipantBotRole"><option value="narrator">Narrator</option><option value="writer">Writer</option><option value="editor">Editor</option><option value="game_master">Game Master</option><option value="custom">Custom bot</option></select></label><label>Name<input id="creatorParticipantBotName" placeholder="Mission Narrator"></label><label>Instructions<textarea id="creatorParticipantBotInstructions" rows="3" placeholder="What should this bot do?"></textarea></label><button id="creatorParticipantBotAdd" type="button" class="primary">Add bot</button>`;$('#creatorParticipantBotAdd').onclick=()=>{const role=$('#creatorParticipantBotRole').value,d={narrator:['Narrator','Describe the scene and transitions without impersonating normal characters or controlling the user.'],writer:['Writing Bot','Help write and continue prose while preserving scenario canon and the user\'s direction.'],editor:['Editor Bot','Review and improve writing for clarity, continuity and style without changing canon unless asked.'],game_master:['Game Master','Run the scenario, introduce permitted events and NPC activity, and preserve player agency.'],custom:['Bot','']}[role],name=$('#creatorParticipantBotName').value.trim()||d[0],instructions=$('#creatorParticipantBotInstructions').value.trim()||d[1];state.scenarioParticipantsDraft.push(scenarioParticipantWinNormalize({type:'bot',name,story_role:role,instructions,diegetic:false,present_at_start:false},state.scenarioParticipantsDraft.length));scenarioParticipantWinRender()}}else{if(state.scenarioParticipantsDraft.some(x=>x.type==='persona'))return toast('User Persona is already here.');state.scenarioParticipantsDraft.push(scenarioParticipantWinNormalize({type:'persona',name:'User Persona',story_role:'user',present_at_start:true,speaking_enabled:false},state.scenarioParticipantsDraft.length));scenarioParticipantWinRender();body.innerHTML='<div class="muted compact">User Persona added. It binds to whoever starts the RP.</div>'}}
function scenarioParticipantWinImportAutofill(){const all=state.creatorCharacters||[];for(const line of lines($('#creatorSideCharacters')?.value||'')){const name=line.split(/\s+[—–-]\s+/,1)[0].trim();if(!name||state.scenarioParticipantsDraft.some(x=>x.name.toLowerCase()===name.toLowerCase()))continue;const c=all.find(x=>String(x.title||'').trim().toLowerCase()===name.toLowerCase());state.scenarioParticipantsDraft.push(scenarioParticipantWinNormalize(c?{type:'character',linked_entity_id:String(c.id),name:c.title,story_role:'secondary',snapshot:scenarioParticipantWinSnapshot(c),present_at_start:true}:{type:'npc',name,story_role:'secondary',instructions:line,present_at_start:true},state.scenarioParticipantsDraft.length))}scenarioParticipantWinRender()}

function buildScenarioWorldBibleSeed(title,summary,payload){const ps=Array.isArray(payload.participants)?payload.participants:[],chars=ps.filter(x=>x.type==='character'||x.type==='npc'),charItems=chars.map(x=>({stable_key:`scenario:participant:${scenarioSlug(x.participant_id||x.name)}`,name:x.name,role:x.story_role==='main'?'main character':'secondary character',content_id:x.linked_entity_id||null,definition:x.snapshot?.payload||{},summary:x.snapshot?.summary||'',source:'scenario-participants',canon_importance:x.story_role==='main'?'High':'Normal'})),core=[...scenarioSeedItems(payload.hard_scenario_rules||[],'core_canon'),scenarioParticipantWinSystemEntry(ps)],current=[...scenarioSeedItems(payload.current_state||[],'current_state'),...chars.filter(x=>!x.present_at_start).map(x=>({stable_key:`scenario:presence:${scenarioSlug(x.name)}`,name:x.name,presence:'offscreen',details:`${x.name} starts offscreen.`,source:'scenario-participants'}))];return {schema_version:5,overview:{name:title||'',description:summary||payload.premise||''},history_backstory:scenarioSeedItems(payload.history_backstory||[],'history_backstory'),background_facts:scenarioSeedItems(payload.scenario_setup?[payload.scenario_setup]:[],'background_facts',{kind:'starting_situation'}),characters:charItems,relationship_history:scenarioSeedItems(payload.relationship_setup||[],'relationship_history'),core_canon:core,locations:scenarioSeedItems(payload.locations||[],'locations'),secrets_knowledge:parseScenarioSecrets(payload.secrets_knowledge||[]),groups_factions:scenarioSeedItems(payload.groups_factions||[],'groups_factions'),important_objects:scenarioSeedItems(payload.important_objects||[],'important_objects'),world_rules:scenarioSeedItems(payload.world_rules||[],'world_rules'),current_state:current,physical_continuity:scenarioSeedItems(payload.physical_continuity||[],'physical_continuity'),story_timeline:scenarioSeedItems(payload.story_timeline||[],'story_timeline'),custom_entries:scenarioSeedItems(payload.goals_hooks||[],'custom_entries',{kind:'story_hook'}),auto_review_conflicts:[]}}
function scenarioLinkedCharacters(){return scenarioParticipantWinCompat().linked}
function openScenarioCreator(existing=null){
  const x=existing||{},p=x.payload||{},tags=(x.tags||[]).join(', ');state.creationResearchDrafts=p.creation_research||{};state.scenarioAutofillResearch=p.web_research||null;state.scenarioParticipantsDraft=scenarioParticipantWinInitial(p,x);
  const side=(p.key_characters||p.side_characters||[]).map(scenarioDetailText).join('\n'),history=(p.history_backstory||p.history_notes||p.history_anchors||[]).map(scenarioDetailText).join('\n'),timeline=(p.story_timeline||p.timeline_seed||[]).map(scenarioDetailText).join('\n'),scene=p.starting_scene||{};
  const search=`<div class="creator-web-autofill"><strong>🌐 Search the web & autofill scenario</strong><span>MelumiyaChat researches the setting, opens detailed franchise/profile pages, classifies canon into Scenario + World Bible fields, and generates Description last.</span><input id="scenarioAutofillQuery" value="${escapeHtml(x.title||'')}" placeholder="Scenario / setting, e.g. Basgiath War College"><label class="creator-check"><input id="scenarioAutofillReplace" type="checkbox"> Replace fields I already filled</label><button id="scenarioAutofillBtn" class="secondary" type="button">🌐 Search the web & autofill scenario</button><div id="scenarioAutofillStatus" class="creator-autofill-status"><span>Official/publisher pages, franchise wikis and detailed guide sites are preferred. Fanon/headcanon stays out.</span></div></div>`;
  openModal(existing?'Edit scenario':'Create scenario',`<div class="creator-form-intro"><span class="tag">Scenario</span><strong>Build the starting world; each RP gets its own copy and can evolve independently.</strong></div>${search}
  <div class="creator-section"><h3>1. Overview</h3><label>Scenario title<input id="creatorTitle" value="${escapeHtml(x.title||'')}"></label><div class="image-field-row"><label>Scenario cover<input id="creatorImage" value="${escapeHtml(p.image_url||'')}" placeholder="Generated or uploaded image"></label><div class="row-actions"><button type="button" id="creatorUploadImage" class="ghost">↑ Upload image</button><button type="button" id="creatorGenerateImage" class="secondary">✦ Generate cover</button></div></div><label>Short card hook<input id="creatorCardHook" value="${escapeHtml(p.card_hook||'')}" placeholder="He thought you were never coming back."></label><label>Public description<textarea id="creatorSummary" rows="4">${escapeHtml(x.summary||'')}</textarea></label><label>Premise<textarea id="creatorPremise" rows="4" placeholder="The concise overall premise of the scenario">${escapeHtml(p.premise||'')}</textarea></label><div class="form-row double"><label>Genre<input id="creatorGenre" value="${escapeHtml(p.tone_genre?.genre||'')}" placeholder="Romantasy, military fantasy…"></label><label>Tone<input id="creatorTone" value="${escapeHtml(p.tone_genre?.tone||'')}" placeholder="Tense, romantic, dark…"></label></div></div>
  <div class="creator-section"><h3>2. Who’s here?</h3><p class="muted compact">Add characters, bots or the user persona. The first character becomes Main automatically; additional characters start as Secondary.</p><div id="creatorParticipantList" class="scenario-participant-list"></div><button id="creatorAddSomeone" type="button" class="secondary">+ Add someone</button><div id="creatorParticipantAddPanel" class="scenario-participant-add hidden"><div class="row-actions"><button type="button" class="ghost small" data-participant-type="character">Character</button><button type="button" class="ghost small" data-participant-type="bot">Bot</button><button type="button" class="ghost small" data-participant-type="persona">Persona</button></div><div id="creatorParticipantAddBody"></div></div><label class="hidden">Search character library<input id="creatorCharacterSearch"></label><select id="creatorParent" class="hidden">${creatorCharacterOptions(p.parent_character_id||x.parent_character||'')}</select><button type="button" id="publicInlineCharacterToggle" class="secondary">+ Create new reusable character</button><div id="publicInlineCharacter" class="inline-character-create hidden"><h3>New reusable character</h3><label>Name<input id="publicInlineCharName"></label><label>Description<textarea id="publicInlineCharSummary" rows="3"></textarea></label>${canonSearchBlock('inline','publicInlineCharName','creatorSource',null)}<label>Tags<input id="publicInlineCharTags"></label>${presetTagButtons('publicInlineCharTags')}<label>Character data (JSON)<textarea id="publicInlineCharPayload" rows="5">{\n  "personality": [],\n  "world_rules": ["never speak for user"]\n}</textarea></label></div><textarea id="creatorSideCharacters" class="hidden">${escapeHtml(side)}</textarea></div>
  <div class="creator-section"><h3>3. User's role</h3><label>Who is the user in this scenario?<textarea id="creatorUserRole" rows="3">${escapeHtml(p.user_role||'')}</textarea></label><label>Required starting facts about the user's role<textarea id="creatorUserFacts" rows="3">${escapeHtml((p.user_role_facts||[]).join('\n'))}</textarea></label></div>
  <div class="creator-section"><h3>4. Starting situation</h3><p class="muted compact">What is true when the RP begins—not the first chat message.</p><textarea id="creatorScenarioSetup" rows="7">${escapeHtml(p.scenario_setup||p.starting_situation||'')}</textarea></div>
  <div class="creator-section"><h3>5. Relationship setup</h3><div class="form-row double"><label>Current relationship<select id="creatorRelationship"><option>Strangers</option><option>Acquaintances</option><option>Friends</option><option>Best friends</option><option>Dating</option><option>Engaged</option><option>Married</option><option>Separated</option><option>Exes</option><option>Rivals</option><option>Enemies</option><option>Coworkers</option><option>Family</option><option>Custom</option></select></label><label>Detail<input id="creatorRelationshipDetail" value="${escapeHtml(p.relationship_detail||'')}"></label></div><label>Established relationship history<textarea id="creatorRelationshipHistory" rows="5">${escapeHtml(scenarioListText(p.relationship_setup||p.relationship_history||[]))}</textarea></label></div>
  <div class="creator-section"><h3>6. Starting scene</h3><div class="form-row triple"><label>Location<input id="creatorSceneLocation" value="${escapeHtml(scene.location||'')}"></label><label>Date / era<input id="creatorSceneDate" value="${escapeHtml(scene.date_or_era||'')}"></label><label>Time<input id="creatorSceneTime" value="${escapeHtml(scene.time||'')}"></label></div><input id="creatorPresentCast" class="hidden" value="${escapeHtml((scene.present_cast||[]).join(', '))}"><p class="muted compact">Presence is set from each participant’s ⋯ menu. Main/Secondary affects narrative priority, not who is physically present.</p><label>What's happening right now?<textarea id="creatorSceneSituation" rows="4">${escapeHtml(scene.situation||'')}</textarea></label></div>
  <div class="creator-section opening-section"><h3>7. Opening Message</h3><textarea id="creatorOpeningMessage" rows="10" placeholder="The actual first RP message">${escapeHtml(p.opening_prompt||p.opening||'')}</textarea><button type="button" id="previewOpeningMessage" class="secondary">👁 Preview first message</button></div>
  <div class="creator-section"><h3>8. History & Backstory</h3><textarea id="creatorHistory" rows="6" placeholder="One pre-RP event/fact per line">${escapeHtml(history)}</textarea></div>
  <div class="creator-section"><h3>9. Locations</h3><textarea id="creatorLocations" rows="5" placeholder="One important place per line">${escapeHtml(scenarioListText(p.locations||[]))}</textarea></div>
  <div class="creator-section"><h3>10. Groups / Factions</h3><textarea id="creatorGroups" rows="5" placeholder="One organization/faction per line">${escapeHtml(scenarioListText(p.groups_factions||[]))}</textarea></div>
  <div class="creator-section"><h3>11. Important Objects</h3><textarea id="creatorObjects" rows="5" placeholder="Artifacts, heirlooms, weapons, documents…">${escapeHtml(scenarioListText(p.important_objects||[]))}</textarea></div>
  <div class="creator-section"><h3>12. Secrets & Knowledge</h3><p class="muted compact">Optional syntax: Secret — known by: Xaden, Violet</p><textarea id="creatorSecrets" rows="5">${escapeHtml(scenarioListText(p.secrets_knowledge||[]))}</textarea></div>
  <div class="creator-section"><h3>13. World Rules / Lore</h3><textarea id="creatorWorldRules" rows="6" placeholder="Magic, military, social, supernatural or setting rules">${escapeHtml(scenarioListText(p.world_rules||[]))}</textarea></div>
  <div class="creator-section"><h3>14. Current State</h3><textarea id="creatorCurrentState" rows="5" placeholder="Current conflict, political state, ongoing problems…">${escapeHtml(scenarioListText(p.current_state||[]))}</textarea></div>
  <div class="creator-section"><h3>15. Physical Continuity</h3><p class="muted compact">Starting physical state only: injuries, scars, clothing, held/missing objects, and damaged places. RP changes remain separate current-state overlays.</p><textarea id="creatorPhysicalContinuity" rows="5" placeholder="One starting physical fact per line">${escapeHtml(scenarioListText(p.physical_continuity||[]))}</textarea></div>
  <div class="creator-section"><h3>16. Timeline / Prior Events</h3><textarea id="creatorTimeline" rows="6" placeholder="2018 — Event\nThree years before RP — Event">${escapeHtml(timeline)}</textarea></div>
  <div class="creator-section"><h3>17. Goals / Hooks</h3><p class="muted compact">Pressure and possibilities, not a fixed plot and never decisions for the user.</p><textarea id="creatorHooks" rows="5">${escapeHtml(scenarioListText(p.goals_hooks||[]))}</textarea></div>
  <div class="creator-section"><h3>18. Hard Scenario Rules</h3><p class="muted compact">Starting canon the RP must preserve unless story events legitimately change it.</p><textarea id="creatorHardRules" rows="6">${escapeHtml(scenarioListText(p.hard_scenario_rules||p.world_facts||[]))}</textarea></div>
  <div class="creator-section"><h3>19. Publishing</h3><div class="form-row triple"><label>Rating<select id="creatorRating"><option ${x.content_rating!=='NSFW'?'selected':''}>SFW</option><option ${x.content_rating==='NSFW'?'selected':''}>NSFW</option></select></label><label>Canon mode<select id="creatorCanon"><option>Strict Canon</option><option>Canon-Inspired</option><option ${(!x.canon_mode||x.canon_mode==='Selective Canon')?'selected':''}>Selective Canon</option><option ${x.canon_mode==='Full AU'?'selected':''}>Full AU</option></select></label><label>Visibility<select id="creatorVisibility"><option ${x.visibility!=='Unlisted'&&x.visibility!=='Public'?'selected':''}>Private</option><option ${x.visibility==='Unlisted'?'selected':''}>Unlisted</option><option ${x.visibility==='Public'?'selected':''}>Public</option></select></label></div><label>Source / franchise<input id="creatorSource" value="${escapeHtml(x.source_name||'Original')}"></label><label>Tags<input id="creatorTags" value="${escapeHtml(tags)}"></label>${presetTagButtons('creatorTags')}</div>
  ${scenarioAdvancedJson(p)}<div class="modal-actions"><button id="savePublicCreation" class="primary" data-type="scenario" data-id="${x.id||''}">Save draft</button></div>`);
  scenarioParticipantWinRender();$('#creatorAddSomeone').onclick=()=>$('#creatorParticipantAddPanel').classList.toggle('hidden');$('#creatorParticipantAddPanel').querySelectorAll('[data-participant-type]').forEach(b=>b.onclick=()=>scenarioParticipantWinPicker(b.dataset.participantType));setSelect('#creatorRelationship',p.relationship_start||'Strangers','Strangers');$('#creatorUploadImage').onclick=()=>chooseLocalImage('creatorImage');$('#creatorGenerateImage').onclick=()=>openImageGenerator('scenario','creatorImage',imagePromptFromFields('scenario'));$('#scenarioAutofillBtn').onclick=runScenarioAutofill;$('#scenarioAutofillQuery').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();runScenarioAutofill()}});$$('.public-tag-preset').forEach(b=>b.onclick=()=>{const input=$('#'+b.dataset.target);if(!input)return;const vals=input.value.split(',').map(x=>x.trim()).filter(Boolean);if(!vals.some(v=>v.toLowerCase()===b.dataset.tag.toLowerCase()))vals.push(b.dataset.tag);input.value=vals.join(', ')});bindCanonSearchButtons();$('#creatorCharacterSearch').oninput=async()=>{state.creatorCharacters=await api(`/api/creator/characters?creator_name=You&q=${encodeURIComponent($('#creatorCharacterSearch').value)}`);const current=$('#creatorParent').value;$('#creatorParent').innerHTML=creatorCharacterOptions(current)};$('#publicInlineCharacterToggle').onclick=()=>$('#publicInlineCharacter').classList.toggle('hidden');$('#previewOpeningMessage').onclick=()=>{const main=scenarioParticipantWinCompat().main,opt=$('#creatorParent')?.selectedOptions?.[0],label=main?.name||opt?.dataset?.title||$('#publicInlineCharName').value||'Scenario';openModal('Opening Message preview',`<div class="message assistant"><div class="message-meta"><span>${escapeHtml(label)}</span><span>· scenario opening</span></div><div class="message-content">${renderRpMarkup($('#creatorOpeningMessage').value||'Nothing written yet.')}</div></div>`)};
}

function cpath(o,path,fallback=''){return path.split('.').reduce((a,k)=>a?.[k],o)??fallback}
function characterAutofillValue(fields,path,fallback=''){
  if(!fields||typeof fields!=='object')return fallback;
  if(Object.prototype.hasOwnProperty.call(fields,path))return fields[path]??fallback;
  const direct=path.split('.').reduce((a,k)=>a?.[k],fields);if(direct!==undefined&&direct!==null)return direct;
  const aliases=[];
  if(path.startsWith('appearance.'))aliases.push(path.replace(/^appearance\./,'appearance_profile.'));
  if(path.startsWith('personality_profile.'))aliases.push(path.replace(/^personality_profile\./,'personality.'));
  if(path==='public_description')aliases.push('description','summary');
  for(const alias of aliases){
    if(Object.prototype.hasOwnProperty.call(fields,alias))return fields[alias]??fallback;
    const value=alias.split('.').reduce((a,k)=>a?.[k],fields);if(value!==undefined&&value!==null)return value;
  }
  return fallback;
}
function ctext(v){return Array.isArray(v)?v.join('\n'):String(v??'')}
function cinput(id,label,value='',placeholder=''){return `<label>${escapeHtml(label)}<input id="${id}" value="${escapeHtml(String(value??''))}" placeholder="${escapeHtml(placeholder)}"></label>`}
function ctextarea(id,label,value='',rows=3,placeholder=''){return `<label>${escapeHtml(label)}<textarea id="${id}" rows="${rows}" placeholder="${escapeHtml(placeholder)}">${escapeHtml(ctext(value))}</textarea></label>`}
function csection(title,subtitle,body,open=false){return `<details class="character-dossier-section" ${open?'open':''}><summary><strong>${escapeHtml(title)}</strong><span>${escapeHtml(subtitle)}</span></summary><div class="character-dossier-body">${body}</div></details>`}
function fieldValue(id){return ($('#'+id)?.value||'').trim()}
function fieldLines(id){return lines($('#'+id)?.value||'')}
function characterAutofillPlaceholder(value){const s=String(value??'').trim().replace(/^[\s'"`([{]+|[\s'"`\])}.,;:!?]+$/g,'');return /^(?:unknown|unk|n\/?a|na|none|null|nil|unspecified|not specified|not stated|not known|not available|not applicable|not provided|not mentioned|none mentioned|unclear|tbd|to be determined|varies|variable|\?|[-–—])(?:\s*\([^)]*\))?$/i.test(s)}
function cleanCharacterAutofillValue(value){if(Array.isArray(value))return value.map(x=>String(x??'').trim()).filter(x=>x&&!characterAutofillPlaceholder(x));const s=String(value??'').trim();return s&&!characterAutofillPlaceholder(s)?s:''}
function setAutofillField(id,value,replace=false){const el=$('#'+id);if(!el)return false;const cleaned=cleanCharacterAutofillValue(value);if(Array.isArray(cleaned)?!cleaned.length:!cleaned)return false;const existing=String(el.value||'').trim();if(!replace&&existing&&!characterAutofillPlaceholder(existing))return false;const v=Array.isArray(cleaned)?cleaned.join('\n'):String(cleaned);el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));return true}
function applyCharacterAutofill(result,replace=false){const f=result?.fields||{};let n=0;const map={
  'public_description':'creatorSummary','identity.full_name':'creatorTitle','identity.aliases':'charAliases','identity.age':'charAge','identity.birthday':'charBirthday','identity.birthplace':'charBirthplace','identity.nationality':'charNationality','identity.languages':'charLanguages','identity.gender':'charGender','identity.pronouns':'charPronouns','identity.sexuality':'charSexuality','identity.occupation':'charOccupation','identity.education':'charEducation',
  'appearance.height':'charHeight','appearance.build':'charBuild','appearance.skin_tone':'charSkinTone','appearance.eye_color':'charEyeColor','appearance.hair_color':'charHairColor','appearance.hair_style':'charHairStyle','appearance.hair_length':'charHairLength','appearance.facial_hair':'charFacialHair','appearance.scars':'charScars','appearance.tattoos':'charTattoos','appearance.piercings':'charPiercings','appearance.distinguishing_features':'charFeatures','appearance.clothing_style':'charClothing','appearance.scent':'charScent','appearance.voice':'charVoicePhysical',
  'personality_profile.traits':'charPersonality','personality_profile.strengths':'charStrengths','personality_profile.flaws':'charFlaws','personality_profile.fears':'charFears','personality_profile.insecurities':'charInsecurities','personality_profile.habits':'charHabits','personality_profile.mannerisms':'charMannerisms','personality_profile.temper':'charTemper','personality_profile.humor':'charHumor','personality_profile.social_style':'charSocialStyle','personality_profile.morals':'charMorals','personality_profile.values':'charValues','personality_profile.boundaries':'charBoundaries',
  'preferences.likes':'charLikes','preferences.dislikes':'charDislikes','preferences.hobbies':'charHobbies','preferences.favorite_foods':'charFavFood','preferences.favorite_drinks':'charFavDrink','preferences.favorite_music':'charFavMusic','preferences.favorite_books':'charFavBooks','preferences.favorite_films':'charFavFilms','preferences.comforts':'charComforts','preferences.pet_peeves':'charPetPeeves','preferences.guilty_pleasures':'charGuiltyPleasures',
  'relationships.family':'charFamily','relationships.friends':'charFriends','relationships.rivals':'charRivals','relationships.exes':'charExes','relationships.current_relationships':'charCurrentRelationships','relationships.important_people':'charImportantPeople','relationships.attachment_style':'charAttachment','relationships.jealousy_style':'charJealousy','relationships.anger_style':'charAnger','relationships.hurt_style':'charHurt','relationships.love_style':'charLove',
  'life_history.background':'charBackground','life_history.childhood':'charChildhood','life_history.upbringing':'charUpbringing','life_history.achievements':'charAchievements','life_history.failures':'charFailures','life_history.losses':'charLosses','life_history.secrets':'charLifeSecrets','life_history.regrets':'charRegrets','life_history.defining_events':'charDefiningEvents',
  'current_life.home':'charHome','current_life.finances':'charFinances','current_life.routines':'charRoutines','current_life.possessions':'charPossessions','current_life.pets':'charPets','current_life.vehicles':'charVehicles','current_life.goals':'charGoals','current_life.problems':'charProblems','current_life.responsibilities':'charResponsibilities',
  'rp_behavior.speech_style':'charSpeech','rp_behavior.vocabulary':'charVocabulary','rp_behavior.accent':'charAccent','rp_behavior.texting_style':'charTexting','rp_behavior.affection_style':'charAffection','rp_behavior.conflict_style':'charConflict','rp_behavior.flirting_style':'charFlirting','rp_behavior.intimacy_boundaries':'charIntimacyBoundaries',
  'knowledge_skills.talents':'charTalents','knowledge_skills.qualifications':'charQualifications','knowledge_skills.combat_skills':'charCombat','knowledge_skills.technical_skills':'charTechnical','knowledge_skills.creative_skills':'charCreative','knowledge_skills.weaknesses':'charSkillWeaknesses','knowledge_skills.knows':'charKnows','knowledge_skills.does_not_know':'charDoesNotKnow',
  'continuity.immutable_facts':'charImmutable','continuity.flexible_facts':'charFlexible','continuity.secrets':'charContinuitySecrets','continuity.knowledge_permissions':'charKnowledgePermissions','continuity.must_never_happen':'charNever',
  'lore.species':'charSpecies','lore.supernatural_status':'charSupernatural','lore.powers':'charPowers','lore.power_limits':'charPowerLimits','lore.weaknesses':'charLoreWeaknesses','lore.affiliations':'charAffiliations','lore.factions':'charFactions','lore.magic_system_notes':'charMagicNotes'
};for(const [path,id] of Object.entries(map)){const v=characterAutofillValue(f,path,'');if(setAutofillField(id,v,replace))n++}
  const status=$('#characterAutofillStatus');if(status){const src=(result.sources||[]).length,backend=result.backend_version||result.version||'?';status.innerHTML=`<strong>✓ ${n} field${n===1?'':'s'} filled · Backend v${escapeHtml(backend)}</strong><span>${src} source${src===1?'':'s'} checked · Client autofill v23-two-phase. Everything is editable before saving.</span>${(result.warnings||[]).length?`<small>${escapeHtml(result.warnings.join(' · '))}</small>`:''}`}
  state.characterAutofillResearch={searched_at:new Date().toISOString(),query:result.query||'',sources:result.sources||[],field_sources:result.field_sources||{},research_notes:result.research_notes||[],warnings:result.warnings||[]};
}
function characterSourceTypeLabel(t=''){return ({franchise_wiki:'Franchise wiki',community_wiki:'Community wiki',reference:'Reference',wiki:'Wiki',web:'Web source'})[String(t)]||'Web source'}
function renderCharacterAutofillCandidates(candidates=[]){const box=$('#characterAutofillCandidates'),go=$('#characterAutofillBtn');if(!box)return;const list=Array.isArray(candidates)?candidates:[];if(!list.length){state.characterAutofillSelectedSource=null;box.innerHTML='<div class="character-source-empty">No matching character sources found. Try the full franchise/title too.</div>';if(go)go.disabled=true;return}let selected=state.characterAutofillSelectedSource;if(!selected||!list.some(c=>c.url===selected)){selected=(list.find(c=>c.recommended)||list[0]).url;state.characterAutofillSelectedSource=selected}box.innerHTML=list.map(c=>`<button type="button" class="character-source-card ${c.url===selected?'selected':''}" data-url="${escapeHtml(c.url||'')}"><span class="character-source-top"><strong>${escapeHtml(c.title||'Character source')}</strong>${c.recommended?'<em>Recommended</em>':''}</span><span>${escapeHtml(characterSourceTypeLabel(c.source_type))} · ${escapeHtml(c.domain||'')}</span>${c.summary?`<small>${escapeHtml(c.summary)}</small>`:''}</button>`).join('');box.querySelectorAll('.character-source-card').forEach(b=>b.onclick=()=>{state.characterAutofillSelectedSource=b.dataset.url||null;renderCharacterAutofillCandidates(list);const status=$('#characterAutofillStatus');if(status)status.innerHTML=`<strong>Source selected</strong><span>${escapeHtml(b.querySelector('strong')?.textContent||b.dataset.url||'Selected character')}</span>`});if(go)go.disabled=false}
async function findCharacterAutofillSources(){const name=fieldValue('creatorAutofillQuery')||fieldValue('creatorTitle'),source=fieldValue('creatorSource');if(!name){alert('Type the character name first.');return}const b=$('#characterAutofillSearchBtn'),go=$('#characterAutofillBtn'),status=$('#characterAutofillStatus');state.characterAutofillSelectedSource=null;if(go)go.disabled=true;if(b){b.disabled=true;b.textContent='Searching…'}if(status)status.innerHTML='<span>Finding matching characters and continuities…</span>';try{const r=await api(`/api/creator/character-autofill?fresh=${Date.now()}`,{method:'POST',headers:{'Cache-Control':'no-store','X-Melumiya-Autofill-Client':'v23-two-phase'},cache:'no-store',body:JSON.stringify({action:'resolve',name,source,client_autofill_version:'v23-two-phase'})});renderCharacterAutofillCandidates(r.candidates||[]);if(status){const n=(r.candidates||[]).length;status.innerHTML=`<strong>${n} match${n===1?'':'es'} found</strong><span>Choose the exact character / continuity, then continue to Autofill.</span>`}}catch(e){if(status)status.innerHTML=`<span class="canon-warning">${escapeHtml(e.message)}</span>`}finally{if(b){b.disabled=false;b.textContent='1 · Find character'}}}
async function runCharacterAutofill(){const name=fieldValue('creatorAutofillQuery')||fieldValue('creatorTitle'),source=fieldValue('creatorSource'),selected=state.characterAutofillSelectedSource;if(!name){alert('Type the character name first.');return}if(!selected){alert('Find the character first, then choose the correct source / continuity.');return}const b=$('#characterAutofillBtn'),replace=Boolean($('#characterAutofillReplace')?.checked),status=$('#characterAutofillStatus');b.disabled=true;b.textContent='Autofilling…';if(status)status.innerHTML='<span>Researching the character you selected. This source is locked for continuity.</span>';try{const existing=buildDetailedCharacterPayload(state.characterDraftBase||{});const r=await api(`/api/creator/character-autofill?fresh=${Date.now()}`,{method:'POST',headers:{'Cache-Control':'no-store','X-Melumiya-Autofill-Client':'v23-two-phase'},cache:'no-store',body:JSON.stringify({action:'autofill',name,source,selected_source_url:selected,existing,client_autofill_version:'v23-two-phase'})});applyCharacterAutofill(r,replace)}catch(e){if(status)status.innerHTML=`<span class="canon-warning">${escapeHtml(e.message)}</span>`}finally{b.disabled=false;b.textContent='2 · Autofill selected character'}}
function buildDetailedCharacterPayload(base={}){const p={...base};
  p.identity={full_name:fieldValue('creatorTitle'),aliases:fieldLines('charAliases'),age:fieldValue('charAge'),birthday:fieldValue('charBirthday'),birthplace:fieldValue('charBirthplace'),nationality:fieldValue('charNationality'),languages:fieldLines('charLanguages'),gender:fieldValue('charGender'),pronouns:fieldValue('charPronouns'),sexuality:fieldValue('charSexuality'),occupation:fieldValue('charOccupation'),education:fieldValue('charEducation')};
  p.appearance_profile={height:fieldValue('charHeight'),build:fieldValue('charBuild'),skin_tone:fieldValue('charSkinTone'),eye_color:fieldValue('charEyeColor'),hair_color:fieldValue('charHairColor'),hair_style:fieldValue('charHairStyle'),hair_length:fieldValue('charHairLength'),facial_hair:fieldValue('charFacialHair'),scars:fieldLines('charScars'),tattoos:fieldLines('charTattoos'),piercings:fieldLines('charPiercings'),distinguishing_features:fieldLines('charFeatures'),clothing_style:fieldValue('charClothing'),scent:fieldValue('charScent'),voice:fieldValue('charVoicePhysical')};
  p.personality_profile={traits:fieldLines('charPersonality'),strengths:fieldLines('charStrengths'),flaws:fieldLines('charFlaws'),fears:fieldLines('charFears'),insecurities:fieldLines('charInsecurities'),habits:fieldLines('charHabits'),mannerisms:fieldLines('charMannerisms'),temper:fieldValue('charTemper'),humor:fieldValue('charHumor'),social_style:fieldValue('charSocialStyle'),morals:fieldLines('charMorals'),values:fieldLines('charValues'),boundaries:fieldLines('charBoundaries')};
  p.preferences={likes:fieldLines('charLikes'),dislikes:fieldLines('charDislikes'),hobbies:fieldLines('charHobbies'),favorite_foods:fieldLines('charFavFood'),favorite_drinks:fieldLines('charFavDrink'),favorite_music:fieldLines('charFavMusic'),favorite_books:fieldLines('charFavBooks'),favorite_films:fieldLines('charFavFilms'),comforts:fieldLines('charComforts'),pet_peeves:fieldLines('charPetPeeves'),guilty_pleasures:fieldLines('charGuiltyPleasures')};
  p.relationships={family:fieldLines('charFamily'),friends:fieldLines('charFriends'),rivals:fieldLines('charRivals'),exes:fieldLines('charExes'),current_relationships:fieldLines('charCurrentRelationships'),important_people:fieldLines('charImportantPeople'),attachment_style:fieldValue('charAttachment'),jealousy_style:fieldValue('charJealousy'),anger_style:fieldValue('charAnger'),hurt_style:fieldValue('charHurt'),love_style:fieldValue('charLove')};
  p.life_history={background:fieldValue('charBackground'),childhood:fieldValue('charChildhood'),upbringing:fieldValue('charUpbringing'),achievements:fieldLines('charAchievements'),failures:fieldLines('charFailures'),losses:fieldLines('charLosses'),secrets:fieldLines('charLifeSecrets'),regrets:fieldLines('charRegrets'),defining_events:fieldLines('charDefiningEvents')};
  p.current_life={home:fieldValue('charHome'),finances:fieldValue('charFinances'),routines:fieldLines('charRoutines'),possessions:fieldLines('charPossessions'),pets:fieldLines('charPets'),vehicles:fieldLines('charVehicles'),goals:fieldLines('charGoals'),problems:fieldLines('charProblems'),responsibilities:fieldLines('charResponsibilities')};
  p.rp_behavior={speech_style:fieldValue('charSpeech'),vocabulary:fieldValue('charVocabulary'),accent:fieldValue('charAccent'),texting_style:fieldValue('charTexting'),affection_style:fieldValue('charAffection'),conflict_style:fieldValue('charConflict'),flirting_style:fieldValue('charFlirting'),intimacy_boundaries:fieldLines('charIntimacyBoundaries')};
  p.knowledge_skills={talents:fieldLines('charTalents'),qualifications:fieldLines('charQualifications'),combat_skills:fieldLines('charCombat'),technical_skills:fieldLines('charTechnical'),creative_skills:fieldLines('charCreative'),weaknesses:fieldLines('charSkillWeaknesses'),knows:fieldLines('charKnows'),does_not_know:fieldLines('charDoesNotKnow')};
  p.continuity={immutable_facts:fieldLines('charImmutable'),flexible_facts:fieldLines('charFlexible'),secrets:fieldLines('charContinuitySecrets'),knowledge_permissions:fieldLines('charKnowledgePermissions'),must_never_happen:fieldLines('charNever')};
  p.lore={species:fieldValue('charSpecies'),supernatural_status:fieldValue('charSupernatural'),powers:fieldLines('charPowers'),power_limits:fieldLines('charPowerLimits'),weaknesses:fieldLines('charLoreWeaknesses'),affiliations:fieldLines('charAffiliations'),factions:fieldLines('charFactions'),magic_system_notes:fieldValue('charMagicNotes')};
  p.image_url=fieldValue('creatorImage');p.occupation=p.identity.occupation;p.personality=p.personality_profile.traits;p.background=p.life_history.background||[p.life_history.childhood,p.life_history.upbringing,...p.life_history.defining_events].filter(Boolean).join('\n');p.backstory=p.background;p.speech_style=p.rp_behavior.speech_style;p.voice=p.appearance_profile.voice;p.world_rules=uniq([...(p.world_rules||[]),...p.continuity.must_never_happen]);p.character_schema='melumiyachat_character_dossier_v2';p.web_research=state.characterAutofillResearch||p.web_research||null;return p}
function openCreatorForm(existing=null,type=null){
  const actual=type||existing?.item_type||'scenario'; if(actual==='scenario'){openScenarioCreator(existing);return;}
  const x=existing||{},p=x.payload||{},idn=p.identity||{},ap=p.appearance_profile||{},pp=p.personality_profile||{},pr=p.preferences||{},rel=p.relationships||{},lh=p.life_history||{},cl=p.current_life||{},rp=p.rp_behavior||{},ks=p.knowledge_skills||{},ct=p.continuity||{},lr=p.lore||{};state.characterDraftBase=p;state.characterAutofillResearch=p.web_research||null;state.creationResearchDrafts=p.creation_research||{};const tags=(x.tags||[]).join(', ');
  state.characterAutofillSelectedSource=null;const top=`<div class="creator-form-intro"><span class="tag">Character dossier</span><strong>Quick Create works with only a name. Everything else is optional.</strong></div><div class="character-autofill"><div><strong>🌐 Two-step character Autofill</strong><span>First find the character. Then choose the exact source / continuity before MelumiyaChat fills anything.</span></div><div class="form-row double"><label>Character / search name<input id="creatorAutofillQuery" value="${escapeHtml(x.title||'')}" placeholder="e.g. Keegan Russ"></label><label>Source / franchise (optional)<input id="creatorSource" value="${escapeHtml(x.source_name==='Original'?'':(x.source_name||''))}" placeholder="e.g. Call of Duty: Ghosts"></label></div><button id="characterAutofillSearchBtn" class="secondary" type="button">1 · Find character</button><div id="characterAutofillCandidates" class="character-source-list"></div><label class="check-row"><input id="characterAutofillReplace" type="checkbox"> Replace fields I already filled</label><button id="characterAutofillBtn" class="secondary" type="button" disabled>2 · Autofill selected character</button><div id="characterAutofillStatus" class="character-autofill-status"><span>No fields are filled until you choose the character/source.</span></div></div>`;
  const basic=`${cinput('creatorTitle','Full name',x.title||idn.full_name||'','Required')}${ctextarea('creatorSummary','Public description',x.summary||'',4,'Short description shown in the library')}${ctextarea('charAliases','Nicknames / aliases',idn.aliases||[],2,'One per line')}${cinput('charAge','Age',idn.age||'')}${cinput('charBirthday','Birthday',idn.birthday||'')}${cinput('charBirthplace','Place of birth',idn.birthplace||'')}${cinput('charNationality','Nationality',idn.nationality||'')}${ctextarea('charLanguages','Languages',idn.languages||[],2,'One per line')}${cinput('charGender','Gender',idn.gender||'')}${cinput('charPronouns','Pronouns',idn.pronouns||'')}${cinput('charSexuality','Sexuality',idn.sexuality||'')}${cinput('charOccupation','Occupation / role',idn.occupation||p.occupation||'')}${cinput('charEducation','Education',idn.education||'')}`;
  const appearance=`${cinput('charHeight','Height',ap.height||'')}${cinput('charBuild','Build / body type',ap.build||'')}${cinput('charSkinTone','Skin tone',ap.skin_tone||'')}${cinput('charEyeColor','Eye colour',ap.eye_color||'')}${cinput('charHairColor','Hair colour',ap.hair_color||'')}${cinput('charHairStyle','Hair style',ap.hair_style||'')}${cinput('charHairLength','Hair length',ap.hair_length||'')}${cinput('charFacialHair','Facial hair',ap.facial_hair||'')}${ctextarea('charScars','Scars',ap.scars||[],2,'One per line')}${ctextarea('charTattoos','Tattoos',ap.tattoos||[],2)}${ctextarea('charPiercings','Piercings',ap.piercings||[],2)}${ctextarea('charFeatures','Distinguishing features',ap.distinguishing_features||[],3)}${cinput('charClothing','Usual clothing / style',ap.clothing_style||'')}${cinput('charScent','Scent',ap.scent||'')}${cinput('charVoicePhysical','Voice',ap.voice||p.voice||'')}`;
  const personality=`${ctextarea('charPersonality','Core personality traits',pp.traits||p.personality||[],3,'One per line')}${ctextarea('charStrengths','Strengths',pp.strengths||[],3)}${ctextarea('charFlaws','Flaws',pp.flaws||[],3)}${ctextarea('charFears','Fears',pp.fears||[],3)}${ctextarea('charInsecurities','Insecurities',pp.insecurities||[],3)}${ctextarea('charHabits','Habits',pp.habits||[],3)}${ctextarea('charMannerisms','Mannerisms',pp.mannerisms||[],3)}${cinput('charTemper','Temper',pp.temper||'')}${cinput('charHumor','Humour',pp.humor||'')}${cinput('charSocialStyle','Social style',pp.social_style||'')}${ctextarea('charMorals','Morals',pp.morals||[],2)}${ctextarea('charValues','Values',pp.values||[],2)}${ctextarea('charBoundaries','Personal boundaries',pp.boundaries||[],2)}`;
  const preferences=`${ctextarea('charLikes','Likes',pr.likes||[],3)}${ctextarea('charDislikes','Dislikes',pr.dislikes||[],3)}${ctextarea('charHobbies','Hobbies',pr.hobbies||[],3)}${ctextarea('charFavFood','Favourite foods',pr.favorite_foods||[],2)}${ctextarea('charFavDrink','Favourite drinks',pr.favorite_drinks||[],2)}${ctextarea('charFavMusic','Favourite music',pr.favorite_music||[],2)}${ctextarea('charFavBooks','Favourite books',pr.favorite_books||[],2)}${ctextarea('charFavFilms','Favourite films / shows',pr.favorite_films||[],2)}${ctextarea('charComforts','Comforts',pr.comforts||[],2)}${ctextarea('charPetPeeves','Pet peeves',pr.pet_peeves||[],2)}${ctextarea('charGuiltyPleasures','Guilty pleasures',pr.guilty_pleasures||[],2)}`;
  const relationships=`${ctextarea('charFamily','Family',rel.family||[],3)}${ctextarea('charFriends','Friends',rel.friends||[],3)}${ctextarea('charRivals','Rivals / enemies',rel.rivals||[],3)}${ctextarea('charExes','Exes',rel.exes||[],2)}${ctextarea('charCurrentRelationships','Current relationships',rel.current_relationships||[],3)}${ctextarea('charImportantPeople','Other important people',rel.important_people||[],3)}${cinput('charAttachment','Attachment style',rel.attachment_style||'')}${cinput('charJealousy','How they act when jealous',rel.jealousy_style||'')}${cinput('charAnger','How they act when angry',rel.anger_style||'')}${cinput('charHurt','How they act when hurt',rel.hurt_style||'')}${cinput('charLove','How they act when in love',rel.love_style||'')}`;
  const history=`${ctextarea('charBackground','General backstory',lh.background||p.background||p.backstory||'',5)}${ctextarea('charChildhood','Childhood',lh.childhood||'',4)}${ctextarea('charUpbringing','Upbringing',lh.upbringing||'',4)}${ctextarea('charDefiningEvents','Defining events — things that changed them',lh.defining_events||[],6,'One event per line')}${ctextarea('charAchievements','Achievements',lh.achievements||[],3)}${ctextarea('charFailures','Failures',lh.failures||[],3)}${ctextarea('charLosses','Losses',lh.losses||[],3)}${ctextarea('charLifeSecrets','Secrets',lh.secrets||[],3)}${ctextarea('charRegrets','Regrets',lh.regrets||[],3)}`;
  const current=`${cinput('charHome','Home / where they live',cl.home||'')}${cinput('charFinances','Finances / economic situation',cl.finances||'')}${ctextarea('charRoutines','Routines',cl.routines||[],3)}${ctextarea('charPossessions','Important possessions',cl.possessions||[],3)}${ctextarea('charPets','Pets',cl.pets||[],2)}${ctextarea('charVehicles','Vehicles',cl.vehicles||[],2)}${ctextarea('charGoals','Current goals',cl.goals||[],3)}${ctextarea('charProblems','Current problems',cl.problems||[],3)}${ctextarea('charResponsibilities','Responsibilities',cl.responsibilities||[],3)}`;
  const behavior=`${cinput('charSpeech','Speech style',rp.speech_style||p.speech_style||'')}${cinput('charVocabulary','Vocabulary',rp.vocabulary||'')}${cinput('charAccent','Accent',rp.accent||'')}${cinput('charTexting','Texting style',rp.texting_style||'')}${cinput('charAffection','Affection style',rp.affection_style||'')}${cinput('charConflict','Conflict style',rp.conflict_style||'')}${cinput('charFlirting','Flirting style',rp.flirting_style||'')}${ctextarea('charIntimacyBoundaries','Intimacy boundaries',rp.intimacy_boundaries||[],3)}`;
  const skills=`${ctextarea('charTalents','Talents',ks.talents||[],3)}${ctextarea('charQualifications','Qualifications',ks.qualifications||[],3)}${ctextarea('charCombat','Combat skills',ks.combat_skills||[],3)}${ctextarea('charTechnical','Technical skills',ks.technical_skills||[],3)}${ctextarea('charCreative','Creative skills',ks.creative_skills||[],3)}${ctextarea('charSkillWeaknesses','Skill weaknesses / limitations',ks.weaknesses||[],3)}${ctextarea('charKnows','What they know',ks.knows||[],3)}${ctextarea('charDoesNotKnow','What they do NOT know',ks.does_not_know||[],3)}`;
  const continuity=`${ctextarea('charImmutable','Immutable facts',ct.immutable_facts||[],4,'Facts the model must never casually change')}${ctextarea('charFlexible','Flexible facts',ct.flexible_facts||[],3)}${ctextarea('charContinuitySecrets','Continuity secrets',ct.secrets||[],3)}${ctextarea('charKnowledgePermissions','Who knows what',ct.knowledge_permissions||[],4,'One rule per line')}${ctextarea('charNever','Must never happen',ct.must_never_happen||p.world_rules||[],4,'Hard prohibitions, one per line')}`;
  const lore=`${cinput('charSpecies','Species / race',lr.species||'')}${cinput('charSupernatural','Supernatural status',lr.supernatural_status||'')}${ctextarea('charPowers','Powers / abilities',lr.powers||[],3)}${ctextarea('charPowerLimits','Power limits / costs',lr.power_limits||[],3)}${ctextarea('charLoreWeaknesses','Lore weaknesses',lr.weaknesses||[],3)}${ctextarea('charAffiliations','Affiliations / organizations',lr.affiliations||[],3)}${ctextarea('charFactions','Factions',lr.factions||[],2)}${ctextarea('charMagicNotes','Magic / power-system notes',lr.magic_system_notes||'',4)}`;
  const image=`<div class="image-field-row"><label>Character image<input id="creatorImage" value="${escapeHtml(p.image_url||'')}" placeholder="Generated or uploaded image"></label><div class="row-actions"><button type="button" id="creatorUploadImage" class="ghost">↑ Upload image</button><button type="button" id="creatorGenerateImage" class="secondary">✦ Generate portrait</button></div></div>`;
  const publish=`<div class="form-row triple"><label>Rating<select id="creatorRating"><option ${x.content_rating!=='NSFW'?'selected':''}>SFW</option><option ${x.content_rating==='NSFW'?'selected':''}>NSFW</option></select></label><label>Canon mode<select id="creatorCanon"><option ${x.canon_mode==='Strict Canon'?'selected':''}>Strict Canon</option><option ${x.canon_mode==='Canon-Inspired'?'selected':''}>Canon-Inspired</option><option ${(!x.canon_mode||x.canon_mode==='Selective Canon')?'selected':''}>Selective Canon</option><option ${x.canon_mode==='Full AU'?'selected':''}>Full AU</option></select></label><label>Visibility<select id="creatorVisibility"><option ${x.visibility!=='Unlisted'&&x.visibility!=='Public'?'selected':''}>Private</option><option ${x.visibility==='Unlisted'?'selected':''}>Unlisted</option><option ${x.visibility==='Public'?'selected':''}>Public</option></select></label></div><label>Tags<input id="creatorTags" value="${escapeHtml(tags)}" placeholder="Slow burn, military, fantasy…"></label>${presetTagButtons('creatorTags')}`;
  openModal(existing?'Edit character':'Create character',`${top}${image}${csection('Identity','Who they are',basic,true)}${csection('Appearance','What they look and sound like',appearance)}${csection('Personality','How they think, feel and behave',personality)}${csection('Likes & dislikes','Preferences, comforts and pet peeves',preferences)}${csection('Relationships','Family, friends, rivals and attachment',relationships)}${csection('Life history','Backstory and defining events',history)}${csection('Current life','Home, routines, goals and responsibilities',current)}${csection('RP behaviour','Dialogue, texting, affection and conflict',behavior)}${csection('Knowledge & skills','Capabilities and knowledge boundaries',skills)}${csection('Continuity rules','Facts and hard prohibitions the model must preserve',continuity)}${csection('Lore / supernatural','Optional fantasy, sci-fi or power-system details',lore)}${csection('Publishing','Library visibility and metadata',publish,true)}<div class="modal-actions"><button id="savePublicCreation" class="primary" data-type="character" data-id="${x.id||''}">Save draft</button></div>`);
  $('#creatorUploadImage').onclick=()=>chooseLocalImage('creatorImage');$('#creatorGenerateImage').onclick=()=>openImageGenerator('character','creatorImage',imagePromptFromFields('character'));$('#characterAutofillSearchBtn').onclick=findCharacterAutofillSources;$('#characterAutofillBtn').onclick=runCharacterAutofill;$('#creatorAutofillQuery').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();findCharacterAutofillSources()}});const resetCharacterSourceChoice=()=>{state.characterAutofillSelectedSource=null;const box=$('#characterAutofillCandidates');if(box)box.innerHTML='';const go=$('#characterAutofillBtn');if(go)go.disabled=true};$('#creatorAutofillQuery').addEventListener('input',resetCharacterSourceChoice);$('#creatorSource').addEventListener('input',resetCharacterSourceChoice);$$('.public-tag-preset').forEach(b=>b.onclick=()=>{const input=$('#'+b.dataset.target);if(!input)return;const vals=input.value.split(',').map(x=>x.trim()).filter(Boolean);if(!vals.some(v=>v.toLowerCase()===b.dataset.tag.toLowerCase()))vals.push(b.dataset.tag);input.value=vals.join(', ')});
}
async function savePublicCreation(type,id){
  let payload={};
  if(type==='character'){
    payload=buildDetailedCharacterPayload(state.characterDraftBase||{});if(state.canonDrafts.main)payload.canon_research=state.canonDrafts.main;payload.creation_research=state.creationResearchDrafts;
    const body={item_type:type,creator_name:'You',title:fieldValue('creatorTitle'),summary:fieldValue('creatorSummary'),content_rating:$('#creatorRating').value,canon_mode:$('#creatorCanon').value,visibility:$('#creatorVisibility').value,source_name:fieldValue('creatorSource')||'Original',parent_character:null,tags:$('#creatorTags').value.split(',').map(x=>x.trim()).filter(Boolean),payload};
    if(!body.title){alert('Give the character a name first.');return}await api(id?`/api/creator/content/${id}`:'/api/creator/content',{method:id?'PATCH':'POST',body:JSON.stringify(body)});closeModal();await loadCreatorContent();toast('Character saved to My Character Library.');return;
  }
  try{payload=JSON.parse($('#creatorPayload')?.value||'{}')}catch(e){alert('Advanced structured data is not valid JSON.');return}
  if($('#creatorImage'))payload.image_url=$('#creatorImage').value||'';
  const c0=scenarioParticipantWinCompat();payload.card_hook=fieldValue('creatorCardHook');payload.premise=fieldValue('creatorPremise');payload.user_role=fieldValue('creatorUserRole');payload.user_role_facts=lines(fieldValue('creatorUserFacts'));payload.participants=c0.participants;payload.primary_participant_ids=c0.participants.filter(x=>(x.type==='character'||x.type==='npc')&&x.story_role==='main').map(x=>x.participant_id);payload.key_characters=c0.key;payload.side_characters=c0.secondary;payload.scenario_setup=fieldValue('creatorScenarioSetup');payload.starting_situation=payload.scenario_setup;payload.opening_prompt=fieldValue('creatorOpeningMessage');payload.relationship_start=$('#creatorRelationship').value;payload.relationship_detail=fieldValue('creatorRelationshipDetail');payload.relationship_setup=lines(fieldValue('creatorRelationshipHistory'));payload.relationship_history=[...payload.relationship_setup];payload.starting_scene={location:fieldValue('creatorSceneLocation'),date_or_era:fieldValue('creatorSceneDate'),time:fieldValue('creatorSceneTime'),present_cast:c0.present,situation:fieldValue('creatorSceneSituation')};payload.history_backstory=lines(fieldValue('creatorHistory'));payload.history_notes=[...payload.history_backstory];payload.locations=lines(fieldValue('creatorLocations'));payload.groups_factions=lines(fieldValue('creatorGroups'));payload.important_objects=lines(fieldValue('creatorObjects'));payload.secrets_knowledge=parseScenarioSecrets(lines(fieldValue('creatorSecrets')));payload.world_rules=lines(fieldValue('creatorWorldRules'));payload.current_state=lines(fieldValue('creatorCurrentState'));payload.physical_continuity=lines(fieldValue('creatorPhysicalContinuity'));payload.story_timeline=lines(fieldValue('creatorTimeline'));payload.timeline_seed=[...payload.story_timeline];payload.goals_hooks=lines(fieldValue('creatorHooks'));payload.hard_scenario_rules=lines(fieldValue('creatorHardRules'));payload.world_facts=[...payload.hard_scenario_rules];payload.tone_genre={genre:fieldValue('creatorGenre'),tone:fieldValue('creatorTone')};payload.creation_research=state.creationResearchDrafts;payload.web_research=state.scenarioAutofillResearch||payload.web_research||null;payload.scenario_schema='melumiyachat_scenario_participants_v1';
  if(!$('#publicInlineCharacter').classList.contains('hidden')&&$('#publicInlineCharName').value.trim()){let cp={};try{cp=JSON.parse($('#publicInlineCharPayload').value||'{}')}catch(e){alert('New character data is not valid JSON.');return}if(state.canonDrafts.inline)cp.canon_research=state.canonDrafts.inline;const char=await api('/api/creator/content',{method:'POST',body:JSON.stringify({item_type:'character',creator_name:'You',title:$('#publicInlineCharName').value.trim(),summary:$('#publicInlineCharSummary').value,content_rating:$('#creatorRating').value,canon_mode:$('#creatorCanon').value,visibility:'Private',source_name:fieldValue('creatorSource')||'Original',tags:$('#publicInlineCharTags').value.split(',').map(x=>x.trim()).filter(Boolean),payload:cp})});state.creatorCharacters.push(char);const has=state.scenarioParticipantsDraft.some(x=>(x.type==='character'||x.type==='npc')&&x.story_role==='main');state.scenarioParticipantsDraft.push(scenarioParticipantWinNormalize({type:'character',linked_entity_id:String(char.id),name:char.title,story_role:has?'secondary':'main',snapshot:scenarioParticipantWinSnapshot(char),present_at_start:true},state.scenarioParticipantsDraft.length));toast('Character saved to My Character Library.')}
  const c=scenarioParticipantWinCompat(),main=c.main,mainObj=main?.linked_entity_id?state.creatorCharacters.find(x=>String(x.id)===String(main.linked_entity_id))||null:null;payload.participants=c.participants;payload.primary_participant_ids=c.participants.filter(x=>(x.type==='character'||x.type==='npc')&&x.story_role==='main').map(x=>x.participant_id);payload.key_characters=c.key;payload.side_characters=c.secondary;payload.starting_scene.present_cast=c.present;if(main){payload.parent_character_id=main.linked_entity_id||null;payload.parent_character_snapshot=main.snapshot||scenarioParticipantWinSnapshot(mainObj)||null}else{delete payload.parent_character_id;delete payload.parent_character_snapshot}payload.linked_characters=c.linked;payload.world_bible_seed=buildScenarioWorldBibleSeed(fieldValue('creatorTitle'),fieldValue('creatorSummary'),payload);
  const body={item_type:'scenario',creator_name:'You',title:$('#creatorTitle').value,summary:$('#creatorSummary').value,content_rating:$('#creatorRating').value,canon_mode:$('#creatorCanon').value,visibility:$('#creatorVisibility').value,source_name:$('#creatorSource').value||'Original',parent_character:main?.name||null,tags:$('#creatorTags').value.split(',').map(x=>x.trim()).filter(Boolean),payload};await api(id?`/api/creator/content/${id}`:'/api/creator/content',{method:id?'PATCH':'POST',body:JSON.stringify(body)});closeModal();await loadCreatorContent();
}

function syncSettings(){
  const c=state.bundle.conversation; const scene=state.bundle.state.scene || {};
  $('#personaName').value=c.persona_name; $('#scenarioName').value=c.scenario_name; $('#canonMode').value=c.canon_mode; $('#timelineMode').value=c.timeline_mode;
  $('#replyLength').value=c.reply_length; $('#initiative').value=c.initiative; $('#relationshipRealism').value=c.relationship_realism;
  const controlled=scene.controlled_character || c.persona_name; $('#controlledCharacterBadge').textContent=`Playing as: ${controlled}`;

  $('#storyStartDate').value=scene.start_date || scene.date || localDate();
  $('#storyStartTime').value=scene.start_time || scene.time || localTime();
  const started=Boolean(scene.story_started);
  $('#storyStartDate').disabled=started; $('#storyStartTime').disabled=started;
  $('#storyClockState').textContent=started?'story running':'not started';
  $('#storyClockState').style.color=started?'var(--good)':'';
  $('#storyClockHelp').textContent=started
    ? 'The start is locked because the RP has begun. Current story time keeps advancing; you can still correct it from World Bible → Current Scene.'
    : 'This date/time belongs to the first sent message. After that, story time advances with the RP.';
  $('#currentStoryClock').innerHTML=`<span class="story-clock-current">Current story time: ${escapeHtml(scene.date||scene.start_date||'—')} · ${escapeHtml(scene.time||scene.start_time||'—')}</span><br><span class="muted">${escapeHtml(scene.last_advance_reason||'The first message has not been sent yet.')}</span>`;
}
function renderSceneStrip(){
  const s=state.bundle?.state?.scene || {};
  const chips=[['Story time',`${s.date||s.start_date||'—'} · ${s.time||s.start_time||'—'}`],['Location',s.location],['Present',(s.present||[]).join(', ')||'unset'],['Controlled',s.controlled_character]];
  $('#sceneStrip').innerHTML=chips.map(([k,v])=>`<span class="scene-chip"><b>${escapeHtml(k)}:</b> ${escapeHtml(v||'unset')}</span>`).join('');
}
function renderMessages(){
  const box=$('#messages'); const msgs=state.bundle?.messages || [];
  if(!msgs.length){box.innerHTML=`<div class="empty-state"><div><h2>Start this RP</h2><p>Choose the first-message date/time in Scene controls, then send your opening message.</p></div></div>`;return;}
  const byGroup={}; msgs.filter(m=>m.role==='assistant'&&m.turn_group).forEach(m=>(byGroup[m.turn_group] ||= []).push(m));
  box.innerHTML=msgs.map(m=>{
    const variants=m.turn_group ? byGroup[m.turn_group]||[] : [];
    const isAlt=m.role==='assistant' && !m.is_selected;
    const sourceStamp=m.source_timestamp?`<span class="message-story-time">· imported ${escapeHtml(m.source_timestamp)}</span>`:'';
    const storyStamp=(m.story_date||m.story_time)?`<span class="message-story-time">· ${escapeHtml(m.story_date||'')} ${escapeHtml(m.story_time||'')}</span>`:sourceStamp;
    const assistantTools=m.role==='assistant'?`${m.model_provider!=='scenario-seed'?`<button class="tiny-btn" data-reroll="${m.id}">↻ Regenerate</button>`:''}${isAlt?`<button class="tiny-btn" data-select-reroll="${m.id}">✓ Make canon</button>`:''}${variants.length>1?`<span class="tiny-btn">${variants.length} versions</span>`:''}<button class="tiny-btn" data-inspect="${m.id}">Why this input?</button>`:'';
    return `<div class="message ${m.role} ${isAlt?'unselected':''}" data-id="${m.id}">
      <div class="message-meta">${storyStamp}${m.model_provider?.startsWith?.('import:')?'<span>imported history</span>':''}${isAlt?'<span>regenerated alternative</span>':''}</div>
      <div class="bubble">${renderRpMarkup(m.content)}</div>
      <div class="message-actions">${assistantTools}<button class="tiny-btn" data-clone-message="${m.id}" title="Create a new RP branch ending here">⎘ Clone from here</button><button class="tiny-btn message-delete-btn" data-delete-message="${m.id}" title="Delete this message">× Delete</button></div>
    </div>`;
  }).join('');
  box.scrollTop=box.scrollHeight;
  $$('[data-reroll]').forEach(b=>b.onclick=()=>reroll(b.dataset.reroll));
  $$('[data-select-reroll]').forEach(b=>b.onclick=()=>selectReroll(b.dataset.selectReroll));
  $$('[data-inspect]').forEach(b=>b.onclick=()=>inspectGeneration(b.dataset.inspect));
  $$('[data-clone-message]').forEach(b=>b.onclick=()=>cloneRpFromMessage(b.dataset.cloneMessage));
  $$('[data-delete-message]').forEach(b=>b.onclick=()=>deleteRpMessage(b.dataset.deleteMessage));
}

async function cloneRpFromMessage(messageId){
  if(!state.conversationId)return;
  if(!confirm('Clone this RP from here?\n\nA new branch will copy the active transcript through this message. The original RP stays untouched.'))return;
  try{
    const result=await api(`/api/conversations/${state.conversationId}/messages/${messageId}/clone`,{method:'POST'});
    await loadConversations();
    state.conversationId=String(result.rp_id||result.conversation_id);
    const picker=$('#conversationSelect'); if(picker)picker.value=state.conversationId;
    await loadBundle();
    notify?.('RP branch created');
  }catch(e){alert(e.message||String(e));}
}

async function deleteRpMessage(messageId){
  if(!state.conversationId)return;
  if(!confirm('Delete this message permanently?\n\nAuto-created continuity sourced only from this message is cleaned up. Manual memories/canon are preserved. This cannot be undone.'))return;
  try{
    await api(`/api/conversations/${state.conversationId}/messages/${messageId}`,{method:'DELETE'});
    await loadBundle();
  }catch(e){alert(e.message||String(e));}
}

function showGenerationTyping(optimisticText=''){
  const box=$('#messages'); if(!box)return;
  $$('.rp-optimistic,.rp-typing').forEach(x=>x.remove());
  if(optimisticText){const u=document.createElement('div');u.className='message user rp-optimistic';u.innerHTML=`<div class="bubble">${renderRpMarkup(optimisticText)}</div>`;box.appendChild(u)}
  const t=document.createElement('div');t.className='message assistant rp-typing';t.setAttribute('aria-label','Character is writing');t.innerHTML='<div class="bubble rp-typing-bubble"><span class="rp-typing-dots" aria-hidden="true"><span class="rp-typing-dot"></span><span class="rp-typing-dot"></span><span class="rp-typing-dot"></span></span></div>';box.appendChild(t);box.scrollTop=box.scrollHeight;
}
function hideGenerationTyping(){$$('.rp-optimistic,.rp-typing').forEach(x=>x.remove())}

async function sendMessage(){
  const input=$('#messageInput'); const content=input.value.trim(); if(!content)return;
  if(/^\/cmd(?:\s|$)/i.test(content)){
    const command=content.replace(/^\/cmd\s*/i,'').trim();
    if(!command){openCustomDirectorCommand();return;}
    input.value=''; await runDirectorCommand(command); return;
  }
  $('#sendBtn').disabled=true; $('#generationNotice').textContent='Character is writing…'; showGenerationTyping(content);
  try{
    const result=await api('/api/chat',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,content,model_key:$('#modelChoiceSelect')?.value||null})});
    input.value=''; hideGenerationTyping();
    const flags=result.validation?.flags||[]; const clock=result.story_clock||{};
    $('#generationNotice').textContent=`Generated in ${result.latency_ms ?? '?'} ms · format/canon ${result.validation?.score ?? '?'}${flags.length?` · ${flags.length} flag(s)`:''} · story ${clock.date||''} ${clock.time||''}`;
    await loadBundle();
  }catch(e){hideGenerationTyping();$('#generationNotice').textContent=e.message;await loadBundle().catch(()=>{});}
  finally{$('#sendBtn').disabled=false;}
}
async function runDirectorCommand(command){
  command=String(command||'').trim(); if(!command)return;
  const send=$('#sendBtn'),cont=$('#continueBtn'),trigger=$('#commandPaletteBtn');
  [send,cont,trigger].forEach(x=>{if(x)x.disabled=true});
  $('#composerMenu')?.classList.add('hidden');
  $('#generationNotice').textContent='Character is writing…'; showGenerationTyping();
  try{
    const result=await api('/api/director',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,command,model_key:$('#modelChoiceSelect')?.value||null})});
    hideGenerationTyping(); $('#generationNotice').textContent=`Director command applied · hidden · one reply only · story clock unchanged${result.latency_ms!=null?` · ${result.latency_ms} ms`:''}`;
    await loadBundle();
  }catch(e){hideGenerationTyping();$('#generationNotice').textContent=`Director command failed · ${e.message}`;await loadBundle().catch(()=>{})}
  finally{[send,cont,trigger].forEach(x=>{if(x)x.disabled=false})}
}
function openCustomDirectorCommand(initial=''){
  openModal('Director command',`<div class="advanced-explain"><strong>Hidden from the RP.</strong><span>This controls the next bot reply only. It is not added as your dialogue, does not advance story time, and the command itself is not saved as canon or memory.</span></div><label>What should the bot do?<textarea id="directorCommandText" rows="5" placeholder="e.g. Make him more jealous, but keep it subtle. Slow the pacing. Let the side characters react more.">${escapeHtml(initial)}</textarea></label><div class="modal-actions"><button id="runDirectorCommandBtn" class="primary">Run hidden command</button></div>`);
  $('#runDirectorCommandBtn').onclick=async()=>{const text=$('#directorCommandText').value.trim();if(!text)return;closeModal();await runDirectorCommand(text)};
  setTimeout(()=>$('#directorCommandText')?.focus(),50);
}
async function reroll(messageId){
  $('#generationNotice').textContent='Regenerating…'; showGenerationTyping();
  try{
    const result=await api('/api/chat',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,content:'',model_key:$('#modelChoiceSelect')?.value||null,reroll_of:messageId})});
    hideGenerationTyping(); $('#generationNotice').textContent=`Regenerated text ready · ${result.latency_ms ?? '?'} ms · NOT canon until selected · story clock unchanged.`;
    await loadBundle();
  }catch(e){hideGenerationTyping();$('#generationNotice').textContent=e.message;await loadBundle().catch(()=>{});}
}
async function selectReroll(id){ await api('/api/rerolls/select',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,message_id:id})}); await loadBundle(); $('#generationNotice').textContent='Selected reroll is now the canonical reply for this turn.'; }
async function continueScene(){
  if(!state.conversationId)return;
  $('#generationNotice').textContent='Character is writing…'; showGenerationTyping();
  try{
    const result=await api('/api/chat',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,content:'',model_key:$('#modelChoiceSelect')?.value||null,continue_without_user:true})});
    hideGenerationTyping(); $('#generationNotice').textContent=`Scene continued · ${result.latency_ms ?? '?'} ms · no user message was invented.`;
    await loadBundle();
  }catch(e){hideGenerationTyping();$('#generationNotice').textContent=e.message;await loadBundle().catch(()=>{});}
}
function inspectGeneration(id){
  const b=state.bundle; const m=b.messages.find(x=>x.id===id);
  openModal('Why did it write it like this?', `<p class="muted">This shows story inputs, not hidden chain-of-thought.</p>
    <label>Model<input value="${escapeHtml(m?.model_provider||'')} / ${escapeHtml(m?.model_name||'')}" readonly></label>
    <label>Story timestamp<input value="${escapeHtml(m?.story_date||'—')} ${escapeHtml(m?.story_time||'—')}" readonly></label>
    <label>Relationship state<textarea rows="7" readonly>${escapeHtml(pretty(b.state.relationship))}</textarea></label>
    <label>Scene state<textarea rows="7" readonly>${escapeHtml(pretty(b.state.scene))}</textarea></label>
    <label>Knowledge state<textarea rows="7" readonly>${escapeHtml(pretty(b.state.knowledge))}</textarea></label>`);
}

function lines(v){return String(v||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean)}
function csv(v){return String(v||'').split(',').map(x=>x.trim()).filter(Boolean)}
function memoryArray(v){if(Array.isArray(v))return v;if(typeof v==='string'){try{const x=JSON.parse(v);if(Array.isArray(x))return x}catch{}return csv(v)}return []}
function memoryWhat(m){return String(m?.what_happened||m?.content||'').trim()}
function memoryMeta(m){const bits=[];const remembers=memoryArray(m?.who_remembers),involved=memoryArray(m?.who_involved);if(remembers.length)bits.push(`Remembers: ${remembers.join(', ')}`);if(involved.length)bits.push(`Involved: ${involved.join(', ')}`);if(m?.story_when)bits.push(`When: ${m.story_when}`);if(m?.story_where)bits.push(`Where: ${m.story_where}`);return bits}
function memoryCardHtml(m,{editable=true}={}){const meta=memoryMeta(m);const source=m?.source_kind||'User-created';const rel=m?.current_relevance||'active';return `<div class="memory-item"><div class="panel-title-row"><div class="memory-title">${escapeHtml(m?.title||m?.memory_type||'Memory')}</div><div class="memory-tags"><span class="tag">${escapeHtml(m?.memory_type||'Other')}</span><span class="tag">${escapeHtml(m?.importance||'Normal')}</span></div></div><div class="memory-body">${escapeHtml(memoryWhat(m))}</div>${meta.length?`<div class="muted compact" style="margin-top:7px">${meta.map(escapeHtml).join(' · ')}</div>`:''}<div class="memory-tags" style="margin-top:8px"><span class="tag">${escapeHtml(rel)}</span>${m?.emotional_weight?`<span class="tag">${escapeHtml(m.emotional_weight)}</span>`:''}<span class="tag">${escapeHtml(source)}</span>${editable?`<button class="tiny-btn" data-edit-memory="${m.id}">edit</button><button class="tiny-btn" data-delete-memory="${m.id}">archive</button>`:''}</div></div>`}
function openMemoryEditor(memory=null){const m=memory||{};const remembers=memoryArray(m.who_remembers).join(', '),involved=memoryArray(m.who_involved).join(', ');const types=['Event','Conversation','Promise','Conflict','Relationship Moment','Discovery','Secret Learned','Preference','Habit','Emotional Moment','Achievement','Injury/Physical Event','Other'];const importance=['Minor','Normal','Important','Critical'];const emotion=['','positive','negative','mixed','neutral','unresolved'];const relevance=['active','resolved','superseded','archived'];openModal(memory?'Edit memory':'Add a memory',`<div class="advanced-explain"><strong>${memory?'Edit what this RP remembers.':'Save something that should matter later.'}</strong><span>Who Remembers controls character knowledge; a memory is not automatically known by everyone.</span></div><label>What Happened<textarea id="memWhat" rows="5" placeholder="What actually happened or became known?">${escapeHtml(memoryWhat(m))}</textarea></label><div class="form-row"><label>Who Remembers<input id="memWhoRemembers" value="${escapeHtml(remembers)}" placeholder="Xaden, Violet, My persona"></label><label>Who Was Involved<input id="memWhoInvolved" value="${escapeHtml(involved)}" placeholder="Xaden, Violet"></label></div><div class="form-row"><label>When<input id="memWhen" value="${escapeHtml(m.story_when||'')}" placeholder="Story date, yesterday, chapter 4…"></label><label>Where<input id="memWhere" value="${escapeHtml(m.story_where||'')}" placeholder="Location, if relevant"></label></div><div class="form-row triple"><label>Memory Type<select id="memType">${types.map(x=>`<option>${escapeHtml(x)}</option>`).join('')}</select></label><label>Importance<select id="memImportance">${importance.map(x=>`<option>${x}</option>`).join('')}</select></label><label>Emotional Weight<select id="memEmotion">${emotion.map(x=>`<option value="${x}">${x||'None / optional'}</option>`).join('')}</select></label></div><div class="form-row"><label>Current Relevance<select id="memRelevance">${relevance.map(x=>`<option>${x}</option>`).join('')}</select></label><label>Source<input value="${escapeHtml(m.source_kind||'User-created')}" readonly></label></div>${m.source_message_id||m.original_context?`<div class="detail-box"><strong>Original Context</strong><div class="muted compact">${m.source_message_id?`Linked RP message: ${escapeHtml(m.source_message_id)}`:'Stored with this memory'} · ${escapeHtml(m.source_kind||'User-created')}</div></div>`:''}<label>Title <span class="muted">(optional)</span><input id="memTitle" value="${escapeHtml(m.title||'')}"></label><div class="modal-actions"><button id="saveMemoryStructured" class="primary">${memory?'Save changes':'Save memory'}</button></div>`);setSelect('#memType',m.memory_type||'Other','Other');setSelect('#memImportance',m.importance||'Normal','Normal');setSelect('#memEmotion',m.emotional_weight||'','');setSelect('#memRelevance',m.current_relevance||'active','active');$('#saveMemoryStructured').onclick=async()=>{const what=$('#memWhat').value.trim();if(!what){toast('Write what happened first.',false);return}const payload={title:$('#memTitle').value.trim()||'Memory',what_happened:what,content:what,who_remembers:csv($('#memWhoRemembers').value),who_involved:csv($('#memWhoInvolved').value),story_when:$('#memWhen').value.trim(),story_where:$('#memWhere').value.trim(),memory_type:$('#memType').value,importance:$('#memImportance').value,emotional_weight:$('#memEmotion').value,current_relevance:$('#memRelevance').value};if(memory)await api(`/api/memories/${memory.id}`,{method:'PATCH',body:JSON.stringify(payload)});else await api('/api/memories',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,...payload,source_kind:'User-created'})});closeModal();await loadBundle();renderDashboard();toast(memory?'Memory updated.':'Memory saved.')};}
function wireMemoryActions(root=document){root.querySelectorAll?.('[data-edit-memory]').forEach(b=>b.onclick=()=>{const m=(state.bundle?.memories||[]).find(x=>String(x.id)===String(b.dataset.editMemory));if(m)openMemoryEditor(m)});root.querySelectorAll?.('[data-delete-memory]').forEach(b=>b.onclick=async()=>{await api(`/api/memories/${b.dataset.deleteMemory}`,{method:'DELETE'});await loadBundle();renderDashboard();if(state.currentView==='chat')openRpMemories();});}
function listText(v){return Array.isArray(v)?v.map(x=>typeof x==='string'?x:(x?.text||x?.fact||x?.event||JSON.stringify(x))).join('\n'):''}
function setSelect(id,value,fallback='Unknown'){const el=$(id);if(!el)return;const vals=[...el.options].map(o=>o.value);el.value=vals.includes(String(value))?String(value):(vals.includes(fallback)?fallback:vals[0]||'')}
function toast(text,good=true){const old=$('.toast');if(old)old.remove();const d=document.createElement('div');d.className=`toast ${good?'good':''}`;d.textContent=text;document.body.appendChild(d);setTimeout(()=>d.remove(),3200)}
async function saveStateDoc(type,payload){await api('/api/state',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,doc_type:type,payload})});await loadBundle();}

function renderDashboard(){
  if(!state.bundle)return; const s=state.bundle.state; const c=state.bundle.conversation;
  $('#dashboardRpName').textContent=c.title; $('#dashboardScope').textContent=`RP: ${c.title} · Scenario: ${c.scenario_name} · Persona: ${c.persona_name}`;
  const cards=[
    ['Now',`${s.scene?.date||'—'} · ${s.scene?.time||'—'}`,s.scene?.location||'Location unset'],
    ['Cast',(s.scene?.present||[]).length?`${s.scene.present.length} present`:'No cast set',(s.scene?.present||[]).join(', ')||'Edit Current Scene'],
    ['Relationship',s.hard_canon?.relationship_status||s.relationship?.phase||'Unset',`Realism: ${c.relationship_realism}`],
    ['Secrets',`${s.knowledge?.facts?.length||0} tracked facts`,'Per-character knowledge stays separate'],
    ['Memories',`${state.bundle.memories.length} active`,'Transcript remains source evidence'],
  ];
  $('#dashboardGrid').innerHTML=cards.map(([k,v,sub])=>`<div class="dash-card"><div class="kicker">${escapeHtml(k)}</div><div class="value">${escapeHtml(v)}</div><div class="sub">${escapeHtml(sub)}</div></div>`).join('');
  const imports=state.bundle.imports||[]; const ip=$('#dashboardImportPanel'); if(ip){ip.classList.toggle('hidden',!imports.length); if(imports.length){const total=imports.reduce((a,x)=>a+Number(x.message_count||0),0);const review=(s.world_bible?.import_review||[]).length;ip.innerHTML=`<div class="panel-title-row"><div><h3>⇲ Imported story history</h3><p class="muted compact">This RP contains ${total} imported messages from ${imports.map(x=>x.source_platform).filter((v,i,a)=>a.indexOf(v)===i).map(escapeHtml).join(', ')}. The transcript stays as evidence while the structured state stays editable.</p></div><div class="page-head-actions"><span class="status-pill ${review?'warn':'good'}">${review} review item${review===1?'':'s'}</span><button class="ghost small" data-view-import="${imports[0].id}">View original</button></div></div>`;}}
  $('#memoryList').innerHTML=state.bundle.memories.length?state.bundle.memories.map(m=>memoryCardHtml(m)).join(''):'<p class="muted">No persistent memories yet for this RP.</p>';
  wireMemoryActions($('#memoryList'));
  const health=[];
  const phys=JSON.stringify(s.physical||{}).toLowerCase(); if(phys.includes('dress')&&phys.includes('jeans'))health.push(['warn','Physical state mentions both dress and jeans — inspect continuity.']);
  if((s.scene?.present||[]).some(x=>(s.scene?.offscreen||[]).includes(x)))health.push(['warn','A character is listed as both present and offscreen.']);
  if((state.bundle.imports||[]).length){const review=(s.world_bible?.import_review||[]).length;health.push([review?'warn':'good',review?`${review} imported finding(s) are marked for review. Open World Bible to edit anything you dislike.`:'Imported transcript was scanned and no review items remain.']);}
  health.push(['good','This dashboard belongs only to the selected RP. Other RPs cannot write into it.']);
  health.push(['good','Transcript remains intact and selected rerolls are the only canon assistant versions.']);
  health.push(['good','Major-event approval is part of the generation contract.']);
  health.push(['good',`Story clock: ${s.scene?.last_advance_reason||'waiting for first message'}`]);
  $('#healthChecks').innerHTML=health.map(([kind,text])=>`<div class="health-row"><span class="health-icon ${kind}">${kind==='good'?'●':'▲'}</span><span>${escapeHtml(text)}</span></div>`).join('');
}

function friendlyEmpty(text){return `<div class="empty-friendly">${escapeHtml(text)}</div>`}
function isLegacyPreRpEvent(x){return String((typeof x==='object'&&x?.type)||'').toLowerCase().includes('pre-rp')}
function bibleArrayForRead(world,key){
  const w=world||{}, current=Array.isArray(w[key])?w[key]:[];
  if(Number(w.schema_version||0)>=2)return current;
  if(current.length)return current;
  if(key==='history_backstory')return (w.timeline||[]).filter(isLegacyPreRpEvent);
  if(key==='story_timeline')return (w.timeline||[]).filter(x=>!isLegacyPreRpEvent(x));
  if(key==='groups_factions')return Array.isArray(w.organizations)?w.organizations:[];
  return current;
}
function normalizeWorldBibleForWrite(world){
  const w={...(world||{})};
  if(Number(w.schema_version||0)<2){
    if(!Array.isArray(w.history_backstory)||!w.history_backstory.length)w.history_backstory=[...bibleArrayForRead(w,'history_backstory')];
    if(!Array.isArray(w.story_timeline)||!w.story_timeline.length)w.story_timeline=[...bibleArrayForRead(w,'story_timeline')];
    if(!Array.isArray(w.groups_factions)||!w.groups_factions.length)w.groups_factions=[...bibleArrayForRead(w,'groups_factions')];
  }
  for(const key of ['history_backstory','background_facts','characters','relationship_history','core_canon','locations','secrets_knowledge','groups_factions','important_objects','world_rules','current_state','physical_continuity','story_timeline','custom_entries'])if(!Array.isArray(w[key]))w[key]=[];
  w.schema_version=Math.max(4,Number(w.schema_version||0));
  return w;
}
function renderWorldLists(){
  if(!state.bundle)return; const s=state.bundle.state; const w=s.world_bible||{};
  const facts=s.knowledge?.facts||[];
  $('#wbKnowledgeList').innerHTML=facts.length?facts.map((f,i)=>`<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(f.fact||f.title||'Fact')}</div><div class="friendly-item-sub">Known by: ${escapeHtml((f.known_by||[]).join(', ')||'not set')} · Hidden from: ${escapeHtml((f.hidden_from||[]).join(', ')||'nobody specified')}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="knowledge" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="knowledge" data-index="${i}">Delete</button></div></div></div>`).join(''):friendlyEmpty('No tracked secrets or knowledge yet. Add one when something matters.');
  const chars=w.characters||[];
  $('#wbCharacterList').innerHTML=chars.length?chars.map((x,i)=>{const traits=Array.isArray(x.personality)?x.personality:(x.personality?[String(x.personality)]:[]);const manner=Array.isArray(x.mannerisms)?x.mannerisms:[];const conf=x.personality_confidence!=null?Math.round(Number(x.personality_confidence)*100):null;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(x.name||x.title||'Character')}</div><div class="friendly-item-sub">${escapeHtml([x.role,x.age?`age ${x.age}`:'',x.job||x.rank].filter(Boolean).join(' · ')||x.summary||'No extra details yet.')}</div>${traits.length?`<div class="character-personality-preview">${traits.slice(0,6).map(t=>`<span class="personality-chip">${escapeHtml(t)}</span>`).join('')}</div>`:''}${x.speech_style?`<div class="friendly-item-sub">Voice: ${escapeHtml(x.speech_style)}</div>`:''}${conf!==null?`<div class="personality-confidence">Imported personality confidence: ${conf}% · ${Number(x.message_sample_size||0)} observed messages${manner.length?` · mannerisms: ${escapeHtml(manner.slice(0,3).join(', '))}`:''}</div>`:''}</div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="character" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="character" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No recurring people added to this RP Bible yet.');
  const locs=w.locations||[];
  $('#wbLocationList').innerHTML=locs.length?locs.map((x,i)=>{const obj=typeof x==='string'?{name:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(obj.name||obj.title||'Place')}</div><div class="friendly-item-sub">${escapeHtml(obj.description||obj.notes||'')}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="location" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="location" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No recurring places saved yet.');
  const timeline=bibleArrayForRead(w,'story_timeline');
  $('#wbTimelineList').innerHTML=timeline.length?timeline.map((x,i)=>{const obj=typeof x==='string'?{event:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(obj.event||obj.title||'Event')}</div><div class="friendly-item-sub">${escapeHtml([obj.date,obj.participants].filter(Boolean).join(' · '))}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="timeline" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="timeline" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No important timeline events saved yet.');
  const custom=w.custom_entries||[];
  $('#wbCustomList').innerHTML=custom.length?custom.map((x,i)=>`<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title"><span class="custom-entry-kind">${escapeHtml(x.category||'Other')}</span>${escapeHtml(x.label||x.title||'Custom entry')}</div><div class="friendly-item-sub">${escapeHtml(x.value||x.details||x.notes||'')}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="custom" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="custom" data-index="${i}">Delete</button></div></div></div>`).join(''):friendlyEmpty('Nothing extra saved yet. Use “Add anything” for pets, vehicles, jobs, objects, family facts, rules, preferences, or anything else.');
  const history=bibleArrayForRead(w,'history_backstory');
  $('#wbHistoryList').innerHTML=history.length?history.map((x,i)=>{const o=typeof x==='string'?{details:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(o.title||o.event||o.details||'Past event')}</div><div class="friendly-item-sub">${escapeHtml([o.when||o.date,o.where,o.people||o.participants,o.current_relevance].filter(Boolean).map(v=>Array.isArray(v)?v.join(', '):v).join(' · '))}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="history" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="history" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No pre-RP history yet. Add things like “they dated in high school” or “his father left when he was nine.”');
  const bg=w.background_facts||[];
  $('#wbBackgroundList').innerHTML=bg.length?bg.map((x,i)=>{const o=typeof x==='string'?{fact:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(o.fact||o.label||o.title||'Background fact')}</div><div class="friendly-item-sub">${escapeHtml(o.details||o.value||'')}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="background" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="background" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No background facts yet. Tiny facts are welcome here.');
  const groups=bibleArrayForRead(w,'groups_factions');
  $('#wbGroupList').innerHTML=groups.length?groups.map((x,i)=>{const o=typeof x==='string'?{name:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(o.name||o.title||'Group')}</div><div class="friendly-item-sub">${escapeHtml(o.details||o.description||o.role||'')}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="group" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="group" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No groups or factions saved yet.');
  const objects=w.important_objects||[];
  $('#wbObjectList').innerHTML=objects.length?objects.map((x,i)=>{const o=typeof x==='string'?{name:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(o.name||o.title||'Object')}</div><div class="friendly-item-sub">${escapeHtml([o.owner,o.location,o.details||o.description].filter(Boolean).join(' · '))}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="object" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="object" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No important objects saved yet.');
  const rules=w.world_rules||[];
  $('#wbRuleList').innerHTML=rules.length?rules.map((x,i)=>{const o=typeof x==='string'?{rule:x}:x;return `<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(o.rule||o.title||'World rule')}</div><div class="friendly-item-sub">${escapeHtml([o.details,o.importance].filter(Boolean).join(' · '))}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="rule" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="rule" data-index="${i}">Delete</button></div></div></div>`}).join(''):friendlyEmpty('No world rules saved yet.');
  const research=w.research||[];
  $('#wbResearchList').innerHTML=research.length?research.map((x,i)=>`<div class="friendly-item"><div class="friendly-item-head"><div><div class="friendly-item-title">${escapeHtml(x.title||'Web research')}</div><div class="friendly-item-sub">${escapeHtml(x.content||'')}${x.source?.label?`\nSource: ${escapeHtml(x.source.label)}`:''}</div></div><div class="friendly-item-actions"><button class="ghost small" data-world-edit="research" data-index="${i}">Edit</button><button class="tiny-btn" data-world-delete="research" data-index="${i}">Delete</button></div></div></div>`).join(''):friendlyEmpty('Nothing from the web has been saved to this RP yet. Search anything, edit it, then save only what you want.');
}

function renderWorldEditors(){
  if(!state.bundle)return; const s=state.bundle.state; const c=state.bundle.conversation; const h=s.hard_canon||{}, r=s.relationship||{}, sc=s.scene||{}, p=s.physical||{};
  $('#worldRpName').textContent=c.title; $('#worldScope').textContent=`RP: ${c.title} · Scenario: ${c.scenario_name} · Persona: ${c.persona_name}`;
  const imports=state.bundle.imports||[];const imp=$('#wbImportSummary');if(imp){imp.classList.toggle('hidden',!imports.length);if(imports.length){const w=s.world_bible||{},review=w.import_review||[],total=imports.reduce((a,x)=>a+Number(x.message_count||0),0);imp.innerHTML=`<div class="panel-title-row"><div><h3>⇲ Rebuilt from imported chat</h3><p class="muted compact">${total} imported messages were scanned to pre-fill this Bible and the Story Dashboard. Everything below is editable; your corrections outrank extraction.</p></div><div class="page-head-actions"><span class="status-pill ${review.length?'warn':'good'}">${review.length} to review</span><button class="ghost small" data-view-import="${imports[0].id}">View original</button></div></div>${review.length?`<div class="import-review-list">${review.slice(0,12).map((x,i)=>`<div class="import-review-item"><div><strong>${escapeHtml(x.kind||'Finding')}</strong><span>${escapeHtml(String(x.value||''))}</span><small>${escapeHtml(x.evidence||'')}</small></div><div class="import-review-actions"><span class="tag">${Math.round(Number(x.confidence||0)*100)}%</span><button class="ghost tiny" data-import-reviewed="${i}">✓ Reviewed</button></div></div>`).join('')}</div>`:'<div class="good-text">✓ The imported structure is ready. You can still edit anything you want.</div>'}`;}}
  const autoBox=$('#wbAutoContinuitySummary');if(autoBox){const w=s.world_bible||{},conf=w.auto_review_conflicts||[],core=(h.auto_facts||[]).filter(x=>x&&typeof x==='object'),sceneAuto=(sc.auto_facts||[]).filter(x=>x&&typeof x==='object'),physAuto=(p.continuity_notes||[]).filter(x=>x&&typeof x==='object'&&String(x.source||'').startsWith('auto-rp')),relAuto=r.auto_current&&typeof r.auto_current==='object'?r.auto_current:null;const autoCount=core.length+sceneAuto.length+physAuto.length+(relAuto?1:0);autoBox.classList.toggle('hidden',!autoCount&&!conf.length);if(autoCount||conf.length){const describe=x=>escapeHtml(String(x.fact||x.event||x.title||x.name||x.details||x.description||x.rule||x.status||JSON.stringify(x)).slice(0,500));autoBox.innerHTML=`<div class="panel-title-row"><div><h3>✨ Learned as the RP evolves</h3><p class="muted compact">MelumiyaChat can add new continuity automatically. User-authored Bible entries always outrank these detected facts.</p></div><span class="status-pill ${conf.length?'warn':'good'}">${autoCount} auto · ${conf.length} review</span></div>${core.length?`<div class="detail-box"><strong>Core canon detected</strong><div class="muted compact">${core.slice(-5).map(describe).join('<br>')}</div></div>`:''}${relAuto?`<div class="detail-box"><strong>Relationship change detected</strong><div class="muted compact">${describe(relAuto)}</div></div>`:''}${sceneAuto.length?`<div class="detail-box"><strong>Current-state facts detected</strong><div class="muted compact">${sceneAuto.slice(-5).map(describe).join('<br>')}</div></div>`:''}${physAuto.length?`<div class="detail-box"><strong>Physical continuity detected</strong><div class="muted compact">${physAuto.slice(-5).map(describe).join('<br>')}</div></div>`:''}${conf.length?`<div class="detail-box"><strong>Needs review — canon protected</strong><div class="muted compact">${conf.slice(-6).map(x=>escapeHtml(String(x.reason||x.message||'Automatic update conflicted with protected canon.'))).join('<br>')}</div></div>`:''}`;}}
  const statuses=['Strangers','Friends','Dating','Engaged','Married','Separated','Divorced','Exes','Enemies']; const rel=String(h.relationship_status||'');
  setSelect('#wbRelationshipStatus',statuses.includes(rel)?rel:'Custom','Custom'); $('#wbRelationshipCustom').value=statuses.includes(rel)?'':rel;
  setSelect('#wbCanonMode',h.source_canon_mode||c.canon_mode,'Selective Canon'); $('#wbRpYear').value=h.rp_year||new Date().getFullYear(); $('#wbCanonNotes').value=listText(h.notes||[]);
  $('#wbRelPhase').value=r.phase||''; setSelect('#wbTrust',r.trust,'Unknown'); setSelect('#wbAffection',r.affection,'Unknown'); $('#wbHurt').value=listText(r.unresolved_hurt||[]); $('#wbHabits').value=listText(r.habits||[]); $('#wbTriggers').value=listText(r.triggers||[]);
  $('#wbSceneDate').value=sc.date||sc.start_date||''; $('#wbSceneTime').value=sc.time||sc.start_time||''; $('#wbSceneLocation').value=sc.location||''; $('#wbControlled').value=sc.controlled_character||c.persona_name||''; $('#wbPresent').value=(sc.present||[]).join(', '); $('#wbOffscreen').value=(sc.offscreen||[]).join(', ');
  const wb=s.world_bible||{},ov=wb.overview||{}; $('#wbOverviewName').value=ov.name||wb.name||wb.title||c.title||''; $('#wbOverviewDescription').value=ov.description||wb.description||wb.summary||'';
  $('#wbPhysicalNotes').value=listText((p.continuity_notes||[]).filter(x=>!(x&&typeof x==='object'&&String(x.source||'').startsWith('auto-rp'))));
  renderWorldLists();
  $('#hardCanonEditor').value=pretty(s.hard_canon);$('#relationshipEditor').value=pretty(s.relationship);$('#knowledgeEditor').value=pretty(s.knowledge);$('#sceneEditor').value=pretty(s.scene);$('#physicalEditor').value=pretty(s.physical);$('#worldBibleEditor').value=pretty(s.world_bible);
}
async function saveDoc(type){
  const map={hard_canon:'#hardCanonEditor',relationship:'#relationshipEditor',knowledge:'#knowledgeEditor',scene:'#sceneEditor',physical:'#physicalEditor',world_bible:'#worldBibleEditor'};
  try{const payload=JSON.parse($(map[type]).value);await saveStateDoc(type,payload);toast('Saved to this RP.');}catch(e){alert(`Could not save: ${e.message}`)}
}
async function saveSimpleWorld(section){
  if(!state.bundle)return; const s=state.bundle.state;
  try{
    if(section==='canon'){
      const h={...(s.hard_canon||{})}; const choice=$('#wbRelationshipStatus').value; h.relationship_status=choice==='Custom'?($('#wbRelationshipCustom').value.trim()||'Custom'):choice;h.source_canon_mode=$('#wbCanonMode').value;h.rp_year=Number($('#wbRpYear').value)||h.rp_year;h.notes=lines($('#wbCanonNotes').value);await saveStateDoc('hard_canon',h);
      await api(`/api/conversations/${state.conversationId}`,{method:'PATCH',body:JSON.stringify({canon_mode:h.source_canon_mode})});
    } else if(section==='relationship'){
      const r={...(s.relationship||{})};r.phase=$('#wbRelPhase').value.trim();r.trust=$('#wbTrust').value;r.affection=$('#wbAffection').value;r.unresolved_hurt=lines($('#wbHurt').value);r.habits=lines($('#wbHabits').value);r.triggers=lines($('#wbTriggers').value);await saveStateDoc('relationship',r);
    } else if(section==='scene'){
      const sc={...(s.scene||{})};sc.date=$('#wbSceneDate').value||sc.date;sc.time=$('#wbSceneTime').value||sc.time;sc.location=$('#wbSceneLocation').value.trim()||'Unset';sc.controlled_character=$('#wbControlled').value.trim()||state.bundle.conversation.persona_name;sc.present=csv($('#wbPresent').value);sc.offscreen=csv($('#wbOffscreen').value);await saveStateDoc('scene',sc);
    } else if(section==='physical'){
      const p={...(s.physical||{})};const auto=(p.continuity_notes||[]).filter(x=>x&&typeof x==='object'&&String(x.source||'').startsWith('auto-rp'));p.continuity_notes=[...lines($('#wbPhysicalNotes').value),...auto];await saveStateDoc('physical',p);
    }
    await loadBundle(); renderWorldEditors(); toast('Saved to this RP.');
  }catch(e){alert(`Could not save: ${e.message}`)}
}

function editKnowledgeFact(index=-1){const k=state.bundle.state.knowledge||{};const f=index>=0?(k.facts||[])[index]||{}:{};openModal(index>=0?'Edit knowledge / secret':'Add knowledge / secret',`<div class="advanced-explain"><strong>World truth is separate from who knows it.</strong><span>Use commas for multiple people.</span></div><div class="field-with-action"><label>What is true?<textarea id="factText" rows="4">${escapeHtml(f.fact||'')}</textarea></label>${researchButton('Research this fact','bible_fact','factText',f.fact||'')}</div><label>Status<select id="factStatus"><option>confirmed</option><option>suspected</option><option>claimed</option><option>misunderstood</option><option>researched</option></select></label><label>Who knows this?<input id="factKnown" value="${escapeHtml((f.known_by||[]).join(', '))}" placeholder="Mariana, Keegan"></label><label>Who does NOT know / is it hidden from?<input id="factHidden" value="${escapeHtml((f.hidden_from||[]).join(', '))}" placeholder="Hesh, children"></label><label>How did they learn it?<input id="factLearned" value="${escapeHtml(f.how_learned||'')}" placeholder="told privately, witnessed, overheard…"></label><div class="modal-actions"><button id="saveFriendlyFact" class="primary">Save to this RP</button></div>`);setSelect('#factStatus',f.status||'confirmed','confirmed');bindCreationResearchButtons();$('#saveFriendlyFact').onclick=async()=>{const doc={...(state.bundle.state.knowledge||{})};const facts=[...(doc.facts||[])];const item={...f,source:'user',fact:$('#factText').value.trim(),status:$('#factStatus').value,known_by:csv($('#factKnown').value),hidden_from:csv($('#factHidden').value),how_learned:$('#factLearned').value.trim(),web_research:state.creationResearchDrafts.bible_fact||f.web_research||[]};if(index>=0)facts[index]=item;else facts.push(item);doc.facts=facts;await saveStateDoc('knowledge',doc);closeModal();renderWorldEditors();toast('Knowledge saved to this RP.');}}

function editWorldCharacter(index=-1){
  const w=state.bundle.state.world_bible||{};const x=index>=0?(w.characters||[])[index]||{}:{};
  const traits=Array.isArray(x.personality)?x.personality.join(', '):(x.personality||'');
  const mannerisms=Array.isArray(x.mannerisms)?x.mannerisms.join(', '):(x.mannerisms||'');
  const evidence=Array.isArray(x.personality_evidence)?x.personality_evidence:[];
  const confidence=x.personality_confidence!=null?Math.round(Number(x.personality_confidence)*100):null;
  openModal(index>=0?'Edit person':'Add person to this RP',`<div class="field-with-action"><label>Name<input id="wcName" value="${escapeHtml(x.name||x.title||'')}"></label>${researchButton('Search web for this person','bible_character','wcNotes',x.name||x.title||'')}</div><div class="form-row double"><label>Role / relationship<input id="wcRole" value="${escapeHtml(x.role||'')}" placeholder="teammate, child, friend…"></label><label>Age<input id="wcAge" value="${escapeHtml(x.age||'')}"></label></div><label>Job / rank<input id="wcJob" value="${escapeHtml(x.job||x.rank||'')}"></label>
    <div class="personality-edit-block"><h4>🧠 Personality & voice</h4><p class="muted compact">${confidence!==null?`MelumiyaChat reconstructed this from ${Number(x.message_sample_size||0)} imported messages (${confidence}% confidence). `:''}Edit anything that doesn't feel like them — your version wins.</p>
      <label>Personality traits<input id="wcPersonality" value="${escapeHtml(traits)}" placeholder="reserved, playful, protective, dry humor…"><span class="muted compact">Separate traits with commas.</span></label>
      <label>Speech style<textarea id="wcSpeech" rows="2" placeholder="How they usually speak…">${escapeHtml(x.speech_style||'')}</textarea></label>
      <div class="form-row double"><label>Affection style<textarea id="wcAffection" rows="2">${escapeHtml(x.affection_style||'')}</textarea></label><label>Conflict behavior<textarea id="wcConflict" rows="2">${escapeHtml(x.conflict_style||'')}</textarea></label></div>
      <label>Emotional expression<textarea id="wcEmotion" rows="2">${escapeHtml(x.emotional_expression||'')}</textarea></label>
      <label>Mannerisms<input id="wcMannerisms" value="${escapeHtml(mannerisms)}" placeholder="sighs, raises an eyebrow, rubs his neck…"></label>
      <label>Relationship-specific behavior<textarea id="wcRelationshipSpecific" rows="2">${escapeHtml(x.relationship_specific||'')}</textarea></label>
      ${evidence.length?`<details><summary>Why did the importer think this?</summary><div class="personality-evidence-list">${evidence.slice(0,14).map(ev=>`<div class="personality-evidence-row"><strong>${escapeHtml(ev.signal||'Observed behavior')}</strong> · ${Math.round(Number(ev.confidence||0)*100)}% · seen ${Number(ev.count||1)} time(s)<br>${escapeHtml(ev.excerpt||'')}</div>`).join('')}</div></details>`:''}
    </div>
    <label>Notes<textarea id="wcNotes" rows="5">${escapeHtml(x.notes||x.summary||'')}</textarea></label><div class="modal-actions"><button id="saveFriendlyCharacter" class="primary">Save person</button></div>`);
  bindCreationResearchButtons();$('#saveFriendlyCharacter').onclick=async()=>{const world={...(state.bundle.state.world_bible||{})};const arr=[...(world.characters||[])];const item={...x,source:'user',name:$('#wcName').value.trim(),role:$('#wcRole').value.trim(),age:$('#wcAge').value.trim(),job:$('#wcJob').value.trim(),notes:$('#wcNotes').value.trim(),personality:$('#wcPersonality').value.split(',').map(v=>v.trim()).filter(Boolean),speech_style:$('#wcSpeech').value.trim(),affection_style:$('#wcAffection').value.trim(),conflict_style:$('#wcConflict').value.trim(),emotional_expression:$('#wcEmotion').value.trim(),mannerisms:$('#wcMannerisms').value.split(',').map(v=>v.trim()).filter(Boolean),relationship_specific:$('#wcRelationshipSpecific').value.trim(),web_research:state.creationResearchDrafts.bible_character||x.web_research||[]};if(index>=0)arr[index]=item;else arr.push(item);world.characters=arr;await saveStateDoc('world_bible',world);closeModal();renderWorldEditors();toast('Person saved to this RP.');}
}
function editWorldLocation(index=-1){const w=state.bundle.state.world_bible||{};let x=index>=0?(w.locations||[])[index]||{}:{};if(typeof x==='string')x={name:x};openModal(index>=0?'Edit place':'Add place',`<div class="field-with-action"><label>Place name<input id="wlName" value="${escapeHtml(x.name||x.title||'')}"></label>${researchButton('Search web for this place','bible_location','wlDesc',x.name||x.title||'')}</div><label>What should MelumiyaChat remember about it?<textarea id="wlDesc" rows="6">${escapeHtml(x.description||x.notes||'')}</textarea></label><div class="modal-actions"><button id="saveFriendlyLocation" class="primary">Save place</button></div>`);bindCreationResearchButtons();$('#saveFriendlyLocation').onclick=async()=>{const world={...(state.bundle.state.world_bible||{})};const arr=[...(world.locations||[])];const item={...x,source:'user',name:$('#wlName').value.trim(),description:$('#wlDesc').value.trim(),web_research:state.creationResearchDrafts.bible_location||x.web_research||[]};if(index>=0)arr[index]=item;else arr.push(item);world.locations=arr;await saveStateDoc('world_bible',world);closeModal();renderWorldEditors();toast('Place saved to this RP.')}}
function editTimeline(index=-1){const w=normalizeWorldBibleForWrite(state.bundle.state.world_bible||{});let x=index>=0?(w.story_timeline[index]||{}):{};if(typeof x==='string')x={event:x};openModal(index>=0?'Edit timeline / history event':'Add timeline / history event',`<div class="field-with-action"><label>What happened?<textarea id="wtEvent" rows="4">${escapeHtml(x.event||x.title||'')}</textarea></label>${researchButton('Research this event / history','bible_timeline','wtEvent',x.event||x.title||'')}</div><label>Date / era<input id="wtDate" value="${escapeHtml(x.date||'')}" placeholder="2026-08-13, high school, three years ago…"></label><label>Who was involved?<input id="wtPeople" value="${escapeHtml(Array.isArray(x.participants)?x.participants.join(', '):(x.participants||''))}"></label><div class="modal-actions"><button id="saveFriendlyTimeline" class="primary">Save event</button></div>`);bindCreationResearchButtons();$('#saveFriendlyTimeline').onclick=async()=>{const world=normalizeWorldBibleForWrite(state.bundle.state.world_bible||{});const arr=[...(world.story_timeline||[])];const item={...x,source:'user',event:$('#wtEvent').value.trim(),date:$('#wtDate').value.trim(),participants:csv($('#wtPeople').value),web_research:state.creationResearchDrafts.bible_timeline||x.web_research||[]};if(index>=0)arr[index]=item;else arr.push(item);world.story_timeline=arr;await saveStateDoc('world_bible',world);closeModal();renderWorldEditors();toast('Timeline / history event saved.')}}
function editCustomEntry(index=-1){
  const w=state.bundle.state.world_bible||{};
  const x=index>=0?(w.custom_entries||[])[index]||{}:{};
  openModal(index>=0?'Edit custom Bible entry':'Add anything to this RP Bible',`<div class="advanced-explain"><strong>No special category needed.</strong><span>Use this for anything the normal sections do not cover: pet, vehicle, job, school, object, family fact, rule, preference, tradition, medical detail, hobby, or your own category.</span></div><label>Type / category<select id="customCategory"><option>Family</option><option>Pet</option><option>Vehicle</option><option>Job / School</option><option>Object</option><option>Organization</option><option>Rule</option><option>Preference</option><option>Tradition</option><option>Health / Physical</option><option>Other</option><option>Custom</option></select></label><label id="customCategoryWrap" class="hidden">Your category<input id="customCategoryText" placeholder="e.g. Motorcycle, Holiday tradition, House rule"></label><label>What is it called?<input id="customLabel" value="${escapeHtml(x.label||x.title||'')}" placeholder="e.g. Keegan's motorcycle"></label><label>What should MelumiyaChat remember?<textarea id="customValue" rows="6" placeholder="Write it normally. No JSON needed.">${escapeHtml(x.value||x.details||x.notes||'')}</textarea></label><div class="modal-actions"><button id="saveCustomEntry" class="primary">Save to this RP</button></div>`);
  const known=['Family','Pet','Vehicle','Job / School','Object','Organization','Rule','Preference','Tradition','Health / Physical','Other'];
  const cat=x.category||'Other';
  setSelect('#customCategory',known.includes(cat)?cat:'Custom','Other');
  $('#customCategoryText').value=known.includes(cat)?'':cat;
  const sync=()=>$('#customCategoryWrap').classList.toggle('hidden',$('#customCategory').value!=='Custom');
  $('#customCategory').onchange=sync;sync();
  $('#saveCustomEntry').onclick=async()=>{
    const world={...(state.bundle.state.world_bible||{})};
    const arr=[...(world.custom_entries||[])];
    const category=$('#customCategory').value==='Custom'?($('#customCategoryText').value.trim()||'Other'):$('#customCategory').value;
    const item={...x,source:'user',category,label:$('#customLabel').value.trim(),value:$('#customValue').value.trim()};
    if(index>=0)arr[index]=item;else arr.push(item);
    world.custom_entries=arr;
    await saveStateDoc('world_bible',world);closeModal();renderWorldEditors();toast('Saved to this RP Bible.');
  };
}
function editStructuredBibleEntry(kind,index=-1){
  const world=normalizeWorldBibleForWrite(state.bundle.state.world_bible||{});
  const configs={
    history:{key:'history_backstory',title:'History & Backstory',singular:'past event',fields:[['title','What happened?','textarea'],['when','When / age / era','input'],['where','Where','input'],['people','Who was involved?','input'],['known_by','Who knows about it?','input'],['current_relevance','Why it still matters now','textarea'],['importance','Canon importance','select']]},
    background:{key:'background_facts',title:'Background Fact',singular:'background fact',fields:[['fact','Fact','textarea'],['details','Optional context','textarea']]},
    group:{key:'groups_factions',title:'Group / Faction',singular:'group',fields:[['name','Name','input'],['role','Role / type','input'],['details','Details','textarea']]},
    object:{key:'important_objects',title:'Important Object',singular:'object',fields:[['name','Name','input'],['owner','Owner / who has it','input'],['location','Current location','input'],['details','Why it matters','textarea']]},
    rule:{key:'world_rules',title:'World Rule',singular:'rule',fields:[['rule','Rule','textarea'],['details','Explanation / limits','textarea'],['importance','Canon importance','select']]},
    relationship_history:{key:'relationship_history',title:'Relationship History',singular:'relationship history',fields:[['title','What happened between them?','textarea'],['when','When','input'],['people','Who was involved?','input'],['details','Details / current effect','textarea']]}
  };
  const c=configs[kind];if(!c)return;const arr=[...(world[c.key]||[])];let x=index>=0?(arr[index]||{}):{};if(typeof x==='string')x={fact:x,details:x};
  const fieldHtml=c.fields.map(([key,label,type])=>{const val=Array.isArray(x[key])?x[key].join(', '):(x[key]||'');if(type==='textarea')return `<label>${escapeHtml(label)}<textarea id="wse_${key}" rows="${key==='title'||key==='fact'||key==='rule'?4:3}">${escapeHtml(val)}</textarea></label>`;if(type==='select')return `<label>${escapeHtml(label)}<select id="wse_${key}"><option>Normal</option><option>Important</option><option>Critical / hard canon</option></select></label>`;return `<label>${escapeHtml(label)}<input id="wse_${key}" value="${escapeHtml(val)}"></label>`}).join('');
  openModal(index>=0?`Edit ${c.singular}`:`Add ${c.singular}`,`<div class="advanced-explain"><strong>Keep it as short or detailed as you want.</strong><span>Only the fields that matter need to be filled.</span></div>${fieldHtml}<div class="modal-actions"><button id="saveStructuredBibleEntry" class="primary">Save to this RP</button></div>`);
  if(c.fields.some(f=>f[2]==='select'))setSelect('#wse_importance',x.importance||'Normal','Normal');
  $('#saveStructuredBibleEntry').onclick=async()=>{const item={...x,source:'user',updated_at:new Date().toISOString()};for(const [key,label,type] of c.fields){const v=$(`#wse_${key}`)?.value?.trim()||'';item[key]=['people','known_by'].includes(key)?csv(v):v}if(index>=0)arr[index]=item;else arr.push(item);const w=normalizeWorldBibleForWrite(state.bundle.state.world_bible||{});w[c.key]=arr;await saveStateDoc('world_bible',w);closeModal();renderWorldEditors();toast(`${c.title} saved.`)};
}
async function saveWorldOverview(){const w={...(state.bundle.state.world_bible||{})};w.overview={...(w.overview||{}),name:$('#wbOverviewName').value.trim(),description:$('#wbOverviewDescription').value.trim(),updated_at:new Date().toISOString()};await saveStateDoc('world_bible',w);await loadBundle();renderWorldEditors();toast('World overview saved.');}
function openBibleAddMenu(){
  openModal('What do you want to add?',`<div class="advanced-explain"><strong>Add it in normal words.</strong><span>Choose the closest type. You can edit or delete it later.</span></div><div class="add-choice-grid"><button class="add-choice" data-bible-add-kind="history"><strong>🕰️ Past / Backstory</strong><span>Something that happened before this RP began.</span></button><button class="add-choice" data-bible-add-kind="background"><strong>📎 Background fact</strong><span>A small persistent fact such as where someone was raised.</span></button><button class="add-choice" data-bible-add-kind="knowledge"><strong>🤫 Fact or secret</strong><span>Something true, suspected, hidden, or known only by certain people.</span></button><button class="add-choice" data-bible-add-kind="character"><strong>👥 Person</strong><span>Family, teammate, friend, child, coworker, recurring NPC.</span></button><button class="add-choice" data-bible-add-kind="location"><strong>🏠 Place</strong><span>Home, room, base, school, café, city, workplace.</span></button><button class="add-choice" data-bible-add-kind="timeline"><strong>🗓️ Event</strong><span>Wedding, argument, reunion, birthday, deployment, milestone.</span></button><button class="add-choice" data-bible-add-kind="group"><strong>🏛️ Group / faction</strong><span>Family, squad, company, kingdom, gang or organization.</span></button><button class="add-choice" data-bible-add-kind="object"><strong>🗝️ Important object</strong><span>Possession, weapon, document, heirloom or tracked item.</span></button><button class="add-choice" data-bible-add-kind="rule"><strong>⚖️ World rule</strong><span>A hard rule, law, system limit or tradition.</span></button><button class="add-choice" data-bible-add-kind="custom"><strong>✨ Anything else</strong><span>Pet, vehicle, object, job, rule, preference, tradition, or your own category.</span></button><button class="add-choice" data-bible-add-kind="web"><strong>🌐 Research from web</strong><span>Search first, edit the result, then save only what you approve.</span></button></div>`);
  $$('[data-bible-add-kind]').forEach(b=>b.onclick=()=>{const kind=b.dataset.bibleAddKind;closeModal();if(kind==='history'||kind==='background'||kind==='group'||kind==='object'||kind==='rule')editStructuredBibleEntry(kind,-1);if(kind==='knowledge')editKnowledgeFact(-1);if(kind==='character')editWorldCharacter(-1);if(kind==='location')editWorldLocation(-1);if(kind==='timeline')editTimeline(-1);if(kind==='custom')editCustomEntry(-1);if(kind==='web')openWebSearch('bible');});
}
function editResearch(index=-1,source=null){const w=state.bundle.state.world_bible||{};const existing=index>=0?(w.research||[])[index]||{}:{};const x=source?{title:source.title||'',content:source.summary||'',category:'Research',source:{label:source.source_label||source.domain||'Web',url:source.url||''},imported_from_web:true,editable:true}:existing;openModal(index>=0?'Edit saved web research':'Edit before saving to World Bible',`<div class="advanced-explain"><strong>You are in control.</strong><span>Change or delete anything from the search result before it becomes part of this RP Bible.</span></div><div class="bible-draft-source">Source: ${escapeHtml(x.source?.label||'Web')} ${x.source?.url?`· ${escapeHtml(x.source.url)}`:''}</div><label>Title<input id="wrTitle" value="${escapeHtml(x.title||'')}"></label><label>Category<input id="wrCategory" value="${escapeHtml(x.category||'Research')}" placeholder="Song, event, place, history…"></label><label>What should this RP remember?<textarea id="wrContent" rows="7">${escapeHtml(x.content||'')}</textarea></label><label>Your notes<textarea id="wrNotes" rows="4">${escapeHtml(x.notes||'')}</textarea></label><div class="modal-actions"><button id="saveResearchDraft" class="primary">Save edited version to this RP</button></div>`);$('#saveResearchDraft').onclick=async()=>{if(index>=0){const world={...(state.bundle.state.world_bible||{})};const arr=[...(world.research||[])];arr[index]={...x,title:$('#wrTitle').value.trim(),category:$('#wrCategory').value.trim(),content:$('#wrContent').value.trim(),notes:$('#wrNotes').value.trim()};world.research=arr;await saveStateDoc('world_bible',world);}else{await api('/api/web/save-to-world',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,title:$('#wrTitle').value,category:$('#wrCategory').value,content:$('#wrContent').value,notes:$('#wrNotes').value,url:x.source?.url||'',source_label:x.source?.label||'Web'})});await loadBundle();}closeModal();renderWorldEditors();toast('Edited web research saved to this RP.')}}
async function deleteWorldItem(kind,index){if(!confirm('Delete this item from this RP Bible?'))return; if(kind==='knowledge'){const d={...(state.bundle.state.knowledge||{})};const a=[...(d.facts||[])];a.splice(index,1);d.facts=a;await saveStateDoc('knowledge',d);}else{const map={character:'characters',location:'locations',timeline:'story_timeline',history:'history_backstory',background:'background_facts',group:'groups_factions',object:'important_objects',rule:'world_rules',relationship_history:'relationship_history',research:'research',custom:'custom_entries'};const d=normalizeWorldBibleForWrite(state.bundle.state.world_bible||{});const key=map[kind];const a=[...(d[key]||[])];a.splice(index,1);d[key]=a;await saveStateDoc('world_bible',d);}renderWorldEditors();toast('Removed from this RP.');}

function webResultCard(x,i,origin){return `<div class="web-result"><h4>${escapeHtml(x.title||'Result')}</h4><p>${escapeHtml(x.summary||'')}</p><div class="web-result-meta"><span class="tag">${escapeHtml(x.source_label||x.domain||'Web')}</span><span class="tag">${escapeHtml(x.authority||'review')}</span></div>${x.url?`<a href="${escapeHtml(x.url)}" target="_blank" rel="noopener">Open source ↗</a>`:''}<div class="web-result-actions"><button class="ghost small" data-web-player="${i}">Just show me</button><button class="secondary small" data-web-director="${i}">Use next reply only</button><button class="secondary small" data-web-use="${i}">Save as character knowledge</button><button class="primary small" data-web-save="${i}">Edit & save to World</button></div></div>`}
function openWebSearch(origin='rp'){openModal(origin==='bible'?'Research for this RP Bible':'Search the web during this RP',`<div class="web-search-intro"><strong>Nothing is canon just because the internet said it.</strong><br>“Just show me” stays player-only. “Use in RP” lets your controlled character know it. “Edit & save to World” lets you rewrite it before saving it to this RP Bible.</div><div class="web-search-bar"><label>What do you want to find?<input id="webSearchQuery" placeholder="song released in 2014, real event, place, movie…"></label><button id="runWebSearch" class="primary">Search</button></div><div id="webSearchMode" class="web-mode-note"></div><div id="webSearchResults" class="web-result-list"></div>`);$('#runWebSearch').onclick=()=>runWebSearch(origin);$('#webSearchQuery').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();runWebSearch(origin)}});setTimeout(()=>$('#webSearchQuery')?.focus(),50)}
async function runWebSearch(origin='rp'){const q=$('#webSearchQuery').value.trim();if(!q)return;$('#runWebSearch').disabled=true;$('#webSearchResults').innerHTML='<div class="canon-searching">Searching…</div>';try{const r=await api(`/api/web/search?q=${encodeURIComponent(q)}`);state.lastWebResults=r.results||[];$('#webSearchMode').textContent=`Search source: ${r.mode}. ${r.broad_web_enabled?'Broad web search is connected.':'Free Wikipedia/Wikidata search is active; broader web search can be connected later.'}`;$('#webSearchResults').innerHTML=state.lastWebResults.length?state.lastWebResults.map((x,i)=>webResultCard(x,i,origin)).join(''):friendlyEmpty('No results found. Try different words.');$$('[data-web-player]').forEach(b=>b.onclick=()=>toast('Player-only: nothing was saved and no character learned it.'));$$('[data-web-director]').forEach(b=>b.onclick=()=>useWebResultForNextReply(Number(b.dataset.webDirector)));$$('[data-web-use]').forEach(b=>b.onclick=()=>useWebResult(Number(b.dataset.webUse)));$$('[data-web-save]').forEach(b=>b.onclick=()=>editResearch(-1,state.lastWebResults[Number(b.dataset.webSave)]));}catch(e){$('#webSearchResults').innerHTML=`<div class="canon-warning">${escapeHtml(e.message)}</div>`}finally{$('#runWebSearch').disabled=false}}
async function useWebResult(index){const x=state.lastWebResults?.[index];if(!x)return;const controlled=state.bundle?.state?.scene?.controlled_character||state.bundle?.conversation?.persona_name||'User character';await api('/api/web/use-in-rp',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,title:x.title,summary:x.summary,url:x.url,source_label:x.source_label||x.domain||'Web',known_by:[controlled]})});await loadBundle();toast(`${controlled} can now know this. Other characters do not automatically know it.`)}
async function useWebResultForNextReply(index){const x=state.lastWebResults?.[index];if(!x)return;closeModal();const research=[x.title,x.summary,x.url?`Source: ${x.url}`:''].filter(Boolean).join(' — ');await runDirectorCommand(`Use this web research as temporary factual context for the next reply only. Do not claim more than the source says and do not save it as canon unless the RP already establishes it. Research: ${research}`)}


function rpCharacterIdentity(){
  const b=state.bundle||{}, c=b.conversation||{}, st=b.state||{}, hc=st.hard_canon||{}, wb=st.world_bible||{};
  const chars=Array.isArray(wb.characters)?wb.characters:[];
  const base=String(hc.base_character||hc.character_name||c.character_name||'').trim();
  const person=chars.find(x=>String(x?.name||'').toLowerCase()===base.toLowerCase())||chars[0]||{};
  const payload=hc.scenario_payload||{};
  const name=base||person.name||c.scenario_name||c.title||'Character';
  const image=person.image_url||person.avatar_url||person.photo||payload.character_image_url||payload.character_photo||payload.image_url||'';
  return {name,image};
}
function renderRpImmersionHeader(){
  const nameEl=$('#rpCharacterName'), photo=$('#rpCharacterPhoto'), initialsEl=$('#rpCharacterInitials'); if(!nameEl||!photo||!initialsEl)return;
  const x=rpCharacterIdentity(); nameEl.textContent=x.name;
  const bg=String(x.image||'').replace(/[\"\'\n\r]/g,''); document.body.style.setProperty('--rp-character-bg',bg?`url(${bg})`:'none');
  const ini=String(x.name||'?').split(/\s+/).filter(Boolean).slice(0,2).map(v=>v[0]?.toUpperCase()||'').join('')||'?'; initialsEl.textContent=ini;
  if(x.image){photo.src=x.image;photo.classList.remove('hidden');initialsEl.classList.add('hidden');photo.onerror=()=>{photo.classList.add('hidden');initialsEl.classList.remove('hidden')}}else{photo.removeAttribute('src');photo.classList.add('hidden');initialsEl.classList.remove('hidden')}
}
// v0.9.3 cinematic RP polish
const RP_LENGTH_DEFS={
  'Short':'1–2 compact paragraphs. Best for quick back-and-forth.',
  'Medium':'2–4 paragraphs. Dialogue, reaction and enough scene detail to feel complete.',
  'Long':'4–7 paragraphs. Fuller scene beats, emotion, movement and atmosphere.',
  'Very Long':'7+ paragraphs when the scene supports it. Maximum detail without deliberate filler.'
};
function openRpMemories(){
  const memories=state.bundle?.memories||[];
  openModal('Memories',`<div class="advanced-explain"><strong>Live continuity for this RP.</strong><span>MelumiyaChat can detect meaningful new memories as the RP evolves. Who Remembers keeps knowledge character-specific; old states can become resolved or superseded instead of being erased.</span></div><div id="rpMemoryModalList" style="display:grid;gap:8px;max-height:52vh;overflow:auto;margin:12px 0">${memories.length?memories.map(m=>memoryCardHtml(m)).join(''):'<div class="empty-state" style="min-height:100px">No saved memories yet. Meaningful RP moments will be detected automatically.</div>'}</div><div class="modal-actions"><button id="rpAddMemoryNow" class="primary">+ Add Memory</button><button id="rpOpenDashboardNow" class="secondary">Story Dashboard</button></div>`);
  $('#rpAddMemoryNow').onclick=()=>openMemoryEditor();
  $('#rpOpenDashboardNow').onclick=()=>{closeModal();showView('dashboard')};
  wireMemoryActions($('#rpMemoryModalList'));
}
function openRpImmersionMenu(){
  const c=state.bundle?.conversation||{}, access=state.modelAccess||{}, usage=access.usage||{}, currentModel=$('#modelChoiceSelect')?.value||'';
  const models=access.models||[];
  const length=c.reply_length||'Long';
  openModal('RP options',`<div class="rp-immersion-menu">
    <div class="rp-menu-section"><h4>Story</h4><div class="rp-menu-actions"><button id="rpMenuContinue" class="secondary">Continue scene</button><button id="rpMenuCommands" class="secondary">RP commands</button><button id="rpMenuMemories" class="secondary">🧠 Memories</button><button id="rpMenuBible" class="secondary">📖 World Bible</button><button id="rpMenuAddBible" class="ghost">+ Add to World Bible</button><button id="rpMenuScene" class="ghost">Scene settings</button><button id="rpMenuWeb" class="ghost">Search the web</button><button id="rpMenuRefresh" class="ghost">Refresh RP</button></div></div>
    <div class="rp-menu-section"><h4>Response length</h4><select id="rpMenuLength">${Object.keys(RP_LENGTH_DEFS).map(x=>`<option ${x===length?'selected':''}>${x}</option>`).join('')}</select><div class="rp-length-definitions">${Object.entries(RP_LENGTH_DEFS).map(([k,v])=>`<div class="rp-length-definition"><strong>${k}</strong><span>${v}</span></div>`).join('')}</div></div>
    <div class="rp-menu-section"><h4>Model & plan</h4><select id="rpMenuModel"><option value="">Plan default</option>${models.map(m=>`<option value="${escapeHtml(m.key)}" ${m.key===currentModel?'selected':''}>${escapeHtml(m.label)}${m.minimum_tier?` · ${escapeHtml(m.minimum_tier)}`:''}</option>`).join('')}</select><div class="rp-menu-plan-line"><span>Current access</span><strong>${escapeHtml(access.plan_tier||'Free')}${usage.total_remaining!=null?` · ${escapeHtml(String(usage.total_remaining))} replies left today`:''}</strong></div><button id="rpMenuPlans" class="ghost full">View plans</button></div>
  </div>`);
  $('#rpMenuContinue').onclick=()=>{closeModal();continueScene()};
  $('#rpMenuCommands').onclick=()=>{closeModal();$('#composerMenu').classList.remove('hidden')};
  $('#rpMenuMemories').onclick=openRpMemories;
  $('#rpMenuBible').onclick=()=>{closeModal();showView('world')};
  $('#rpMenuAddBible').onclick=()=>{closeModal();openBibleAddMenu()};
  $('#rpMenuScene').onclick=()=>{closeModal();openSceneSettings()};
  $('#rpMenuWeb').onclick=()=>{closeModal();openWebSearch('rp')};
  $('#rpMenuRefresh').onclick=async()=>{closeModal();await loadBundle();toast('RP refreshed')};
  $('#rpMenuPlans').onclick=()=>{closeModal();showView('plans')};
  $('#rpMenuModel').onchange=e=>{const real=$('#modelChoiceSelect');if(real){real.value=e.target.value;renderCustomerModelAccess()}};
  $('#rpMenuLength').onchange=async e=>{const value=e.target.value;await api(`/api/conversations/${state.conversationId}`,{method:'PATCH',body:JSON.stringify({reply_length:value})});await loadBundle();toast(`Response length: ${value}`);openRpImmersionMenu()};
}
function openSceneSettings(){ document.body.classList.add('rp-settings-open'); $('#sceneSettingsPanel').classList.add('open'); $('#sceneSettingsBackdrop').classList.remove('hidden'); }
function closeSceneSettings(){ document.body.classList.remove('rp-settings-open'); $('#sceneSettingsPanel').classList.remove('open'); $('#sceneSettingsBackdrop').classList.add('hidden'); }
function openModal(title,body){$('#modalTitle').textContent=title;$('#modalBody').innerHTML=body;$('#modal').classList.remove('hidden');}
function closeModal(){$('#modal').classList.add('hidden');}

async function viewOriginalImport(importId){
  try{const x=await api(`/api/imports/${importId}`);openModal(`Imported chat · ${x.source_platform}`,`<div class="import-intro"><strong>Original source evidence</strong><span>${escapeHtml(x.source_filename||x.detected_format||'Pasted transcript')} · ${x.message_count} messages. This is preserved separately from the editable World Bible.</span></div><textarea class="import-source-view" rows="22" readonly>${escapeHtml(x.raw_text||'')}</textarea>`);}catch(e){alert(e.message)}
}
async function markImportReviewed(index){
  const world={...(state.bundle.state.world_bible||{})};const review=[...(world.import_review||[])];review.splice(index,1);world.import_review=review;await saveStateDoc('world_bible',world);renderWorldEditors();renderDashboard();toast('Marked reviewed. Your edited Bible remains the truth for this RP.');
}

function bindActions(){
  const rpMore=$('#rpMoreBtn'); if(rpMore)rpMore.onclick=openRpImmersionMenu; const rpExit=$('#rpExitMenuBtn');if(rpExit)rpExit.onclick=()=>showView('home');
  const rpToggle=$('#currentRpNavToggle'); if(rpToggle)rpToggle.onclick=toggleCurrentRpNav;
  $('#sendBtn').onclick=sendMessage; $('#continueBtn').onclick=continueScene; $('#messageInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.ctrlKey||e.metaKey)){e.preventDefault();sendMessage();}});
  $('#commandPaletteBtn').onclick=()=>$('#composerMenu').classList.toggle('hidden');
  $$('[data-director-command]').forEach(b=>b.onclick=()=>runDirectorCommand(b.dataset.directorCommand));
  $$('[data-director-custom]').forEach(b=>b.onclick=()=>{$('#composerMenu').classList.add('hidden');openCustomDirectorCommand()});
  $$('[data-composer-action]').forEach(b=>b.onclick=()=>{const action=b.dataset.composerAction;$('#composerMenu').classList.add('hidden');if(action==='web')openWebSearch('rp');if(action==='dashboard')showView('dashboard');if(action==='world')showView('world');});
  $('#saveChatSettings').onclick=async()=>{
    const body={persona_name:$('#personaName').value,scenario_name:$('#scenarioName').value,canon_mode:$('#canonMode').value,timeline_mode:$('#timelineMode').value,reply_length:$('#replyLength').value,initiative:$('#initiative').value,relationship_realism:$('#relationshipRealism').value};
    await api(`/api/conversations/${state.conversationId}`,{method:'PATCH',body:JSON.stringify(body)});
    const scene={...(state.bundle.state.scene||{})};
    if(!scene.story_started){
      scene.start_date=$('#storyStartDate').value||scene.start_date||localDate(); scene.start_time=$('#storyStartTime').value||scene.start_time||localTime();
      scene.date=scene.start_date; scene.time=scene.start_time;
      await api('/api/state',{method:'POST',body:JSON.stringify({conversation_id:state.conversationId,doc_type:'scene',payload:scene})});
    }
    await loadBundle(); closeSceneSettings();
  };
  $('#newConversationBtn').onclick=()=>showView('discover');
  $('#publicNewScenarioBtn').onclick=()=>openCreatorForm(null,'scenario'); $('#creatorScenarioCta').onclick=()=>openCreatorForm(null,'scenario');
  $('#publicNewCharacterBtn').onclick=()=>openCreatorForm(null,'character'); $('#creatorCharacterCta').onclick=()=>openCreatorForm(null,'character');
  $('#refreshCreatorContent').onclick=loadCreatorContent;
  $('#browseScenariosBtn').onclick=()=>showView('discover');
  if($('#importChatBtn')) $('#importChatBtn').onclick=openChatImport;
  if($('#importChatHeroBtn')) $('#importChatHeroBtn').onclick=openChatImport;
  $('#discoverRefresh').onclick=loadDiscover;
  const tagToggle=$('#discoverTagsToggle'); if(tagToggle)tagToggle.onclick=()=>{state.tagsExpanded=!state.tagsExpanded;updateDiscoverTagsToggle();};
  $('#scenarioSearch').addEventListener('input',renderDiscover); $('#creatorSearch').addEventListener('input',renderDiscover); $('#discoverRating').onchange=renderDiscover; $('#discoverCanon').onchange=renderDiscover;
  $('#clearScenarioFilters').onclick=()=>{state.selectedTags.clear();$('#scenarioSearch').value='';$('#creatorSearch').value='';$('#discoverRating').value='';$('#discoverCanon').value='';renderDiscoverTagChips();renderDiscover();};
  document.addEventListener('click',async e=>{if(e.target?.id==='confirmStartScenario')await startScenario(e.target.dataset.id);});
  document.addEventListener('click',async e=>{if(e.target?.id==='savePublicCreation')await savePublicCreation(e.target.dataset.type,e.target.dataset.id||null);});
  $('#homeRefresh').onclick=async()=>{state.home=await api('/api/home');renderHome();}; $('#homeBrowseAll').onclick=()=>showView('discover'); $('#homeAllRps').onclick=()=>showView('rps'); $('#homeSeeAllRps').onclick=()=>showView('rps'); $('#homeGreeting').onclick=()=>showView('profile'); $('#homeCreateCharacterWin').onclick=()=>openCreatorForm(null,'character'); $('#homeCreateScenarioWin').onclick=()=>openCreatorForm(null,'scenario'); $('#homeCreatePersonaWin').onclick=()=>openPersonaForm(); $('#homeCreateRpWin').onclick=()=>showView('discover');
  $('#refreshFavorites').onclick=async()=>{state.favorites=await api('/api/favorites');state.favoriteIds=new Set(state.favorites.map(x=>String(x.id)));renderFavorites();};
  $('#openMyCreations').onclick=()=>showView('creations'); $('#creationsNewCharacter').onclick=()=>openCreatorForm(null,'character'); $('#creationsNewScenario').onclick=()=>openCreatorForm(null,'scenario');
  $('#newPersonaBtn').onclick=()=>openPersonaForm(); $('#editProfileBtn').onclick=openProfileEditor; if($('#signOutBtn'))$('#signOutBtn').onclick=()=>MelumiyaAuth.logout();
  $('#explorePlansBtn').onclick=()=>showView('plans'); $('#topbarPlansBtn').onclick=()=>showView('plans'); if($('#notificationBell'))$('#notificationBell').onclick=openNotifications;
  if($('#applyPlanPromo')) $('#applyPlanPromo').onclick=async()=>{const n=$('#planPromoNotice');try{await applyPlanPromoCode($('#planPromoCode').value);if(n&&state.appliedPromo?.kind!=='tier_reward'){n.className='promo-notice good';n.textContent=state.appliedPromo?.message||'Code applied.';}}catch(e){if(n){n.className='promo-notice bad';n.textContent=e.message;}}};
  document.addEventListener('click',e=>{if(e.target?.closest?.('[data-go-plans]'))showView('plans');});
  $('#sendHelpTicket').onclick=async()=>{try{const d=await api('/api/help/tickets',{method:'POST',body:JSON.stringify({category:$('#helpCategory').value,subject:$('#helpSubject').value,body:$('#helpBody').value})});$('#helpNotice').textContent=`Sent${d.case?.reference?' · '+d.case.reference:''}. Your help request is now in the private support queue.`;$('#helpSubject').value='';$('#helpBody').value='';await loadHelpTickets();if(d.case?.id)await openHelpTicket(d.case.id);}catch(e){$('#helpNotice').textContent=e.message;}}; if($('#refreshHelpTickets'))$('#refreshHelpTickets').onclick=loadHelpTickets;
  document.addEventListener('click',async e=>{if(e.target?.id==='savePersonaForm')await savePersonaForm(e.target.dataset.id||null);if(e.target?.id==='saveProfileForm'){await api('/api/profile/me',{method:'PATCH',body:JSON.stringify({display_name:$('#profileName').value,bio:$('#profileBio').value,location:$('#profileLocation').value,avatar_url:$('#profileAvatar').value,links:$('#profileLinks').value.split('\n').map(x=>x.trim()).filter(Boolean)})});closeModal();await loadPlatformShell();showView('profile');}});
  $('#mobileSceneBtn').onclick=openSceneSettings; $('#closeSceneSettings').onclick=closeSceneSettings; $('#sceneSettingsBackdrop').onclick=closeSceneSettings;
  $('#modalClose').onclick=closeModal; $('#modal').addEventListener('click',e=>{if(e.target.id==='modal')closeModal();});
  $$('[data-save-doc]').forEach(b=>b.onclick=()=>saveDoc(b.dataset.saveDoc));
  $$('[data-save-simple]').forEach(b=>b.onclick=()=>saveSimpleWorld(b.dataset.saveSimple));
  $('#refreshDashboard').onclick=async()=>{await loadBundle();renderDashboard();};
  $('#dashboardOpenBible').onclick=()=>showView('world');
  $('#worldAddAnything').onclick=openBibleAddMenu;
  $('#worldResearchBtn').onclick=()=>openWebSearch('bible'); $('#wbResearchInline').onclick=()=>openWebSearch('bible');
  $('#toggleAdvancedWorld').onclick=()=>{const adv=$('#worldAdvancedMode'),simple=$('#worldSimpleMode');const opening=adv.classList.contains('hidden');adv.classList.toggle('hidden',!opening);simple.classList.toggle('hidden',opening);$('#toggleAdvancedWorld').textContent=opening?'← Back to Simple editor':'Advanced JSON';if(opening)renderWorldEditors();};
  $('#wbSaveOverview').onclick=saveWorldOverview; $('#wbAddHistory').onclick=()=>editStructuredBibleEntry('history',-1); $('#wbAddBackgroundFact').onclick=()=>editStructuredBibleEntry('background',-1); $('#wbAddFact').onclick=()=>editKnowledgeFact(-1); $('#wbAddCharacter').onclick=()=>editWorldCharacter(-1); $('#wbAddLocation').onclick=()=>editWorldLocation(-1); $('#wbAddGroup').onclick=()=>editStructuredBibleEntry('group',-1); $('#wbAddObject').onclick=()=>editStructuredBibleEntry('object',-1); $('#wbAddRule').onclick=()=>editStructuredBibleEntry('rule',-1); $('#wbAddTimeline').onclick=()=>editTimeline(-1); $('#wbAddCustom').onclick=()=>editCustomEntry(-1);
  $('#addMemoryBtn').onclick=()=>openMemoryEditor();
  document.addEventListener('click',async e=>{const edit=e.target?.closest?.('[data-world-edit]');if(edit){const kind=edit.dataset.worldEdit,idx=Number(edit.dataset.index);if(['history','background','group','object','rule','relationship_history'].includes(kind))editStructuredBibleEntry(kind,idx);if(kind==='knowledge')editKnowledgeFact(idx);if(kind==='character')editWorldCharacter(idx);if(kind==='location')editWorldLocation(idx);if(kind==='timeline')editTimeline(idx);if(kind==='research')editResearch(idx);if(kind==='custom')editCustomEntry(idx);return;}const del=e.target?.closest?.('[data-world-delete]');if(del){await deleteWorldItem(del.dataset.worldDelete,Number(del.dataset.index));}});
  document.addEventListener('click',e=>{const v=e.target?.closest?.('[data-view-import]');if(v){viewOriginalImport(v.dataset.viewImport);return;}const r=e.target?.closest?.('[data-import-reviewed]');if(r){markImportReviewed(Number(r.dataset.importReviewed));return;}});
  document.addEventListener('click',e=>{if(!e.target.closest?.('.composer-shell'))$('#composerMenu')?.classList.add('hidden');});
  window.addEventListener('resize',()=>{if(window.innerWidth>1100)closeSceneSettings();updateDiscoverTagsToggle();});
}

init().catch(err=>{console.error(err);document.body.insertAdjacentHTML('beforeend',`<div style="position:fixed;bottom:20px;right:20px;max-width:520px;background:#511;color:#fff;padding:12px;border-radius:10px;z-index:99999"><strong>MelumiyaChat page load issue</strong><div>${escapeHtml(err.message)}</div></div>`)});
