'use strict';
/* Production workspace features. No note content or passwords are stored in browser storage. */
P.gear='<path d="m9 3 1-1h4l1 1 .5 2 2 .8 1.8-.7 2 3.4-1.4 1.5V13l1.4 1.5-2 3.4-1.8-.7-2 .8L15 21h-6l-.5-3-2-.8-1.8.7-2-3.4L4 13v-2L2.6 9.5l2-3.4 1.8.7 2-.8z"/><circle cx="12" cy="11.5" r="3"/>';
P.star='<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-3-5.6 3 1.1-6.2L3 9.6l6.2-.9z"/>';
P.restore='<path d="M4 11a8 8 0 1 1 2 7M4 4v7h7"/>';
P.focus='<path d="M8 3H3v5M16 3h5v5M3 16v5h5M21 16v5h-5"/>';
P.link='<path d="m10 13 4-4M8 15l-1 1a4 4 0 0 1-6-6l5-5a4 4 0 0 1 6 0M16 9l1-1a4 4 0 0 1 6 6l-5 5a4 4 0 0 1-6 0"/>';
P.clock='<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>';
P.undo='<path d="m8 4-5 5 5 5M3 9h11a6 6 0 0 1 0 12"/>';
P.redo='<path d="m16 4 5 5-5 5M21 9H10a6 6 0 0 0 0 12"/>';
P.copy='<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/>';
const baseDrawEdit=drawEdit,baseBackToList=backToList;
backToList=function(){if(S.focus)toggleFocus(false);baseBackToList()};
let passwordRevealTimer,settingsSaving=Promise.resolve(),dialogResolver=null,findRanges=[],findIndex=0;
const activeItems=()=>S.items.filter(i=>!i.deleted_at);
const selectedItems=()=>visible().filter(i=>S.selected?.has(i.id));
const stamp=t=>new Date(t).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'});
function mergeItems(items){for(const item of Array.isArray(items)?items:[items]){const existing=S.items.find(i=>i.id===item.id);if(existing)Object.assign(existing,item);else S.items.push(item)}}
function matchHTML(text,q=S.q){text=String(text??'');if(!q)return h(text);const needle=q.toLocaleLowerCase(),lower=text.toLocaleLowerCase();let start=0,found,out='';while((found=lower.indexOf(needle,start))!==-1){out+=h(text.slice(start,found))+'<mark>'+h(text.slice(found,found+q.length))+'</mark>';start=found+q.length;if(!q.length)break}return out+h(text.slice(start))}
function visible(){
 const v=S.view,q=S.q.toLocaleLowerCase();let items=S.items.filter(item=>{
  if(v==='trash'?!item.deleted_at:!!item.deleted_at)return false;
  if(v==='favorites'&&!item.favorite)return false;
  if(v.startsWith('t:')&&group(item)!==v.slice(2))return false;
  if(v.startsWith('s:')&&item.section!==v.slice(2))return false;
  return !q||[title(item),textOf(item),item.content,item.user,item.section,group(item)].join(' ').toLocaleLowerCase().includes(q);
 });
 const sort=S.settings?.sort||'edited';return items.sort((a,b)=>sort==='alpha'?title(a).localeCompare(title(b)):sort==='created'?(b.created||b.ts)-(a.created||a.ts):sort==='manual'?(a.order??a.created??a.ts)-(b.order??b.created??b.ts):b.ts-a.ts);
}
function drawSide(){
 const items=activeItems(),nav=(v,icon,label,n,section)=>`<div class="nv-wrap ${S.view===v?'on':''}"><button class="nv" data-v="${h(v)}" ${S.view===v?'aria-current="page"':''}>${ic(icon)}<span class="label">${h(label)}</span><span class="count">${n}</span></button>${section!==undefined?`<button class="icon-button section-more" data-section-menu="${h(section)}" aria-label="Options for ${h(section)}" aria-haspopup="menu" aria-expanded="false">${ic('more')}</button>`:''}</div>`;
 $('#side').innerHTML=`<div class="brand"><div class="brand-mark">${ic('note')}</div><div><div class="brand-name">Nick’s Storage</div><div class="brand-caption">Notes, files & passwords</div></div><button class="icon-button back" id="side-back" aria-label="Back to items">${ic('back')}</button></div><nav><div class="nav-label">Workspace</div>${nav('all','all','All items',items.length)}${nav('favorites','star','Favorites',items.filter(i=>i.favorite).length)}${Object.entries(TY).map(([k,v])=>nav('t:'+k,v[0],v[1],items.filter(i=>group(i)===k).length)).join('')}<div class="group-label"><span>Sections</span><button class="icon-button" id="as" aria-label="New section">${ic('plus')}</button></div>${S.sections.map(name=>nav('s:'+name,'hash',name,items.filter(i=>i.section===name).length,name)).join('')||'<div class="no-sections">Create a section for a project.</div>'}<div class="nav-divider"></div>${nav('trash','trash','Trash',S.items.filter(i=>i.deleted_at).length)}</nav><div class="side-footer"><button class="lock-button" id="storage-open">${ic('file')}Storage overview</button><button class="lock-button" id="lock">${ic('lock')}Lock workspace</button></div>`;
}
const originalSideClick=$('#side').onclick;
$('#side').onclick=guard(async e=>{if(e.target.closest('[data-v]')){S.selected?.clear();S.selecting=false;S.selectionAnchor=null}if(e.target.closest('#storage-open')){await openSettings('storage');return}await originalSideClick(e)});
function drawRows(){
 const items=visible(),view=S.view,rows=$('#rows');
 $('#nw').disabled=view==='trash';$('#nw').title=view==='trash'?'Leave Trash to add items':'New item';
 $('#lt').textContent=view==='all'?'All items':view==='favorites'?'Favorites':view==='trash'?'Trash':view.startsWith('t:')?TY[view.slice(2)][1]:view.slice(2);
 $('#list-count').textContent=items.length+' item'+(items.length===1?'':'s')+(view==='trash'?' · kept for 30 days':'');
 if(!$('#list-controls')){const controls=document.createElement('div');controls.id='list-controls';$('#q').closest('label').after(controls)}
 const sorts=[['edited','Recently edited'],['created','Date created'],['alpha','Alphabetical'],['manual','Manual order']];
 $('#list-controls').innerHTML=`<button id="sort-items" class="section-sort section-picker" aria-label="Sort items" aria-haspopup="menu" aria-expanded="false">Sort: ${h(sorts.find(([id])=>id===S.settings?.sort)?.[1]||'Recently edited')} <span>⌄</span></button><button class="subtle-button" id="select-items" aria-pressed="${!!S.selecting}">${S.selecting?'Done':'Select'}</button>`;
 $('#sort-items').onclick=e=>{const anchor=e.currentTarget;closeMenus();const menu=$('#context-menu');menu.classList.add('organization-menu');menu.innerHTML='<div class="menu-caption">Sort Items</div>'+sorts.map(([id,label])=>`<button role="menuitemradio" class="sort-choice" aria-checked="${S.settings.sort===id}" data-sort="${id}">${ic('hash')}${h(label)}${S.settings.sort===id?' · Selected':''}</button>`).join('');menu.onclick=guard(async event=>{const id=event.target.closest('[data-sort]')?.dataset.sort;if(!id)return;closeMenus();S.settings.sort=id;drawRows();await saveSettings()});openPopover(menu,anchor)};
 $('#select-items').onclick=()=>{S.selecting=!S.selecting;if(!S.selecting)S.selected?.clear();drawRows()};
 if(!$('#bulk-actions')){const bulk=document.createElement('div');bulk.id='bulk-actions';rows.before(bulk)}
 const ids=selectedItems().map(i=>i.id);$('#bulk-actions').classList.toggle('hide',!S.selecting);
 $('#bulk-actions').innerHTML=`<div class="bulk-count"><span>${ids.length} selected</span><button id="select-all">Select all</button><button id="select-none">Clear</button></div><div class="bulk-buttons">${view==='trash'?'<button data-bulk="restore">Restore</button><button data-bulk="purge" class="danger-text">Delete All Selected</button>':'<button data-bulk="move">Move</button><button data-bulk="download">Download</button><button data-bulk="trash" class="danger-text">Delete Selected</button>'}</div>`;
 $('#select-all').onclick=()=>{S.selected=new Set(items.map(i=>i.id));selectionChanged()};$('#select-none').onclick=()=>{S.selected.clear();S.selectionAnchor=null;selectionChanged()};
 $('#bulk-actions').onclick=guard(async e=>{const action=e.target.closest('[data-bulk]')?.dataset.bulk;if(action)await bulkAction(action)});
 const dt=t=>new Date(t).toLocaleDateString(undefined,{month:'short',day:'numeric'});
 rows.innerHTML=items.map(item=>{
  const type=group(item),text=isText(item)?(item.type==='note'?textOf(item):item.content??richOf(item)?.plain):null;
  let preview=isText(item)?(text===undefined?'Text document':text.replace(/\s+/g,' ')||'An empty page, ready for you.'):item.type==='password'?(item.user||'No username'):size(item.size||0);
  if(S.q&&text){const position=text.toLowerCase().indexOf(S.q.toLowerCase());if(position>20)preview='…'+text.slice(position-20).replace(/\s+/g,' ')}
  if(view==='trash')preview=Math.max(0,30-Math.floor((Date.now()-item.deleted_at)/864e5))+' days left in Trash';
  const image=item.type==='file'&&kind(item.title)==='img'&&!item.deleted_at;
  return `<div class="item-row ${S.selecting?'selecting':''}" data-row-id="${h(item.id)}" ${S.settings?.sort==='manual'&&!S.selecting?'draggable="true"':''}>${S.selecting?`<input class="item-select" type="checkbox" data-select="${h(item.id)}" aria-label="Select ${h(title(item))}" ${S.selected.has(item.id)?'checked':''}>`:''}<button class="row ${item.id===S.sel?'on':''}" data-id="${h(item.id)}" data-type="${type}" ${item.id===S.sel?'aria-current="true"':''}><span class="file-icon">${image?`<img loading="lazy" alt="" src="${h(src(item))}">`:ic(TY[type]?.[0]||'file')}</span><span class="row-main"><span class="row-title">${item.favorite?'☆ ':''}${matchHTML(title(item))}</span><span class="row-preview">${matchHTML(preview.slice(0,100))}</span></span><span class="row-date">${dt(item.ts)}</span></button><button class="icon-button row-more" data-item-menu="${h(item.id)}" aria-label="Options for ${h(title(item))}" aria-haspopup="menu" aria-expanded="false">${ic('more')}</button></div>`;
 }).join('')||`<div class="empty-list"><strong>${view==='trash'?'Trash is empty':S.q?'Nothing matched':'A fresh start.'}</strong>${view==='trash'?'Deleted items can be restored for 30 days.':S.q?'Try another word or section.':'Create a note or upload a file with +.'}</div>`;
 addSettingsButtons();
}
function selectionChanged(){
 const valid=new Set(visible().map(i=>i.id));for(const id of S.selected)if(!valid.has(id))S.selected.delete(id);
 S.selecting=S.selected.size>0;drawRows();
}
function selectRange(id){
 const ids=visible().map(i=>i.id),end=ids.indexOf(id);if(end<0)return;
 const anchor=ids.includes(S.selectionAnchor)?S.selectionAnchor:ids.includes(S.sel)?S.sel:id,start=ids.indexOf(anchor),select=!S.selected.has(id);
 S.selectionAnchor=anchor;for(let i=Math.min(start,end);i<=Math.max(start,end);i++){select?S.selected.add(ids[i]):S.selected.delete(ids[i])}
 selectionChanged();
}
$('#rows').onmousedown=e=>{if(e.shiftKey&&e.target.closest('[data-row-id]'))e.preventDefault()};
$('#rows').onchange=e=>{const input=e.target.closest('[data-select]');if(input){input.checked?S.selected.add(input.dataset.select):S.selected.delete(input.dataset.select);S.selectionAnchor=input.dataset.select;selectionChanged()}};
$('#rows').onclick=guard(async e=>{
 const input=e.target.closest('[data-select]');if(input){if(e.shiftKey){e.preventDefault();selectRange(input.dataset.select)}return}
 const more=e.target.closest('[data-item-menu]');if(more){const item=S.items.find(i=>i.id===more.dataset.itemMenu);if(item)openContext({id:item.id},more);return}
 const row=e.target.closest('[data-id]');if(!row)return;const id=row.dataset.id;
 if(e.shiftKey){e.preventDefault();selectRange(id);return}
 if(S.selecting){S.selected.has(id)?S.selected.delete(id):S.selected.add(id);S.selectionAnchor=id;selectionChanged();return}
 S.selectionAnchor=id;await openNote(id);
});
$('#q').oninput=e=>{S.q=e.target.value;drawRows();refreshFind()};
async function openContext(target,anchor){
 $('#context-menu').onclick=contextClickHandler;
 const same=menuAnchor===anchor&&!$('#context-menu').classList.contains('hide');closeMenus();if(same)return;S.menu=target;
 const item=S.items.find(i=>i.id===target.id),menu=$('#context-menu');
 const button=(action,icon,label,cls='')=>`<button role="menuitem" class="${cls}" data-action="${action}">${ic(icon)}${h(label)}</button>`;
 let locationInfo='';if(item){try{const loc=await api('GET','/items/'+item.id+'/locations');locationInfo='<div class="menu-caption">Saved: '+h(loc.label)+'</div><div class="menu-caption">'+h(loc.metadata)+(loc.replicas?' · Two file copies':'')+'</div>'}catch{locationInfo='<div class="menu-caption">Storage location unavailable</div>'}}
 if(S.menu!==target)return;
 if(target.section!==undefined)menu.innerHTML='<div class="menu-caption">Section options</div>'+button('rename-section','hash','Rename Section')+button('delete-section','trash','Delete section','danger');
 else if(target.move)menu.innerHTML='<div class="menu-caption">Move to section</div>'+['',...S.sections].map(name=>`<button role="menuitem" data-action="move" data-section="${h(name)}">${ic('hash')}${h(name||'No section')}${item.section===name?' · Selected':''}</button>`).join('');
 else if(item?.deleted_at)menu.innerHTML=locationInfo+button('restore','restore','Restore')+button('purge','trash','Delete permanently','danger');
 else if(item)menu.innerHTML=locationInfo+`<div class="menu-caption">Edited ${h(stamp(item.ts))}</div><div class="menu-caption">Created ${h(stamp(item.created||item.ts))}</div>`+button('favorite','star',item.favorite?'Remove favorite':'Add to favorites')+button('duplicate','copy','Duplicate')+button('move-menu','hash','Move to section')+(isText(item)?button('copy-link','link','Copy note link'):'')+(isText(item)||item.type==='file'?button('download','download','Download'):'')+(S.settings?.sort==='manual'?button('up','up','Move up')+button('down','download','Move down'):'')+'<div class="menu-divider"></div>'+button('delete-item','trash','Move to Trash','danger');
 else return;
 openPopover(menu,anchor);
}
$('#context-menu').onclick=guard(async e=>{
 const control=e.target.closest('[data-action]'),target=S.menu;if(!control||!target)return;const action=control.dataset.action,section=control.dataset.section;closeMenus();
 const item=S.items.find(i=>i.id===target.id);
 if(action==='rename-section'){
  const name=await ask({title:'Rename Section',input:true,value:target.section,ph:'Section Name',ok:'Save Name'});if(!name||name===target.section)return;
  await flush();const result=await api('PUT','/sections/'+encodeURIComponent(target.section),{name});S.sections=result.sections;mergeItems(await api('GET','/items'));if(S.view==='s:'+target.section)S.view='s:'+result.name;drawSide();drawRows();drawEdit();toast('Section Renamed');return;
 }
 if(action==='delete-section'){
  if(!await ask({title:'Delete section?',msg:'The items inside it will stay in All items.',ok:'Delete section',danger:true}))return;
  await flush();await api('DELETE','/sections/'+encodeURIComponent(target.section));S.sections=S.sections.filter(s=>s!==target.section);mergeItems(await api('GET','/items'));if(S.view==='s:'+target.section)S.view='all';drawSide();drawRows();drawEdit();return;
 }
 if(!item)return;
 if(action==='move-menu'){const chosen=await pickSection(item.section);if(chosen!==null)await moveItems([item.id],chosen);return}
 if(action==='move'){await moveItems([item.id],section||'');return}
 if(action==='favorite'){await flush();mergeItems(await api('POST','/items/bulk',{ids:[item.id],action:item.favorite?'unfavorite':'favorite'}));drawSide();drawRows();drawEdit();return}
 if(action==='duplicate'){await flush();const copy=await api('POST','/items/'+item.id+'/duplicate',{});reveal(copy);toast('Duplicated');return}
 if(action==='delete-item'){await trashItems([item.id]);return}
 if(action==='restore'){await restoreItems([item.id]);return}
 if(action==='purge'){await purgeItem(item);return}
 if(action==='download'){await download(item);return}
 if(action==='copy-link'){copyText(location.origin+'/#note='+item.id);return}
 if(action==='up'||action==='down'){await moveManual(item.id,action==='up'?-1:1)}
});
async function purgeItem(item){
 if(!item?.deleted_at)return;
 const loc=await api('GET','/items/'+item.id+'/locations');let source='both';
 if(loc.replicas){source=await chooseCopy();if(!source)return}
 if(!await ask({title:'Delete permanently?',msg:loc.replicas&&source!=='both'?`Delete only the ${source.toUpperCase()} file copy? The other copy and its entry will stay in Trash.`:'Delete this item and its available file copies? Separate historical backups are not erased.',ok:'Delete permanently',danger:true}))return;
 const result=await api('DELETE','/trash/'+item.id+'?source='+source);
 if(!result.remaining){pending.delete(item.id);S.selected?.delete(item.id);S.items=S.items.filter(i=>i.id!==item.id);if(S.sel===item.id)S.sel=null}
 drawSide();drawRows();drawEdit();toast(result.remaining?'Copy deleted; '+result.remaining.toUpperCase()+' copy kept':'Permanently deleted');
}
function chooseCopy(){return new Promise(resolve=>{showSheet('Delete Which Copy?','<p class="muted">Both file copies exist. Deleting one keeps the remaining copy usable.</p><div class="picker-list"><button data-copy="vps">VPS Only</button><button data-copy="r2">R2 Only</button><button data-copy="both">Both</button><button data-copy="cancel">Cancel</button></div>');dialogResolver=resolve;$('#feature-sheet').onclick=e=>{const b=e.target.closest('[data-copy]');if(b){dialogResolver=null;featureDialog().close();resolve(b.dataset.copy==='cancel'?null:b.dataset.copy)}}})}
async function openNote(id,push=true){
 const item=S.items.find(i=>i.id===id);if(!item)return toast('This note is not available');await flush();S.sel=id;
 if(item.deleted_at)S.view='trash';else if(!visible().some(i=>i.id===id)){S.view='all';S.q='';$('#q').value=''}
 drawSide();drawRows();drawEdit();pane('edit',push);
 if(isText(item)&&push){try{history.replaceState({notesPane:'edit'},'','#note='+item.id)}catch{}}
}
async function moveItems(ids,section){await flush();mergeItems(await api('POST','/items/bulk',{ids,action:'move',section}));drawSide();drawRows();drawEdit();toast('Moved')}
async function restoreItems(ids){mergeItems(await api('POST','/items/bulk',{ids,action:'restore'}));drawSide();drawRows();drawEdit();toast('Restored')}
async function trashItems(ids){
 await flush();const result=await api('POST','/items/bulk',{ids,action:'trash'});mergeItems(result);if(ids.includes(S.sel))S.sel=null;S.selected.clear();drawSide();drawRows();drawEdit();if(!S.sel)pane('list');notifyUndo(ids);
}
function notifyUndo(ids){const t=$('#toast');clearTimeout(toast.timer);t.classList.add('on','actionable');t.innerHTML='Moved to Trash <button id="undo-delete">Undo</button>';$('#undo-delete').onclick=guard(async()=>{await restoreItems(ids);t.classList.remove('actionable')});toast.timer=setTimeout(()=>t.classList.remove('on','actionable'),9000)}
async function bulkAction(action){
 const items=selectedItems(),ids=items.map(i=>i.id);if(!ids.length)return toast('Select at least one item');
 if(action==='move'){const section=await pickSection('');if(section!==null)await moveItems(ids,section)}
 else if(action==='trash'){if(await ask({title:'Delete '+ids.length+' Selected Items?',msg:'Move the selected items to Trash? You can restore them for 30 days.',ok:'Move To Trash',danger:true}))await trashItems(ids)}
 else if(action==='purge')await purgeSelected(items);
 else if(action==='restore')await restoreItems(ids);
 else if(action==='download'){
  toast('Preparing ZIP…');
  if(items.some(i=>i.type==='password')&&!await ask({title:'Download readable passwords?',msg:'This ZIP will contain the selected passwords in readable JSON files. Keep it private.',ok:'Download'}))return;
  await flush();const ticket=await api('POST','/export-ticket',{ids});
  const t=$('#toast');clearTimeout(toast.timer);t.classList.add('on','actionable');t.innerHTML='ZIP ready <a id="download-ready" href="'+h(ticket.url)+'" download="vault-export.zip">Download ZIP</a>';toast.timer=setTimeout(()=>t.classList.remove('on','actionable'),30000);
 }
}
let bulkDeleteRunning=false;
async function purgeSelected(items){
 if(bulkDeleteRunning)return;
 if(!await ask({title:'Permanently Delete '+items.length+' Selected Items?',msg:'Delete all available VPS and R2 file copies of these selected Trash items? This cannot be undone. Separate historical backups are not erased.',ok:'Delete All Selected',danger:true}))return;
 bulkDeleteRunning=true;const removed=new Set();let failed=0,message='';
 try{for(const item of items){try{await api('DELETE','/trash/'+item.id+'?source=both');removed.add(item.id)}catch(e){failed++;message=e.userMessage||'Please retry the remaining items';if(e.status===401)throw e}}}
 finally{S.items=S.items.filter(i=>!removed.has(i.id));for(const id of removed){S.selected.delete(id);pending.delete(id)}if(removed.has(S.sel))S.sel=null;bulkDeleteRunning=false;drawSide();drawRows();drawEdit()}
 toast(removed.size+' permanently deleted'+(failed?' · '+failed+' failed: '+message:''));
}
function saveBlob(blob,name){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000)}
async function moveManual(id,delta){const items=visible(),index=items.findIndex(i=>i.id===id),other=index+delta;if(other<0||other>=items.length)return;[items[index],items[other]]=[items[other],items[index]];await saveManual(items.map(i=>i.id))}
async function saveManual(ids){await flush();mergeItems(await api('POST','/items/bulk',{ids,action:'reorder'}));drawRows()}

/* Dialogs and saved theme profiles. */
function featureDialog(){let d=$('#feature-dialog');if(!d){d=document.createElement('dialog');d.id='feature-dialog';d.innerHTML='<div id="feature-sheet"></div>';document.body.append(d);d.addEventListener('cancel',()=>{if(!d.dataset.approval)hideSettings()});d.addEventListener('click',e=>{if(e.target===d&&!d.dataset.approval)hideSettings()})}return d}
let resourceTimer=null,approvalTimer=null,settingsEpoch=0,resourceResume=null,approvalResume=null;
function showSheet(title,body){clearTimeout(resourceTimer);clearTimeout(approvalTimer);resourceResume=null;approvalResume=null;settingsEpoch++;const existing=$('#feature-dialog');if(existing){existing.oncancel=null;existing.onclick=null;delete existing.dataset.approval;}const d=featureDialog();$('#feature-sheet').innerHTML=`<header class="sheet-head"><h2>${h(title)}</h2><button id="sheet-close" class="icon-button" aria-label="Close">${ic('back')}</button></header><div class="sheet-body">${body}</div>`;$('#sheet-close').onclick=hideSettings;d.classList.toggle('settings-dialog',title==='Settings');
 const tabs=$('#feature-sheet .settings-tabs');
 if(tabs){const body=$('#feature-sheet .sheet-body'),layout=document.createElement('div'),content=document.createElement('div');layout.className='settings-layout';content.className='settings-content';tabs.setAttribute('role','navigation');tabs.setAttribute('aria-label','Settings categories');const children=[...body.childNodes];layout.append(tabs,content);for(const child of children)if(child!==tabs)content.append(child);body.append(layout);body.classList.add('settings-body')}
 themeNativeSelects(d);if(!d.open)d.showModal();return d}
function themeNativeSelects(root){
 for(const select of root.querySelectorAll('select')){
  const options=[...select.options].map(o=>({value:o.value,label:o.textContent,disabled:o.disabled})),button=document.createElement('button');
  button.id=select.id;button.type='button';button.className='themed-select';button.value=select.value;button.disabled=select.disabled;button.dataset.themedPicker='true';button.setAttribute('aria-haspopup','menu');button.setAttribute('aria-expanded','false');
  const label=select.getAttribute('aria-label')||select.closest('label')?.firstChild?.textContent.trim()||'Choose Option';button.setAttribute('aria-label',label);
  const paint=()=>button.innerHTML='<span>'+h(options.find(o=>o.value===button.value)?.label||'Choose…')+'</span><span aria-hidden="true">⌄</span>';paint();
  button.onclick=()=>openThemedPicker(button,label,options,button.value,value=>{button.value=value;paint();button.dispatchEvent(new Event('change',{bubbles:true}))});
  button.onchange=select.onchange;select.replaceWith(button);
 }
}
function openThemedPicker(anchor,label,options,value,onPick){
 const wasOpen=menuAnchor===anchor;closeMenus();if(wasOpen)return;
 const menu=document.createElement('div');menu.className='popover organization-menu themed-picker';menu.setAttribute('role','menu');menu.setAttribute('aria-label',label);
 menu.innerHTML='<div class="menu-caption">'+h(label)+'</div>'+options.map(option=>`<button type="button" role="menuitemradio" aria-checked="${option.value===value}" data-choice="${h(option.value)}" ${option.disabled?'disabled':''}>${ic('hash')}<span>${h(option.label)}${option.value===value?' · Selected':''}</span></button>`).join('');
 (anchor.closest('dialog')||document.body).append(menu);menu.onclick=guard(async e=>{const choice=e.target.closest('[data-choice]');if(!choice||choice.disabled)return;const value=choice.dataset.choice;closeMenus(true);await onPick(value)});
 menu.onkeydown=e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();closeMenus(true)}};openPopover(menu,anchor);
}
function hideSettings(){clearTimeout(resourceTimer);clearTimeout(approvalTimer);resourceResume=null;approvalResume=null;settingsEpoch++;const d=$('#feature-dialog');if(d){delete d.dataset.approval;d.oncancel=null;d.onclick=null;}if(d?.open)d.close();dialogResolver?.(null);dialogResolver=null}
function pickSection(selected){return new Promise(resolve=>{showSheet('Move to section','<div class="picker-list">'+['',...S.sections].map(name=>`<button data-pick-section="${h(name)}">${ic('hash')}${h(name||'No section')}${name===selected?' · Current':''}</button>`).join('')+'</div>');dialogResolver=resolve;$('#feature-sheet').onclick=e=>{const button=e.target.closest('[data-pick-section]');if(button){dialogResolver=null;featureDialog().close();resolve(button.dataset.pickSection)}}})}
function addSettingsButtons(){
 const listHead=$('.list-head');if(listHead&&!listHead.querySelector('.settings-open')){const button=document.createElement('button');button.className='icon-button settings-open';button.setAttribute('aria-label','Settings');button.innerHTML=ic('gear');button.onclick=()=>guard(openSettings)();listHead.append(button)}
 const head=$('.editor-head');if(head&&!head.querySelector('.settings-open')){const button=document.createElement('button');button.className='icon-button settings-open';button.setAttribute('aria-label','Settings');button.innerHTML=ic('gear');button.onclick=()=>guard(openSettings)();head.append(button)}
}
function rgb(color){return [1,3,5].map(i=>parseInt(color.slice(i,i+2),16))}
function tone(color,ratio){return '#'+rgb(color).map(c=>Math.round(c*ratio).toString(16).padStart(2,'0')).join('')}
function applyTheme(useCached=false){
 let color=useCached?null:S.settings?.profiles?.find(p=>p.id===S.settings.active)?.color;
 if(!color){try{color=localStorage.getItem('vault.theme.color')}catch{}}
 if(!/^#[0-9a-f]{6}$/i.test(color||''))color='#bcb7ff';
 try{localStorage.setItem('vault.theme.color',color)}catch{}
 let style=$('#saved-theme');if(!style){style=document.createElement('style');style.id='saved-theme';document.head.append(style)}
 const dark=tone(color,.13),selected=tone(color,.22),line=tone(color,.31),icon=tone(color,.77);
 style.textContent=`:root{--ac:${color};--acs:${selected}} .nv-wrap.on{background:${selected};color:${color};box-shadow:inset 2px 0 ${color}}.nv-wrap.on .label,.on .nv .count,.row.on .row-title{color:${color}}.row.on{background:${dark};border-color:${line};box-shadow:inset 2px 0 ${icon}}.brand-mark,.file-icon,.row.on .file-icon{background:${tone(color,.095)};color:${color};border-color:${line}}.new-button,.primary:not(.danger),.check-toggle[data-checked=true]{background:${color};border-color:${color};color:#15131d}.format-button[aria-pressed=true],.format-button[aria-expanded=true],.popover button:hover,.popover button:focus-visible,#es{background:${dark};border-color:${line};color:${color}}.row-more:hover,.format-button:hover,.crumb svg,.field button,.choice-check{color:${color}}.check-toggle:hover,.search:focus-within,.field:focus-within{border-color:${color}}.note-link{color:${color}}`;
}
async function saveSettings(){const snapshot=JSON.parse(JSON.stringify(S.settings));settingsSaving=settingsSaving.catch(()=>{}).then(()=>api('PUT','/settings',snapshot));return settingsSaving}
async function openSettings(tab='appearance'){
 if(typeof tab!=='string')tab='appearance';
 const tabs=`<div class="settings-tabs"><button data-settings-tab="security" class="${tab==='security'?'on':''}">Security</button><button data-settings-tab="migration" class="${tab==='migration'?'on':''}">Migrate</button><button data-settings-tab="resources" class="${tab==='resources'?'on':''}">Resources</button><button data-settings-tab="appearance" class="${tab==='appearance'?'on':''}">Appearance</button><button data-settings-tab="devices" class="${tab==='devices'?'on':''}">Devices</button><button data-settings-tab="storage" class="${tab==='storage'?'on':''}">Storage</button></div>`;
 if(tab==='resources'){
  showSheet('Settings',tabs+'<div id="resources-panel"><p class="muted">Measuring Website Resources…</p></div>');bindSettingsTabs();const epoch=settingsEpoch;let previous=null;
  const refresh=async()=>{if(settingsEpoch!==epoch||!featureDialog().open||document.visibilityState!=='visible')return;try{const data=await api('GET','/resources');if(settingsEpoch!==epoch||!featureDialog().open||document.visibilityState!=='visible')return;let cpu=null;if(previous&&data.cpuUsec>=previous.cpuUsec&&data.sampleTime>previous.sampleTime)cpu=(data.cpuUsec-previous.cpuUsec)/1e6/(data.sampleTime-previous.sampleTime)*100;previous=data;
   $('#resources-panel').innerHTML=`<h3>Website Usage</h3><div class="storage-grid"><div><span>Website RAM</span><strong>${h(size(data.memoryBytes))}</strong></div><div><span>Website CPU</span><strong>${cpu===null?'Measuring…':cpu.toFixed(1)+'%'}</strong></div></div><p class="muted">${h(data.scope)}. CPU: 100% means one core. Refreshes every five seconds only while this panel is open.</p>`;
  }catch{if(settingsEpoch===epoch&&$('#resources-panel'))$('#resources-panel').textContent='Resource readings unavailable. Retrying…'}if(settingsEpoch===epoch&&featureDialog().open&&document.visibilityState==='visible')resourceTimer=setTimeout(refresh,5000)};resourceResume=()=>{previous=null;refresh()};await refresh();return;
 }
 if(tab==='security'||tab==='migration'){
  const migration=tab==='migration';showSheet('Settings',tabs+`<h3>${migration?'Migrate To Another VPS':'Change PIN'}</h3><p class="muted">${migration?'Downloads setup files, the encrypted database, encryption key, available files including Trash, and accessible app backups. Already permanently deleted data cannot be recreated. Choose a strong export passphrase and keep it safe.':'Enter the current PIN and confirm the new PIN. Other sessions will be signed out after approval.'}</p><form id="sensitive-form"><label class="setting-label">Current PIN<input id="current-pin" type="password" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autocomplete="off"></label>${migration?'<label class="setting-label">Export Passphrase<input id="export-phrase" type="password" minlength="12" maxlength="200" required autocomplete="new-password"></label><label class="setting-label">Confirm Export Passphrase<input id="confirm-phrase" type="password" minlength="12" maxlength="200" required autocomplete="new-password"></label>':'<label class="setting-label">New PIN<input id="new-pin" type="password" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autocomplete="new-password"></label><label class="setting-label">Confirm New PIN<input id="confirm-pin" type="password" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autocomplete="new-password"></label>'}<button class="primary" type="submit">${migration?'Accept & Request Migration':'Request PIN Change'}</button><button class="secondary" id="cancel-sensitive" type="button">Cancel</button><p id="request-error" role="status" class="muted"></p></form><p class="muted">In your SSH terminal run <code>nicks-storage approvals</code>. Then approve this exact request with Y or deny with N. This is VPS approval, not an authenticator-app code.</p>`);bindSettingsTabs();$('#cancel-sensitive').onclick=hideSettings;
  const form=$('#sensitive-form');form.noValidate=true;const errorBox=$('#request-error');errorBox.setAttribute('role','alert');errorBox.setAttribute('aria-live','assertive');form.prepend(errorBox);
  form.onsubmit=async e=>{
   e.preventDefault();const checked=validateSensitiveForm(migration);if(!checked)return;
   const button=e.submitter||form.querySelector('button[type=submit]');if(button.disabled)return;button.disabled=true;
   try{if(!await askSensitiveConfirm(migration))return;const result=await api('POST','/approvals',checked);waitForApproval(result.id,migration)}
   catch(error){showRequestError(error.userMessage||error.message||'Request failed. Please retry.')}
   finally{if(button.isConnected)button.disabled=false}
  };return;
 }
 if(tab==='devices'){
  showSheet('Settings',tabs+'<p class="muted">Loading login history…</p>');bindSettingsTabs();const epoch=settingsEpoch;const devices=await api('GET','/devices');if(!featureDialog().open||settingsEpoch!==epoch)return;
  showSheet('Settings',tabs+'<p class="muted">Last 50 successful logins. Browser information is reported by the device; it is not verified device identity.</p><div class="device-list">'+devices.map(d=>`<article class="device-entry"><strong>${h(d.current?'This session':d.active?'Active session':'Past login')}</strong><p>${h(d.agent||'Unknown browser')}</p><small>${h(stamp(d.at))} · ${h(d.ip||'Unknown IP')}</small>${d.active&&!d.current?`<button class="secondary" data-revoke="${h(d.id)}">Sign out session</button>`:''}</article>`).join('')+'</div>');bindSettingsTabs();for(const b of document.querySelectorAll('[data-revoke]'))b.onclick=guard(async()=>{await api('DELETE','/devices/'+b.dataset.revoke);await openSettings('devices')});return;
 }
 if(tab==='storage'){
  showSheet('Settings',tabs+'<p class="muted">Loading storage…</p>');bindSettingsTabs();const epoch=settingsEpoch;const data=await api('GET','/storage');if(!featureDialog().open||settingsEpoch!==epoch)return;
  const used=data.objectBytes??(data.activeBytes+data.trashBytes),percent=Math.min(100,used/data.quotaBytes*100);
  showSheet('Settings',tabs+`<p class="muted">${h(data.provider||'Demo storage')} · app storage limit</p><div class="storage-total">${h(size(used))}<span> of ${h(size(data.quotaBytes))}</span></div><progress class="storage-progress" max="100" value="${percent}"></progress><div class="storage-grid"><div><span>VPS File Copies</span><strong>${h(size(data.locations?.vpsFileBytes||0))}</strong></div><div><span>VPS Database & Metadata</span><strong>${h(size(data.locations?.vpsDatabaseBytes||0))}</strong></div><div><span>R2 File Copies</span><strong>${h(size(data.locations?.r2FileBytes||0))}</strong></div><div><span>VPS Free Disk</span><strong>${h(size(data.locations?.vpsDiskFree||0))}</strong></div></div><p class="muted">VPS metadata is not a second file copy. Separate historical backups are not erased by deleting an item.</p><div class="budget-panel"><h3>Storage & budget</h3><div class="storage-grid"><div><span>Estimated monthly storage</span><strong>$${Number(data.estimatedMonthlyUSD||0).toFixed(2)} USD</strong></div><div><span>Month-to-date estimate</span><strong>$${Number(data.estimatedMTDUSD||0).toFixed(2)} USD</strong></div></div><p>Budget: $${Number(data.estimatedMTDUSD||0).toFixed(2)} / $${Number(data.budgetUSD??1).toFixed(2)} USD</p><progress class="storage-progress" max="1" value="${Math.min(1,(data.estimatedMTDUSD||0)/(data.budgetUSD||1))}"></progress>${(data.warnings||[]).map(w=>`<p class="budget-warning">${h(w)}</p>`).join('')}<p class="muted">${h(data.estimateNotice||'Demo estimates only.')}</p></div><div class="storage-grid"><div><span>Active files & notes</span><strong>${h(size(data.activeBytes))}</strong></div><div><span>In Trash</span><strong>${h(size(data.trashBytes))}</strong></div><div><span>Notes</span><strong>${data.counts.note}</strong></div><div><span>Files</span><strong>${data.counts.file}</strong></div><div><span>Passwords</span><strong>${data.counts.password}</strong></div></div><p class="muted">Trash is kept for 30 days. Files up to ${h(size(data.maxUploadBytes))}; editable text up to 2 MB.</p>`);bindSettingsTabs();return;
 }
 const profile=S.settings.profiles.find(p=>p.id===S.settings.active)||S.settings.profiles[0];
 showSheet('Settings',tabs+`<label class="setting-label">Saved theme profile<select id="theme-profile">${S.settings.profiles.map(p=>`<option value="${h(p.id)}" ${p.id===profile.id?'selected':''}>${h(p.name)}</option>`).join('')}</select></label><div class="theme-preview" style="--preview-color:${profile.color}"><i></i><span>${h(profile.name)}</span></div><label class="setting-label">Accent color<input id="theme-hex" value="${h(profile.color)}" maxlength="7" autocomplete="off" spellcheck="false" aria-label="Theme hex color"></label><div class="theme-swatches">${['#bcb7ff','#a9c7f4','#a4d6bf','#efc19c','#f2a8c2','#d0d0da'].map(color=>`<button data-theme-color="${color}" aria-label="Use ${color}" style="background:${color}"></button>`).join('')}</div><div class="setting-actions"><button class="primary" id="save-theme">Save color</button><button class="secondary" id="new-theme">Save as new profile</button></div><p class="muted" id="theme-status">Profiles and colors are saved to your account and sync between devices.</p>`);
 bindSettingsTabs();$('#theme-profile').onchange=guard(async e=>{S.settings.active=e.target.value;applyTheme();await saveSettings();await openSettings()});
 const color=()=>{const value=$('#theme-hex').value.trim();if(!/^#[0-9a-f]{6}$/i.test(value)){$('#theme-status').textContent='Use a hex color such as #bcb7ff.';return null}return value.toLowerCase()};
 $('#save-theme').onclick=guard(async()=>{const value=color();if(!value)return;profile.color=value;applyTheme();await saveSettings();$('#theme-status').textContent='Color saved.'});
 for(const button of document.querySelectorAll('[data-theme-color]'))button.onclick=()=>{$('#theme-hex').value=button.dataset.themeColor;$('#save-theme').click()};
 $('#new-theme').onclick=guard(async()=>{if(S.settings.profiles.length>=20)return toast('You can save up to 20 profiles');const value=color();if(!value)return;hideSettings();const name=await ask({title:'New theme profile',input:true,ph:'Profile name',ok:'Save profile'});if(!name)return;const profile={id:'theme-'+crypto.getRandomValues(new Uint32Array(2)).join('-'),name:name.slice(0,40),color:value};S.settings.profiles.push(profile);S.settings.active=profile.id;applyTheme();await saveSettings();await openSettings()});
}
function showRequestError(message,field){
 const box=$('#request-error');if(box){box.textContent=message;box.scrollIntoView?.({block:'nearest'});}
 if(field){field.setAttribute('aria-invalid','true');field.focus({preventScroll:true})}
}
function validateSensitiveForm(migration){
 const form=$('#sensitive-form');form.querySelectorAll('[aria-invalid]').forEach(i=>i.removeAttribute('aria-invalid'));$('#request-error').textContent='';
 const current=$('#current-pin');if(!/^[0-9]{6}$/.test(current.value)){showRequestError('Current PIN must contain exactly 6 digits.',current);return null}
 if(migration){const phrase=$('#export-phrase'),confirm=$('#confirm-phrase');
  if(phrase.value!==confirm.value){showRequestError('Export passphrases do not match. Enter the same passphrase in both boxes.',confirm);return null}
  if(phrase.value.length<12||phrase.value.length>200){showRequestError('Export passphrase must contain 12–200 characters.',phrase);return null}
  return {kind:'migration',currentPin:current.value,passphrase:phrase.value,confirmPassphrase:confirm.value};
 }
 const pin=$('#new-pin'),confirm=$('#confirm-pin');
 if(pin.value!==confirm.value){showRequestError('New PIN and confirmation do not match.',confirm);return null}
 if(!/^[0-9]{6}$/.test(pin.value)){showRequestError('New PIN must contain exactly 6 digits.',pin);return null}
 if(pin.value===current.value){showRequestError('Choose a new PIN that differs from your current PIN.',pin);return null}
 if(new Set(pin.value).size===1||['123456','654321','012345','543210','111222','121212'].includes(pin.value)){showRequestError('Choose a less predictable PIN.',pin);return null}
 return {kind:'pin',currentPin:current.value,newPin:pin.value,confirmPin:confirm.value};
}
function askSensitiveConfirm(migration){return Promise.resolve(window.confirm(migration?'Accept creation of a sensitive migration archive, subject to VPS approval?':'Request a PIN change, subject to VPS approval?'))}
function waitForApproval(id,migration){
 clearTimeout(approvalTimer);showSheet('Waiting For VPS',`<p id="approval-status" role="status">Waiting For VPS Approval…</p><p class="muted">Request: <code>${h(id)}</code><br>Run <code>nicks-storage approvals</code> in your SSH terminal. Check this request ID before typing Y.</p><button id="approval-cancel" class="secondary">Cancel Request</button>`);
 const cancel=async()=>{try{await api('DELETE','/approvals/'+id);clearTimeout(approvalTimer);hideSettings();toast('Request Cancelled')}catch(e){toast(e.userMessage||'Could not cancel; checking request status')}};
 $('#approval-cancel').onclick=cancel;$('#sheet-close').onclick=cancel;const dialog=featureDialog();dialog.dataset.approval=id;dialog.oncancel=e=>{e.preventDefault();cancel()};dialog.onclick=e=>{if(e.target===dialog)cancel()};
 const poll=async()=>{if(!dialog.open||document.visibilityState!=='visible')return;try{const result=await api('GET','/approvals/'+id);if(!dialog.open||document.visibilityState!=='visible')return;
  if(result.status==='approved'){approvalResume=null;dialog.oncancel=null;dialog.onclick=null;hideSettings();await leave(true);say('PIN Changed. Enter Your New PIN.');return}
  if(result.status==='ready'){approvalResume=null;$('#approval-status').innerHTML='Migration Ready. <a href="'+h(result.download)+'" download>Download Encrypted Migration</a><p class="muted">Keep your export passphrase. Available for one hour; one download.</p>';return}
  if(['cancelled','expired','denied','failed','downloaded'].includes(result.status)){approvalResume=null;$('#approval-status').textContent=result.message||('Request '+result.status);$('#approval-cancel').textContent='Close';delete dialog.dataset.approval;dialog.oncancel=null;dialog.onclick=null;$('#sheet-close').onclick=hideSettings;$('#approval-cancel').onclick=()=>{dialog.oncancel=null;dialog.onclick=null;hideSettings()};return}
  $('#approval-status').textContent=result.status==='building'?'VPS Approved — Preparing Encrypted Migration…':'Waiting For VPS Approval…';
 }catch(e){if($('#approval-status'))$('#approval-status').textContent=e.userMessage||'Connection interrupted; retrying…'}if(document.visibilityState==='visible'&&dialog.open)approvalTimer=setTimeout(poll,1500)};approvalResume=poll;poll();
}
function bindSettingsTabs(){for(const button of document.querySelectorAll('[data-settings-tab]'))button.onclick=()=>guard(()=>openSettings(button.dataset.settingsTab))()}

/* Rich-document allowlist. Template parsing keeps external resources inert until validated. */
const VAULT_ID=/^[0-9a-f]{32}$/;
function sanitizeNote(html){
 const template=document.createElement('template');template.innerHTML=String(html||'');
 const allowed=new Set(['PRE','DIV','P','BR','SPAN','B','STRONG','I','EM','U','S','A','IMG','VIDEO','AUDIO','IFRAME']);
 function clean(parent){for(let node of [...parent.children]){
  if(node.tagName==='DIV'&&node.classList.contains('note-code')){node.replaceWith(makeCodeBlock(node.querySelector('.code-text')?.textContent||'',node.dataset.codeLanguage||'text'));continue}
  if(['SCRIPT','STYLE','OBJECT','EMBED','FORM','INPUT','TEXTAREA','SELECT','META','LINK','BASE'].includes(node.tagName)){node.remove();continue}
  if(node.tagName==='FONT'){const span=document.createElement('span');span.style.color=node.getAttribute('color')||node.style.color;span.style.fontSize=node.style.fontSize;span.append(...node.childNodes);node.replaceWith(span);node=span}
  if(!allowed.has(node.tagName)){clean(node);node.replaceWith(...node.childNodes);continue}
  const classes=new Set(node.classList),data={...node.dataset},style={color:node.style.color,size:node.style.fontSize,weight:node.style.fontWeight},source=node.getAttribute('src')||'',href=node.getAttribute('href')||'';
  for(const attr of [...node.attributes])node.removeAttribute(attr.name);
  if(/^(#[a-f\d]{3,8}|rgba?\([\d.,%\s]+\)|[a-z]{3,20})$/i.test(style.color))node.style.color=style.color;
  if(/^(1[4-9]|2[0-9]|3[0-2])px$/.test(style.size))node.style.fontSize=style.size;
  if(['bold','700','400','normal'].includes(style.weight))node.style.fontWeight=style.weight;
  if(node.tagName==='A'){
   if(VAULT_ID.test(data.noteLink||'')){node.className='note-link';node.dataset.noteLink=data.noteLink;node.href='#note='+data.noteLink;node.contentEditable='false'}
   else if(/^https?:\/\/[^\s]+$/i.test(href)){node.href=href;node.className='web-link';node.target='_blank';node.rel='noopener noreferrer';node.contentEditable='false'}
   else if(/^\/api\/files\/[0-9a-f]{32}\?download=1$/.test(href)){node.href=href;node.className='attachment-open';node.target='_blank';node.rel='noopener noreferrer'}
   else{clean(node);node.replaceWith(...node.childNodes);continue}
  }
  if(['IMG','VIDEO','AUDIO','IFRAME'].includes(node.tagName)){
   if(!/^\/api\/files\/[0-9a-f]{32}\?inline=1$/.test(source)){node.remove();continue}
   node.src=source;
   if(node.tagName==='IMG'){node.alt=(data.name||'Attached image').slice(0,180);node.loading='lazy'}
   if(node.tagName==='VIDEO'||node.tagName==='AUDIO'){node.controls=true;node.setAttribute('preload','metadata');node.setAttribute('playsinline','')}
   if(node.tagName==='IFRAME'){node.title='Attached PDF';node.setAttribute('sandbox','');node.loading='lazy';node.referrerPolicy='no-referrer'}
  }
  if(classes.has('note-attachment')&&node.tagName==='DIV'&&VAULT_ID.test(data.fileId||'')){node.className='note-attachment';node.dataset.fileId=data.fileId;node.dataset.name=String(data.name||'File').slice(0,180);node.contentEditable='false'}
  if(classes.has('attachment-caption'))node.classList.add('attachment-caption');
  if(classes.has('attachment-label'))node.classList.add('attachment-label');
  if(classes.has('note-heading')&&node.tagName==='DIV'&&['1','2'].includes(data.heading)){node.className='note-heading';node.dataset.heading=data.heading;node.dataset.folded=String(data.folded==='true')}
  if(classes.has('heading-text'))node.classList.add('heading-text');
  if(classes.has('fold-toggle')&&node.tagName==='SPAN'){node.className='fold-toggle';node.contentEditable='false';node.setAttribute('role','button');node.tabIndex=0;node.setAttribute('aria-label','Collapse heading');node.textContent='›'}
  if(classes.has('note-check')&&node.tagName==='DIV')node.className='note-check';
  if(classes.has('check-text'))node.classList.add('check-text');
  if(classes.has('check-toggle')&&node.tagName==='SPAN'){const checked=data.checked==='true';node.className='check-toggle';node.contentEditable='false';node.setAttribute('role','checkbox');node.tabIndex=0;node.dataset.checked=String(checked);node.setAttribute('aria-checked',String(checked));node.setAttribute('aria-label','Checklist item');node.textContent=checked?'✓':''}
  else clean(node);
 }}
 clean(template.content);const out=document.createElement('div');out.append(template.content);return out.innerHTML;
}
function makeCodeBlock(text='',language='text'){
 const block=document.createElement('div');block.className='note-code';block.dataset.codeLanguage=/^[a-z0-9+_.#-]{1,30}$/i.test(language)?language:'text';block.contentEditable='false';
 const bar=document.createElement('div');bar.className='code-toolbar';bar.contentEditable='false';
 const label=document.createElement('span');label.className='code-language';label.textContent=block.dataset.codeLanguage==='text'?'Code':block.dataset.codeLanguage;
 const copy=document.createElement('span');copy.className='code-copy';copy.textContent='Copy';copy.contentEditable='false';copy.setAttribute('role','button');copy.tabIndex=0;copy.setAttribute('aria-label','Copy code block');
 const pre=document.createElement('pre');pre.className='code-text';pre.contentEditable='true';pre.spellcheck=false;pre.setAttribute('aria-label','Code block');pre.textContent=text;
 bar.append(label,copy);block.append(bar,pre);return block;
}
function fencedFragment(text){
 const lines=String(text).replace(/\r\n/g,'\n').split('\n'),fragment=document.createDocumentFragment();let found=false;
 for(let i=0;i<lines.length;i++){
  const open=lines[i].match(/^\s*(```|''')([a-z0-9+_.#-]{0,30})\s*$/i);let end=i+1;
  if(open){while(end<lines.length&&lines[end].trim()!==open[1])end++}
  if(open&&end<lines.length){fragment.append(makeCodeBlock(lines.slice(i+1,end).join('\n'),open[2]||'text'));found=true;i=end}
  else{const div=document.createElement('div');div.textContent=lines[i];if(!lines[i])div.append(document.createElement('br'));fragment.append(div)}
 }
 return found?fragment:null;
}
function convertFencedLines(fx){
 const lines=[...fx.children];let changed=false,last=null;
 for(let i=0;i<lines.length;i++){
  const line=lines[i];if(line.matches('.note-code,.note-check,.note-attachment,.note-heading'))continue;
  const open=line.textContent.match(/^\s*(```|''')([a-z0-9+_.#-]{0,30})\s*$/i);if(!open)continue;
  let end=i+1;while(end<lines.length&&!lines[end].matches('.note-code,.note-check,.note-attachment,.note-heading')&&lines[end].textContent.trim()!==open[1])end++;
  if(end>=lines.length||lines[end].textContent.trim()!==open[1])continue;
  const block=makeCodeBlock(lines.slice(i+1,end).map(l=>l.textContent).join('\n'),open[2]||'text');line.replaceWith(block);for(let j=i+1;j<=end;j++)lines[j].remove();changed=true;last=block;i=end;
 }
 if(last){let blank=last.nextElementSibling;if(!blank||blank.matches('.note-code,.note-attachment,.note-check,.note-heading')){blank=document.createElement('div');blank.innerHTML='<br>';last.after(blank)}fx._caret(blank)}
 return changed;
}
function insertCodeText(fx,pre,text){
 const selection=getSelection();if(!selection.rangeCount)return;const range=selection.getRangeAt(0);if(!pre.contains(range.startContainer)||!pre.contains(range.endContainer))return;
 range.deleteContents();const node=document.createTextNode(text);range.insertNode(node);range.setStartAfter(node);range.collapse(true);selection.removeAllRanges();selection.addRange(range);fx._remember();fx._save('code');
}
async function copyCode(copy){
 const text=copy.closest('.note-code')?.querySelector('.code-text')?.textContent||'';let copied=false;
 try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(text);copied=true}}catch{}
 if(!copied){const input=document.createElement('textarea');input.value=text;input.style.cssText='position:fixed;opacity:0';document.body.append(input);input.select();copied=!!document.execCommand('copy');input.remove()}
 if(copied){copy.textContent='Copied';setTimeout(()=>{if(copy.isConnected)copy.textContent='Copy'},1500)}else toast('Copy blocked by the browser');
}

function insertIntoNote(fx,node){if(!fx?.isConnected)return;const range=fx._restore();range.deleteContents();range.insertNode(node);const cursor=document.createRange();cursor.setStartAfter(node);cursor.collapse(true);getSelection().removeAllRanges();getSelection().addRange(cursor);fx._remember();fx._save('insert')}
function updateUndoButtons(fx){if($('#note-undo'))$('#note-undo').disabled=!fx._canUndo?.();if($('#note-redo'))$('#note-redo').disabled=!fx._canRedo?.()}
function updateChecklist(item,fx){
 const bar=$('#checklist-tools');if(!bar)return;const checks=[...fx.querySelectorAll('.check-toggle')],done=checks.filter(c=>c.dataset.checked==='true').length;
 bar.classList.toggle('hide',!checks.length);$('#checklist-progress').textContent=done+' / '+checks.length+' done';
 const hidden=!!S.settings.checklists?.[item.id];fx.classList.toggle('hide-completed',hidden);$('#hide-done').textContent=hidden?'Show completed':'Hide completed';$('#hide-done').setAttribute('aria-pressed',String(hidden));
}
function moveCompleted(fx){
 let run=[];const finish=before=>{if(run.length){const sorted=run.filter(row=>row.querySelector('.check-toggle')?.dataset.checked!=='true').concat(run.filter(row=>row.querySelector('.check-toggle')?.dataset.checked==='true'));for(const row of sorted)fx.insertBefore(row,before);run=[]}};
 for(const child of [...fx.children]){if(child.classList.contains('note-check'))run.push(child);else finish(child)}finish(null);fx._caret(fx,true);fx._save('checklist');
}
function updateFolds(fx){
 let hiddenUntil=0;for(const child of [...fx.children]){
  const level=child.classList.contains('note-heading')?Number(child.dataset.heading):0;
  if(level&&hiddenUntil&&level<=hiddenUntil)hiddenUntil=0;
  child.classList.toggle('fold-hidden',!!hiddenUntil);
  if(level){const folded=child.dataset.folded==='true',toggle=child.querySelector(':scope > .fold-toggle');if(toggle){toggle.setAttribute('aria-expanded',String(!folded));toggle.setAttribute('aria-label',(folded?'Expand':'Collapse')+' heading')}if(!hiddenUntil&&folded)hiddenUntil=level}
 }
}
function makeHeading(line,level){
 const source=line.classList.contains('note-heading')?line.querySelector(':scope > .heading-text'):line;
 const body=document.createElement('div');
 if(level){body.className='note-heading';body.dataset.heading=String(level);body.dataset.folded='false';const toggle=document.createElement('span');toggle.className='fold-toggle';toggle.contentEditable='false';toggle.setAttribute('role','button');toggle.tabIndex=0;toggle.textContent='›';const text=document.createElement('span');text.className='heading-text';text.append(...source.childNodes);body.append(toggle,text)}else body.append(...source.childNodes);
 body.querySelectorAll('[style]').forEach(node=>node.style.removeProperty('font-size'));if(!body.textContent.trim()&&!body.querySelector('br'))body.append(document.createElement('br'));line.replaceWith(body);return body;
}
function applyHeadingFormat(fx,range,value){
 const rows=[...fx.children].filter(row=>range.collapsed?(row.contains(range.startContainer)||range.startContainer===fx&&row===fx.lastElementChild):range.intersectsNode(row));
 if(!rows.length||rows.some(row=>row.classList.contains('note-check')||row.classList.contains('note-attachment')))return false;
 if(value===16&&!rows.some(row=>row.classList.contains('note-heading')))return false;
 let last;for(const row of rows)last=makeHeading(row,value===26?1:value===20?2:0);
 fx._caret(last.querySelector('.heading-text')||last,true);fx._save('heading');return true;
}
function handleHeadingEnter(e,fx){
 const selection=getSelection(),range=selection.rangeCount?selection.getRangeAt(0):null;
 const target=e.target.closest('.fold-toggle');if(target&&[' ','Enter'].includes(e.key)){e.preventDefault();target.click();return true}
 if(e.key!=='Enter'||e.shiftKey||!range)return false;
 let node=range.startContainer.nodeType===3?range.startContainer.parentElement:range.startContainer;
 const row=node.closest?.('.note-heading');if(!row||!fx.contains(row))return false;
 const label=row.querySelector(':scope > .heading-text');if(!label.contains(range.startContainer))return false;
 e.preventDefault();if(!label.textContent.trim()){const body=makeHeading(row,0);fx._caret(body);fx._save('heading');return true}
 if(!range.collapsed)range.deleteContents();const tail=document.createRange();tail.selectNodeContents(label);tail.setStart(range.startContainer,range.startOffset);const fragment=tail.extractContents(),body=document.createElement('div');body.append(fragment);if(!body.textContent)body.innerHTML='<br>';row.after(body);fx._caret(body);fx._save('heading');return true;
}
function noteReady(item,fx){
 const tools=$('.note-tools');
 const control=(id,label,icon)=>{const button=document.createElement('button');button.id=id;button.className='format-button';button.setAttribute('aria-label',label);button.title=label;button.innerHTML=ic(icon);return button};
 const code=control('note-code','Insert Code Block','file');code.textContent='</>';code.onpointerdown=()=>fx._remember();code.onclick=()=>{const range=fx._restore(),block=makeCodeBlock(range.toString());insertNoteBlocks(fx,block);fx._caret(block.querySelector('.code-text'),true)};tools.append(code);
 const undo=control('note-undo','Undo','undo'),redo=control('note-redo','Redo','redo'),insert=control('note-insert','Insert','plus');insert.append(document.createTextNode('Insert'));tools.prepend(undo,redo);tools.insertBefore($('#add-check'),$('#note-bold'));tools.insertBefore(insert,$('#note-bold'));
 undo.onclick=()=>fx._undo(false);redo.onclick=()=>fx._undo(true);
 insert.onclick=e=>{fx._remember();closeMenus();const menu=$('#context-menu');menu.innerHTML=`<button role="menuitem" data-insert="time">${ic('clock')}Insert date & time</button><button role="menuitem" data-insert="web">${ic('link')}Website hyperlink</button><button role="menuitem" data-insert="link">${ic('link')}Link to a note</button><button role="menuitem" data-insert="file">${ic('up')}Embed a file</button>`;menu.onclick=guard(async event=>{const action=event.target.closest('[data-insert]')?.dataset.insert;if(!action)return;closeMenus();menu.onclick=contextClickHandler;
  if(action==='web'){
   hideSettings();const href=await ask({title:'Website hyperlink',input:true,ph:'https://example.com',ok:'Insert link'});if(!href)return;
   let url;try{url=new URL(href)}catch{return toast('Enter a full https:// or http:// address')}
   if(!['http:','https:'].includes(url.protocol))return toast('Use an https:// or http:// address');
   const a=document.createElement('a');a.href=url.href;a.textContent=url.href;a.className='web-link';a.target='_blank';a.rel='noopener noreferrer';a.contentEditable='false';insertIntoNote(fx,a);return;
  }
  if(action==='time'){insertIntoNote(fx,document.createTextNode(new Date().toLocaleString()));return}
  if(action==='link'){await insertNoteLink(fx,item);return}
  const input=$('#embed-input');input.onchange=guard(async()=>{const files=[...input.files];input.value='';await embedFiles(files,item,fx)});input.click();
 });openPopover(menu,e.currentTarget)};
 const bar=document.createElement('div');bar.id='checklist-tools';bar.className='checklist-tools';bar.innerHTML='<span id="checklist-progress"></span><button id="hide-done" aria-pressed="false">Hide completed</button><button id="sort-done">Completed to bottom</button>';tools.after(bar);
 $('#hide-done').onclick=guard(async()=>{S.settings.checklists[item.id]=!S.settings.checklists[item.id];updateChecklist(item,fx);await saveSettings()});$('#sort-done').onclick=()=>moveCompleted(fx);
 updateUndoButtons(fx);updateChecklist(item,fx);updateFolds(fx);
 const originalPointer=fx.onpointerdown;fx.onpointerdown=e=>{if(e.target.closest('.fold-toggle,.code-copy')){fx._remember();e.preventDefault()}else originalPointer?.(e)};
 const originalClick=fx.onclick;fx.onclick=guard(async e=>{
  const copy=e.target.closest('.code-copy');if(copy){e.preventDefault();await copyCode(copy);return}
  const heading=e.target.closest('.fold-toggle');if(heading){const row=heading.closest('.note-heading');row.dataset.folded=String(row.dataset.folded!=='true');fx._caret(row.querySelector('.heading-text'),true);fx._save('fold');return}
  const link=e.target.closest('[data-note-link]');if(link){e.preventDefault();await openNote(link.dataset.noteLink);return}originalClick?.(e);
 });
 fx.ondragover=e=>{if(e.dataTransfer?.types.includes('Files')){e.preventDefault();fx.classList.add('drop-target')}};fx.ondragleave=()=>fx.classList.remove('drop-target');
 fx.ondrop=guard(async e=>{e.preventDefault();fx.classList.remove('drop-target');const files=[...(e.dataTransfer?.files||[])];if(!files.length)return;
  const range=document.caretRangeFromPoint?.(e.clientX,e.clientY);if(range&&fx.contains(range.startContainer)){const selection=getSelection();selection.removeAllRanges();selection.addRange(range);fx._remember()}
  await embedFiles(files,item,fx);
 });
 // Searching uses visual ranges and never writes search markup into saved note content.
 createFindBar();refreshFind();if(S.q&&findRanges.length)jumpFind(1);
 const oldInput=fx.oninput;fx.oninput=e=>{if(convertFencedLines(fx))fx._save('code-fence');else oldInput?.(e);refreshFind()};
 const oldPaste=fx.onpaste;fx.onpaste=e=>{
  const pre=e.target.closest('.code-text');if(pre){e.preventDefault();insertCodeText(fx,pre,e.clipboardData.getData('text/plain'));return}
  const fragment=fencedFragment(e.clipboardData.getData('text/plain'));if(fragment){e.preventDefault();insertNoteBlocks(fx,fragment);return}oldPaste?.(e);
 };
 fx.addEventListener('keydown',e=>{
  const copy=e.target.closest('.code-copy');if(copy&&['Enter',' '].includes(e.key)){e.preventDefault();e.stopPropagation();copyCode(copy);return}
  const pre=e.target.closest('.code-text');if(!pre)return;
  if(e.key==='Enter'&&(e.ctrlKey||e.metaKey)){e.preventDefault();e.stopPropagation();let blank=pre.closest('.note-code').nextElementSibling;if(!blank){blank=document.createElement('div');blank.innerHTML='<br>';pre.closest('.note-code').after(blank)}fx._caret(blank);fx._save('code');return}
  if(e.key==='Enter'||e.key==='Tab'){e.preventDefault();e.stopPropagation();insertCodeText(fx,pre,e.key==='Tab'?'  ':'\n')}
 },true);
 showBacklinks(item);
}
const contextClickHandler=$('#context-menu').onclick;
async function insertNoteLink(fx,item){
 const notes=activeItems().filter(note=>isText(note)&&note.id!==item.id);
 if(!notes.length)return toast('Create another note first');
 showSheet('Link to a note','<input class="sheet-search" id="link-search" type="search" aria-label="Find a note" placeholder="Find a note…"><div class="picker-list" id="link-options"></div>');
 const render=()=>{const q=$('#link-search').value.toLowerCase();$('#link-options').innerHTML=notes.filter(note=>title(note).toLowerCase().includes(q)).map(note=>`<button data-link-target="${note.id}">${ic('note')}${h(title(note))}</button>`).join('')||'<p class="muted">No matching notes.</p>'};render();$('#link-search').oninput=render;
 $('#link-options').onclick=e=>{const id=e.target.closest('[data-link-target]')?.dataset.linkTarget;if(!id)return;const target=notes.find(note=>note.id===id);hideSettings();if(!fx.isConnected)return;const a=document.createElement('a');a.href='#note='+id;a.className='note-link';a.dataset.noteLink=id;a.contentEditable='false';a.textContent=title(target);insertIntoNote(fx,a)};
}
function showBacklinks(item){
 const links=activeItems().filter(other=>other.id!==item.id&&isText(other)&&richOf(other)?.html.includes('data-note-link="'+item.id+'"'));
 if(!links.length)return;const bar=document.createElement('div');bar.className='backlinks';bar.innerHTML='<span>Linked from</span>'+links.map(note=>`<button data-backlink="${note.id}">${h(title(note))}</button>`).join('');$('#edit').append(bar);bar.onclick=e=>{const id=e.target.closest('[data-backlink]')?.dataset.backlink;if(id)guard(()=>openNote(id))()};
}
async function uploadOne(file,section=''){
 if(!OK_EXT.includes(ext(file.name)))throw Object.assign(new Error('Unsupported file'),{userMessage:'This file type is not supported: '+file.name});if(file.size>MAX_MB*1048576)throw Object.assign(new Error('Too large'),{userMessage:'Choose files under '+MAX_MB+' MB'});
 const data=new FormData();data.append('file',file);data.append('section',section);const item=await api('POST','/files',data);item._text=isText(item);if(isText(item))item.content=await file.text();S.items.push(item);drawSide();drawRows();return item;
}
function insertNoteBlocks(fx,fragment){
 const range=fx._restore();range.deleteContents();let line=range.startContainer.nodeType===3?range.startContainer.parentElement:range.startContainer;
 while(line&&line!==fx&&line.parentNode!==fx)line=line.parentNode;
 const last=fragment.nodeType===11?fragment.lastChild:fragment;
 if(line&&line!==fx)line.after(fragment);else fx.insertBefore(fragment,fx.childNodes[range.startOffset]||null);
 const blank=document.createElement('div');blank.innerHTML='<br>';last.after(blank);fx._caret(blank);fx._save('insert');
}
async function embedFiles(files,item,fx){
 if(!files.length)return;await flush();fx._remember();
 for(const file of files){
  if(kind(file.name)==='txt'){
   if(file.size>2*1024**2)throw Object.assign(new Error('Too large'),{userMessage:'Editable text files are limited to 2 MB'});
   if(fx.isConnected){const text=await file.text(),container=document.createElement('div');container.innerHTML=plainHTML(text);const fragment=document.createDocumentFragment();fragment.append(...container.childNodes);insertNoteBlocks(fx,fragment)}
   continue;
  }
  const uploaded=await uploadOne(file,item.section||'');if(!fx.isConnected){toast('File saved in Files');continue}
  const block=document.createElement('div');block.className='note-attachment';block.contentEditable='false';block.dataset.fileId=uploaded.id;block.dataset.name=uploaded.title;
  const type=kind(uploaded.title),url='/api/files/'+uploaded.id+'?inline=1';let preview='';
  if(type==='img')preview=`<img src="${url}" alt="${h(uploaded.title)}" loading="lazy">`;
  else if(type==='vid')preview=`<video src="${url}" controls playsinline preload="metadata"></video>`;
  else if(type==='aud')preview=`<audio src="${url}" controls preload="metadata"></audio>`;
  else if(type==='pdf')preview=`<iframe src="${url}" title="Attached PDF" sandbox loading="lazy"></iframe>`;
  block.innerHTML=preview+`<span class="attachment-caption"><span class="attachment-label">${h(uploaded.title)} · ${h(size(uploaded.size))}</span><a href="/api/files/${uploaded.id}?download=1" target="_blank" rel="noopener noreferrer">Open / download</a></span>`;
  insertNoteBlocks(fx,block);
 }
 toast('Added to note');
}

/* Search matching and next/previous navigation. */
function createFindBar(){if($('#find-note'))return;const bar=document.createElement('div');bar.id='find-note';bar.className='find-note hide';bar.innerHTML='<span id="find-count"></span><button id="find-prev" aria-label="Previous match">↑</button><button id="find-next" aria-label="Next match">↓</button>';$('.note-tools')?.after(bar);$('#find-prev').onclick=()=>jumpFind(-1);$('#find-next').onclick=()=>jumpFind(1)}
function findTextRanges(fx,q){
 const parts=[],walker=document.createTreeWalker(fx,NodeFilter.SHOW_TEXT);let text,joined='',previous=null;
 while(text=walker.nextNode()){
  if(text.parentElement.closest('.check-toggle,.fold-toggle,.note-attachment')||!text.length)continue;
  let block=text.parentElement;while(block.parentNode&&block.parentNode!==fx&&block!==fx)block=block.parentElement;
  if(previous&&block!==previous)joined+='\n';previous=block;
  const start=joined.length;joined+=text.textContent;parts.push({node:text,start,end:joined.length});
 }
 if(!parts.length||!q)return [];
 const locate=offset=>{let low=0,high=parts.length-1;while(low<high){const mid=(low+high)>>1;if(parts[mid].end<=offset)low=mid+1;else high=mid}return parts[low]};
 const expression=new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'giu'),ranges=[];let match;
 while(match=expression.exec(joined)){const start=locate(match.index),end=locate(match.index+match[0].length-1),range=document.createRange();range.setStart(start.node,match.index-start.start);range.setEnd(end.node,match.index+match[0].length-end.start);ranges.push(range)}
 return ranges;
}
function refreshFind(){
 const fx=$('#fx'),bar=$('#find-note');findRanges=[];findIndex=-1;
 if(window.CSS?.highlights){CSS.highlights.delete('vault-find');CSS.highlights.delete('vault-find-current')}
 if(!fx||!bar)return;const q=S.q.trim().toLowerCase();bar.classList.toggle('hide',!q);if(!q)return;
 findRanges=findTextRanges(fx,q);
 if(window.CSS?.highlights&&window.Highlight)CSS.highlights.set('vault-find',new Highlight(...findRanges));
 $('#find-count').textContent=findRanges.length+' match'+(findRanges.length===1?'':'es');$('#find-prev').disabled=$('#find-next').disabled=!findRanges.length;
}
function jumpFind(delta){
 if(!findRanges.length)return;findIndex=(findIndex+delta+findRanges.length)%findRanges.length;const range=findRanges[findIndex];
 const fx=$('#fx');fx.classList.remove('hide-completed');for(const heading of fx.querySelectorAll('.note-heading'))heading.dataset.folded='false';updateFolds(fx);
 const parent=range.startContainer.parentElement;parent.scrollIntoView({block:'center',behavior:'smooth'});
 if(window.CSS?.highlights&&window.Highlight)CSS.highlights.set('vault-find-current',new Highlight(range));else{getSelection().removeAllRanges();getSelection().addRange(range.cloneRange())}
 $('#find-count').textContent=(findIndex+1)+' / '+findRanges.length;
}

/* Editor chrome and session hygiene. */
drawEdit=function(){
 clearTimeout(passwordRevealTimer);findRanges=[];
 const item=S.items.find(i=>i.id===S.sel),e=$('#edit');
 if(item?.deleted_at){disposeRichEditor?.();disposeRichEditor=null;closeMenus();e.innerHTML=`<header class="editor-head"><button id="trash-back" class="icon-button back" aria-label="Back to items">${ic('back')}</button><div class="crumb">Trash</div></header><div class="editor-empty"><div class="empty-icon">${ic('trash')}</div><h2>${h(title(item))}</h2><p>Moved to Trash ${h(stamp(item.deleted_at))}.<br>Automatically deleted after 30 days.</p><div class="trash-actions"><button id="restore-current" class="primary">Restore</button><button id="purge-current" class="secondary">Delete permanently</button></div></div>`;
  $('#trash-back').onclick=backToList;$('#restore-current').onclick=guard(()=>restoreItems([item.id]));$('#purge-current').onclick=guard(()=>purgeItem(item));addSettingsButtons();return;
 }
 if(item?.type==='password'&&!item._detail){e.innerHTML='<div class="editor-empty">Opening password…</div>';const id=item.id;guard(async()=>{const full=await api('GET','/items/'+id);Object.assign(item,full,{_detail:true});if(S.sel===id)drawEdit()})();return}
 baseDrawEdit();addSettingsButtons();
 if(!item){if(S.focus)toggleFocus(false);return}
 const head=$('.editor-head'),focus=document.createElement('button');focus.id='focus-mode';focus.className='icon-button';focus.setAttribute('aria-label',S.focus?'Exit focus mode':'Focus mode');focus.title=S.focus?'Exit focus mode':'Focus mode';focus.innerHTML=ic('focus');focus.onclick=()=>toggleFocus();head.insertBefore(focus,head.querySelector('.settings-open'));
 if(item.type==='password'){
  const original=$('#sw').onclick;$('#sw').onclick=e=>{original(e);clearTimeout(passwordRevealTimer);if($('#pw').type==='text')passwordRevealTimer=setTimeout(()=>{const pw=$('#pw');if(pw){pw.type='password';$('#sw').textContent='Show';$('#sw').setAttribute('aria-label','Show password')}},30000)};
 }
};
function toggleFocus(value=!S.focus){S.focus=!!value;$('#app').classList.toggle('focus',S.focus);const button=$('#focus-mode');if(button){button.setAttribute('aria-label',S.focus?'Exit focus mode':'Focus mode');button.title=S.focus?'Exit focus mode':'Focus mode'}if(S.focus)pane('edit')}
function setupAppFeatures(){
 S.selected=new Set();S.settings=S.settings||{profiles:[{id:'default',name:'Default',color:'#bcb7ff'}],active:'default',sort:'edited',checklists:{}};applyTheme(true);
 const input=document.createElement('input');input.type='file';input.id='embed-input';input.multiple=true;input.accept=OK_EXT.map(e=>'.'+e).join(',');input.className='hide';document.body.append(input);
 $('#fi').onchange=guard(async()=>{const files=[...$('#fi').files];$('#fi').value='';if(S.view==='trash')return toast('Leave Trash to upload files.');if(!files.length)return;await flush();let last;for(const file of files)last=await uploadOne(file,S.view.startsWith('s:')?S.view.slice(2):'');if(last)await openNote(last.id);toast(files.length+' file'+(files.length===1?'':'s')+' uploaded')});
 addEventListener('dragover',e=>{if(e.dataTransfer?.types.includes('Files'))e.preventDefault()});
 addEventListener('drop',guard(async e=>{if(e.target.closest('#fx'))return;const files=[...(e.dataTransfer?.files||[])];if(!files.length)return;e.preventDefault();if($('#app').classList.contains('hide'))return;if(S.view==='trash')return toast('Leave Trash to upload files.');await flush();for(const file of files)await uploadOne(file,S.view.startsWith('s:')?S.view.slice(2):'');toast('Files saved')}));
 $('#rows').ondragstart=e=>{const row=e.target.closest('[data-row-id]');if(row&&S.settings.sort==='manual'){S.dragId=row.dataset.rowId;e.dataTransfer.setData('application/x-vault-item',S.dragId)}};
 $('#rows').ondragover=e=>{if(S.dragId)e.preventDefault()};$('#rows').ondrop=guard(async e=>{if(!S.dragId)return;e.preventDefault();const row=e.target.closest('[data-row-id]'),id=S.dragId;S.dragId=null;if(!row||row.dataset.rowId===id)return;const ids=visible().map(i=>i.id).filter(i=>i!==id);ids.splice(ids.indexOf(row.dataset.rowId),0,id);await saveManual(ids)});$('#rows').ondragend=()=>S.dragId=null;
 addEventListener('keydown',e=>{if(e.key==='Escape'&&S.focus&&!$('#feature-dialog')?.open&&$('#dlg').classList.contains('hide')&&[...document.querySelectorAll('.popover')].every(el=>el.classList.contains('hide')))toggleFocus(false)},{capture:true});
 setInterval(()=>{if(csrf&&!$('#app').classList.contains('hide')&&document.visibilityState==='visible'&&Date.now()-lastInteraction<240000)guard(()=>api('POST','/session/touch',{}))()},60000);
 addEventListener('hashchange',()=>{const id=location.hash.slice(6);if(location.hash.startsWith('#note=')&&VAULT_ID.test(id)&&!$('#app').classList.contains('hide'))guard(()=>openNote(id,false))()});
}

/* No metric/approval polling while hidden; resume only the open visible panel. */
document.addEventListener('visibilitychange',()=>{
 clearTimeout(resourceTimer);clearTimeout(approvalTimer);
 if(document.visibilityState==='visible'&&!$('#app').classList.contains('hide')){resourceResume?.();approvalResume?.()}
});
addEventListener('pagehide',()=>{clearTimeout(resourceTimer);clearTimeout(approvalTimer)});
addEventListener('pageshow',e=>{if(e.persisted&&document.visibilityState==='visible'&&!$('#app').classList.contains('hide')){resourceResume?.();approvalResume?.()}});
