/* =====================================================================
   NOWILLS SQC ERP — Application core
   Shell • routing • engines • drawers • security • live engine • PWA
   ===================================================================== */
(function(){
'use strict';
const D=DATA; const $=s=>document.querySelector(s), $$=s=>Array.from(document.querySelectorAll(s));
const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

/* ---------------- Icons ---------------- */
const IC={
 dash:'M3 3h7v9H3zM14 3h7v5h-7zM14 12h7v9h-7zM3 16h7v5H3z',
 quarry:'M3 20L9 6l4 8 3-5 5 11z',
 drill:'M12 2v6M8 8h8l-1 12H9zM10 20v2M14 20v2',
 crush:'M4 4h16M6 4l4 8h4l4-8M8 12v6h8v-6M9 21h6',
 scale:'M4 7h16v4H4zM9 11v6h6v-6M7 21h10M12 17v4',
 pile:'M3 15l4.5-9 4.5 9zM13 15l4.5-9 4.5 9zM7.5 21l4.5-7 4.5 7z',
 tag:'M12 2H2v10l9 9 9-9zM7 7h.01',
 hardhat:'M4 18h16v-2a8 6 0 0 0-6-5.9V8a2 2 0 0 0-4 0v2.1A8 6 0 0 0 4 16zM2 18h20',
 doc:'M7 3h7l5 5v13H7zM14 3v5h5M10 12h6M10 16h6',
 cert:'M12 3a5 5 0 1 1 0 10 5 5 0 0 1 0-10zM8.5 12.5L6.5 21l5.5-3 5.5 3-2-8.5',
 site:'M3 21h18M5 21V7l7-4 7 4v14M9 21v-6h6v6',
 exc:'M2 17h6l2-6 6-2 4-4 2 2-3 5h-5l-3 5zM5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM14 21h6',
 truck:'M1 8h13v9H1zM14 11h4l3 3v3h-7zM5.5 20a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6zM17.5 20a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6z',
 fuel:'M5 21V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v16M3 21h12M13 9h3a2 2 0 0 1 2 2v6a1.5 1.5 0 0 0 3 0V8l-3-3',
 wrench:'M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4L14 13l-3-3 3.7-3.7z',
 cart:'M9 21a1.3 1.3 0 1 0 0-2.6A1.3 1.3 0 0 0 9 21zM18 21a1.3 1.3 0 1 0 0-2.6 1.3 1.3 0 0 0 0 2.6zM2 3h3l2.6 12h10L21 7H6',
 wh:'M3 21V9l9-6 9 6v12M3 21h18M7 21v-8h10v8M7 17h10',
 users:'M9 11.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 20c0-3.5 2.7-6 6-6s6 2.5 6 6M16 4.3a3.5 3.5 0 0 1 0 6.4M18.5 14.3c2.2.8 3.5 2.8 3.5 5.7',
 hr:'M12 11.5a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21c0-4 3.6-7.5 8-7.5s8 3.5 8 7.5',
 bank:'M3 10l9-7 9 7M4 10v9M8 10v9M12 10v9M16 10v9M20 10v9M2 19h20',
 cash:'M2 7h20v11H2zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM5 10h.01M19 15h.01',
 budget:'M4 20h16M6.5 20v-6M11 20V8M15.5 20v-9M20 20V5',
 pct:'M19 5L5 19M8 8.5A1.5 1.5 0 1 0 8 5.5a1.5 1.5 0 0 0 0 3zM16 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',
 shield:'M12 2l8 3v6c0 5-3.5 8.5-8 11-4.5-2.5-8-6-8-11V5zM9 12l2 2 4-4',
 leaf:'M12 21c-5.2 0-8-3.2-8-8.2C4 7 9.5 3.6 16 3c.9 6.5-1 11.9-4.2 14.4M12 21c0-6.5 2.4-10.6 6.5-13.5',
 docs:'M6 2h8l5 5v15H6zM14 2v5h5M9 12h8M9 16h5',
 report:'M3 3v18h18M8 17v-5M13 17V8M18 17v-8',
 ai:'M12 3l1.7 4.6 4.6 1.7-4.6 1.7L12 15.6l-1.7-4.6L5.7 9.3l4.6-1.7zM19 14.5l.9 2.3 2.3.9-2.3.9-.9 2.3-.9-2.3-2.3-.9 2.3-.9zM5 15.5l.7 1.9 1.9.7-1.9.7L5 20.7l-.7-1.9-1.9-.7 1.9-.7z',
 gear:'M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1',
 search:'M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM21 21l-4.4-4.4',
 bell:'M18 8a6 6 0 1 0-12 0c0 7-3 8-3 8h18s-3-1-3-8M10.3 21a2 2 0 0 0 3.4 0',
 check:'M9 11l3 3 8-8M20 12v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h9',
 wifi:'M5 12.5a11 11 0 0 1 14 0M8.5 16a6.5 6.5 0 0 1 7 0M12 19.5h.01M2 9a16 16 0 0 1 20 0',
 plus:'M12 5v14M5 12h14', sun:'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v3M12 20v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M1 12h3M20 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1',
 moon:'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z',
 expand:'M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3M3 12h18',
 help:'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01',
 user:'M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM5 21c0-3.9 3.1-6.5 7-6.5s7 2.6 7 6.5',
 x:'M18 6L6 18M6 6l12 12', print:'M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 14h12v8H6z',
 download:'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
 mail:'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6',
 share:'M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7M16 6l-4-4-4 4M12 2v14',
 refresh:'M23 4v6h-6M1 20v-6h6M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15',
 clock:'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2',
 filter:'M22 3H2l8 9.5V19l4 2v-8.5z',
 menu:'M4 6h16M4 12h16M4 18h16', star:'M12 2.5l2.9 6 6.6.9-4.8 4.5 1.2 6.6-5.9-3.2-5.9 3.2 1.2-6.6L2.5 9.4l6.6-.9z',
 chev:'M9 6l6 6-6 6', grid:'M3 3h8v8H3zM13 3h8v5h-8zM13 12h8v9h-8zM3 15h8v6H3z',
 bolt:'M13 2L4 14h6l-1 8 9-12h-6l1-8z', eye:'M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
 flow:'M4 4h6v6H4zM14 14h6v6h-6zM10 7h5v4M7 10v5h4M13 18h-4', sliders:'M4 7h8M18 7h2M4 17h2M10 17h10M14 5a2 2 0 1 0 4 0 2 2 0 0 0-4 0zM6 15a2 2 0 1 0 4 0 2 2 0 0 0-4 0z',
 play:'M6 4l14 8-14 8z', save:'M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2zM17 21v-8H7v8M7 3v5h8'};
function icon(n,s){return '<svg viewBox="0 0 24 24" width="'+(s||18)+'" height="'+(s||18)+'" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="'+(IC[n]||IC.doc)+'"/></svg>';}

/* ---------------- State ---------------- */
const store=(k,v)=>{try{if(v===undefined)return JSON.parse(localStorage.getItem('nw_'+k)||'null');localStorage.setItem('nw_'+k,JSON.stringify(v));}catch(e){return null;}};
const state=Object.assign({theme:'dark',roleName:'Group Chief Executive Officer',co:'grp',qry:'all',prj:'all',br:'all',period:'This Month',cur:'GHS',lang:'en',offline:false,favs:['exec','quarry','projects'],recent:['exec'],customize:false,sec:{mfa:1,dev:1,to:1,audit:1,wm:0,mask:0,ip:0,dl:1},cmp:{co:[],qry:[],prj:[]},presets:store('presets')||[]},store('state')||{});
Object.assign(state,store('state')||{});
/* NOWILLS-LARAVEL: server is authoritative for identity & currency default */
if(window.__SERVER){state.roleName=__SERVER.role;if(__SERVER.currency)state.cur=__SERVER.currency;}
function role(){if(window.__SERVER&&__SERVER.modules)return {n:__SERVER.role,mods:__SERVER.modules.slice(),mask:0,ro:0};return D.ROLES.find(r=>r.n===state.roleName)||D.ROLES[1];}
function save(){store('state',state);}
document.documentElement.dataset.theme=state.theme;

/* ---------------- NWX public API ---------------- */
const NWX=window.NWX={state,icon};
NWX.t=k=>{const l=D.I18N[state.lang]||{};if(l[k])return l[k];if(D.I18N.en[k])return D.I18N.en[k];const def=(D.MODULES_DEF||[]).find(m=>m[0]===k);return def?def[1]:k;};
function cur(){return D.CURRENCIES.find(c=>c.c===state.cur)||D.CURRENCIES[0];}
NWX.fmtM=v=>{ if(v==null||isNaN(v))return '—'; const c=cur(); const x=v/c.r; const a=Math.abs(x);
 if(a>=1e9)return c.s+(x/1e9).toFixed(2)+'B'; if(a>=1e6)return c.s+(x/1e6).toFixed(2)+'M';
 if(a>=1e3)return c.s+(x/1e3).toFixed(1)+'k'; return c.s+Math.round(x*100)/100;};
Charts.setFormatters(NWX.fmtM);
NWX.scope=()=>{ const SL=(window.__SERVER||{}).scopeLists||{};
 const find=(l,id)=>((l||[]).find(x=>String(x.id)===String(id))||{});
 const co=Object.assign({id:'grp',n:'Group (Consolidated)'},find(SL.companies,state.co));
 const q=Object.assign({id:'all',n:'All quarries'},find(SL.quarries,state.qry));
 const b=Object.assign({id:'all',n:'All branches'},find(SL.branches,state.br));
 const pj=(state.prj&&state.prj!=='all')?(find(SL.projects,state.prj).n||state.prj):null;
 const parts=[co.id!=='grp'?co.n:null,q.id!=='all'?q.n:null,b.id!=='all'?b.n:null,pj].filter(Boolean);
 return {f:1,co:String(co.id||'grp'),qry:String(q.id||'all'),br:String(b.id||'all'),prj:String(state.prj||'all'),label:parts.length?parts.join(' · '):'Group · all quarries · all branches'}; };

/* ---------------- Toast / Modal / Drawer ---------------- */
NWX.toast=(t,s,kind)=>{ const d=document.createElement('div'); d.className='toast '+(kind||'');
 d.innerHTML='<div style="flex:1"><b>'+esc(t)+'</b><small>'+esc(s||'')+'</small></div>'; $('#toasts').appendChild(d);
 setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .4s';setTimeout(()=>d.remove(),400);},kind==='red'?6500:4200);};
NWX.modal=(title,html,mount,okLabel)=>{ const ov=document.createElement('div'); ov.className='modal-ov';
 ov.innerHTML='<div class="modal"><div class="modal-h">'+icon('bolt')+'<h3>'+esc(title)+'</h3><button class="iconbtn x">'+icon('x')+'</button></div><div class="modal-b">'+html+'</div>'+(okLabel?'<div class="modal-f"><button class="btn ghost mclose">'+NWX.t('close')+'</button><button class="btn pri mok">'+esc(okLabel)+'</button></div>':'')+'</div>';
 document.body.appendChild(ov); const close=()=>ov.remove();
 ov.onclick=e=>{if(e.target===ov)close();}; ov.querySelector('.x').onclick=close;
 const mc=ov.querySelector('.mclose'); if(mc)mc.onclick=close;
 const ok=ov.querySelector('.mok'); if(ok)ok.onclick=()=>{close();NWX.toast('Done',title+' — committed to ERP with audit trail.','green');NWX.audit('Modal action',title);};
 if(mount)mount(ov.querySelector('.modal-b'));
 return ov;};
let openDrawer=null;
NWX.drawer=(id,title,ico,html,mount)=>{ closeDrawer();
 const d=document.createElement('div'); d.className='drawer'; d.id='dw-'+id;
 d.innerHTML='<div class="drawer-h">'+icon(ico||'grid')+'<h3>'+esc(title)+'</h3><button class="tbtn x">'+icon('x')+'</button></div><div class="drawer-b"></div>';
 d.querySelector('.drawer-b').innerHTML=html; document.body.appendChild(d); $('#backdrop').classList.add('on');
 requestAnimationFrame(()=>d.classList.add('on')); openDrawer=d;
 d.querySelector('.x').onclick=closeDrawer; if(mount)mount(d.querySelector('.drawer-b'));
 return d;};
function closeDrawer(){ if(openDrawer){openDrawer.classList.remove('on');const od=openDrawer;setTimeout(()=>od.remove(),250);openDrawer=null;} $('#backdrop').classList.remove('on');}
$('#backdrop').onclick=closeDrawer;

/* ---------------- Audit ---------------- */
if(!window.NWLOG){window.NWLOG=[{a:'2026-08-10 08:41',b:'J. Nowills',c:'Group CEO',d:'Dashboard viewed',e:'Group · Executive',f:'MBP-16 · 102.89.14.7',g:'c-01J','h':'9f2e'},{a:'2026-08-10 08:44',b:'A. Mensah',c:'COO',d:'Chart drilled down — Revenue Trend',e:'NQL · Shai Hills',f:'HQ-PC-021 · 102.89.14.2',g:'c-01K','h':'71ac'},{a:'2026-08-10 08:47',b:'R. Asante',c:'Fin Controller',d:'Data exported — AR aging',e:'Group',f:'FIN-045 · 102.89.15.9',g:'c-01L','h':'c448'},{a:'2026-08-10 08:52',b:'S. Adjei',c:'Site Engineer',d:'Offline capture queued — site diary',e:'NCL · P-2221',f:'Tab-A8 (offline)',g:'c-01M','h':'3d91'},{a:'2026-08-10 08:58',b:'System',c:'—',d:'Hash-chain checkpoint anchored',e:'Cluster acc-erp-01',f:'srv',g:'c-01N','h':'aa02'}];}
NWX.audit=(action,scope)=>{ const prev=NWLOG[NWLOG.length-1]; let h=2166136261; const s2=(prev?prev.h:'')+action+Date.now();
 for(let i=0;i<s2.length;i++){h^=s2.charCodeAt(i);h=Math.imul(h,16777619);}
 NWLOG.push({a:new Date().toISOString().slice(0,16).replace('T',' '),b:'J. Nowills',c:state.roleName,d:action,e:scope||NWX.scope().label,f:'This device · 102.89.14.7',g:'c-'+Math.random().toString(36).slice(2,6).toUpperCase(),h:(h>>>0).toString(36)});};

/* ---------------- drill-down / entities ---------------- */
NWX.drill=p=>{ NWX.audit('KPI drilled down',p.title+' / '+(p.label||''));
 const rnd=D.mulberry(D.hseed((p.title||'')+(p.label||'')));
 const isMoney=p.fmt==='money'; const total=p.value||(1200000*(0.5+rnd()));
 const ents=['GH Karim & Partners','Volta Infra','Adenta Dev','Kpone LP','KMA / AshGold','Goldline Civil','Tema Port Exp.','Min. of Roads'];
 let rows='<table class="tbl"><thead><tr><th>Doc №</th><th>Date</th><th>Entity</th><th>Reference</th><th class="num">Qty (t)</th><th class="num">Amount</th><th>Status</th></tr></thead><tbody>';
 let rem=total; for(let i=0;i<9;i++){ const v=i===8?rem:Math.round(total*(0.06+rnd()*0.18)); rem-=v;
  rows+='<tr><td class="mono">TX-'+(77000+Math.floor(rnd()*9000))+'</td><td>2026-08-'+String(1+Math.floor(rnd()*9)).padStart(2,'0')+'</td><td>'+ents[Math.floor(rnd()*ents.length)]+'</td><td>'+['Invoice','Weighbridge ticket','Journal','Receipt','Certificate','Dispatch note'][Math.floor(rnd()*6)]+'</td><td class="num">'+(isMoney?'—':Math.round(40+rnd()*900).toLocaleString())+'</td><td class="num">'+(isMoney?NWX.fmtM(v):Math.round(v).toLocaleString())+'</td><td><span class="badge '+(rnd()>0.25?'green':'amber')+'">'+(rnd()>0.25?'Posted':'Open')+'</span></td></tr>'; }
 rows+='</tbody></table>';
 NWX.modal('Drill-down — '+esc(p.title)+(p.label?' · '+esc(p.label):''),
  '<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px"><span class="chip on">Live subledger</span><span class="chip">✓ reconciled to GL</span><span class="chip">✓ '+(p.dimension||'source documents')+'</span><span class="chip">'+NWX.scope().label+'</span><span style="margin-left:auto"><button class="btn sm" id="ddcsv">'+icon('download',14)+' CSV</button></span></div>'+rows+
  '<div style="margin-top:10px;color:var(--muted);font-size:11.5px">9 of 1,214 matching records shown · open the owning module for full pagination, bulk actions and approvals.</div>',
  b=>{ b.querySelector('#ddcsv').onclick=()=>Charts.csv('drilldown.csv',[['Doc','Date','Entity','Ref','Qty','Amount','Status']].concat(Array.from(b.querySelectorAll('tbody tr')).map(tr=>Array.from(tr.children).map(td=>td.textContent)))); });};
NWX.rowDrill=(cfg,row)=>{ const t=row.a||row.no||'Record'; NWX.drill({title:cfg.id+' — '+t,label:row.b||row.e||'',value:row.d||row.g||120000,fmt:'money'}); };
NWX.entity=(kind,obj)=>{ NWX.audit('Entity opened',kind+': '+obj.label);
 NWX.modal((obj.label||kind),'<div class="statgrid">'+[['Type',kind.toUpperCase()],['Scope',NWX.scope().label],['Reference',obj.tag||obj.id||'—'],['Status',obj.status?'Alert':'Normal']].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="font-size:12.5px">'+esc(x[1])+'</b></div>').join('')+'</div><p style="color:var(--muted);font-size:12.5px">'+(esc(obj.detail||'Live record from the ERP spine. Follow linked module for transactions, documents and history.'))+'</p>',null,'Open owning module');};
NWX.ganttMoved=row=>{NWX.toast('Schedule re-baselined',row.name+' — forecast finish '+row.ae+' (change request CR auto-raised).','blue');NWX.audit('Gantt bar moved',row.name+' → '+row.ae);};
NWX.riskItem=(it,i)=>{NWX.modal('Risk #'+(i+1)+' — '+it.label,'<div class="statgrid">'+[['Likelihood','█'.repeat(it.l)],['Impact','█'.repeat(it.i)],['Score',(it.l*it.i)+' / 25'],['Owner',['E. Osei','R. Asante','A. Mensah'][i%3]]].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="font-size:12px">'+x[1]+'</b></div>').join('')+'</div><p style="font-size:12.5px;color:var(--muted)">Controls: weekly review · linked alerts · AI monitoring. Escalate via Exception Centre.</p>',null,'Raise action');};
NWX.alertOpen=r=>{ NWX.modal(r.no+' — '+r.src,'<p style="margin-top:0">'+esc(r.desc)+'</p><div class="hrow"><span class="nm">Severity</span><b><span class="badge '+(r.sev==='critical'||r.sev==='high'?'red':r.sev==='medium'?'amber':'grey')+'">'+r.sev.toUpperCase()+'</span></b></div><div class="hrow"><span class="nm">Company / site</span><b>'+r.co+' · '+r.site+'</b></div><div class="hrow"><span class="nm">Owner</span><b>'+r.own+'</b></div><div class="hrow"><span class="nm">Due</span><b>'+r.due+'</b></div><div class="hrow"><span class="nm">Escalation</span><b>'+r.esc+'</b></div><div class="hrow"><span class="nm">Status</span><b>'+r.st+'</b></div>',b=>{
   const foot=document.createElement('div');foot.className='modal-f';foot.innerHTML='<button class="btn ghost">Assign</button><button class="btn" >Acknowledge</button><button class="btn">Escalate</button><button class="btn pri">Resolve</button>';
   b.parentElement.appendChild(foot); foot.querySelectorAll('button').forEach(btn=>btn.onclick=()=>{ r.st=btn.textContent==='Resolve'?'Resolved':btn.textContent; NWX.toast('Alert updated',r.no+' → '+r.st,'green'); NWX.audit('Alert '+r.st.toLowerCase(),r.no); NWX.bumpBadges(); b.closest('.modal-ov').remove(); }); });};

/* ---------------- Live plumbing ---------------- */
let LIVE_FNS=[];
NWX.addLive=fn=>LIVE_FNS.push(fn);
NWX.refreshKpis=()=>{ renderCards($('#kpis'),window._mod.cards); };
NWX.bumpBadges=()=>{ const open=NWA.filter(a=>a.st!=='Resolved').length;
 const pend=(window.NWAPPROVE||[]).filter(a=>a.st==='Pending').length;
 const nb=$('#bNotif'); if(nb){nb.textContent=open>99?'99+':open; nb.style.display=open?'':'none';}
 const tb=$('#bTasks'); if(tb){tb.textContent=pend; tb.style.display=pend?'':'none';}
 const bn=$('#bnn'); if(bn){bn.textContent=open>9?'9+':open; bn.style.display=open?'':'none';}
 $$('.mi .bdg.js-alert').forEach(x=>x.textContent=Math.max(1,Math.round(open/6)));};

/* ---------------- Sidebar ---------------- */
function renderSidebar(){ const nav=$('#sbNav'); const favs=state.favs||[]; const rec=state.recent||[];
 const roleObj=role();
 const vis=D.MODULES_DEF.filter(m=>roleObj.mods.includes(m[0]));
 let h='';
 if(favs.length){ h+='<div class="sb-sec">★ Favourites</div>'+vis.filter(m=>favs.includes(m[0])).map(mi).join(''); }
 if(rec.length){ h+='<div class="sb-sec">Recent</div>'+vis.filter(m=>rec.slice(0,4).includes(m[0])&&!favs.includes(m[0])).slice(0,4).map(mi).join(''); }
 let lastG='';
 vis.forEach(m=>{ if(m[3]!==lastG){h+='<div class="sb-sec">'+m[3]+'</div>';lastG=m[3];} h+=mi(m); });
 nav.innerHTML=h;
 nav.querySelectorAll('.mi').forEach(el=>{ el.onclick=e=>{ if(e.target.classList.contains('star'))return; go(el.dataset.id); closeDrawer(); if(window.innerWidth<861)document.body.classList.remove('sb-open'); };
  const st=el.querySelector('.star'); if(st)st.onclick=e=>{e.stopPropagation(); const id=el.dataset.id; const i2=state.favs.indexOf(id);
   if(i2>=0)state.favs.splice(i2,1);else state.favs.unshift(id); save(); renderSidebar(); NWX.audit('Favourite toggled',id);}; });
 function mi(m){ const active=state.mod===m[0]; const isFav=state.favs.includes(m[0]);
  const bdg=m[0]==='hse'?'<span class="bdg red js-alert">1</span>':m[0]==='alerts'?'<span class="bdg red">'+'</span>':['ipc','proc','maint'].includes(m[0])?'<span class="bdg js-alert">'+(m[0]==='ipc'?9:m[0]==='proc'?17:26)+'</span>':'';
  return '<div class="mi'+(active?' active':'')+'" data-id="'+m[0]+'" title="'+esc(m[1])+'"><span class="ic">'+icon(m[2],19)+'</span><span class="lbl">'+esc(NWX.t(m[0]))+'</span>'+bdg+'<span class="star'+(isFav?' on':'')+'">'+(isFav?'★':'☆')+'</span></div>'; }}
$('#sbSearch').oninput=e=>{ const q=e.target.value.toLowerCase(); $$('#sbNav .mi').forEach(m=>m.style.display=m.textContent.toLowerCase().includes(q)?'':'none'); $$('#sbNav .sb-sec').forEach(s2=>s2.style.display=q?'none':''); if(!q)renderSidebar(); };
$('#burger').onclick=()=>document.body.classList.toggle('sb-open');
$('#sbCollapse').onclick=()=>{ $('#app').classList.toggle('sb-collapsed'); };
function go(id){ const roleObj=role();
 if(!roleObj.mods.includes(id)){NWX.toast('Restricted','Your role «'+roleObj.n+'» has no access to this module. Ask your System Administrator.','red');NWX.audit('Access denied',id);return;}
 location.hash='#/'+id; renderModule(id); }
NWX.go=go;
NWX.remount=function(){try{renderModule(state.mod);}catch(e){console.warn('remount',e);}};

/* ---------------- KPI cards ---------------- */
function fmtKpi(v,u){ if(u==='GHS')return NWX.fmtM(v); if(u==='%')return (Math.round(v*10)/10)+'%'; if(u==='t')return Charts? (Math.abs(v)>=1000?(v/1e3).toFixed(1)+'k t':v+' t'):v; if(u==='text')return v; if(u==='#')return Math.round(v).toLocaleString(); if(u==='h')return v+' h'; if(u==='L')return Math.abs(v)>=1000?(v/1e3).toFixed(1)+'k L':v+' L'; if(u==='num')return Math.abs(v)>=10000?(v/1e3).toFixed(1)+'k':v.toLocaleString(); return v; }
function renderCards(host,cards){ if(!host)return;
 host.innerHTML=cards.map((k,i)=>{ const up=k.delta>=0; const good=(k.good||'up')==='up'?(up):( !up);
  const achW=k.ach!=null?Math.min(100,Math.max(0,k.ach)):null;
  const masked=state.sec.mask&&role().mask&&k.u==='GHS';
  return '<div class="kpi s-'+k.st+(masked?' kpi-masked':'')+'" data-i="'+i+'">'+
  '<div class="kpi-top"><span class="kpi-title">'+esc(k.t)+'</span><span class="kpi-ic">'+(k.url?'<span title="Open workspace" style="font-size:13px">\u2197</span>':icon('chev',16))+'</span></div>'+
  '<div class="kpi-val">'+fmtKpi(k.v,k.u)+(k.u==='text'?'':'')+'</div>'+
  '<div class="kpi-sub">'+(k.u==='text'?'':('<span class="delta '+(up?'up':'dn')+'">'+(up?'▲':'▼')+' '+Math.abs(k.delta)+'%</span> <span>'+NWX.t('vsPrev')+': '+fmtKpi(k.prev,k.u)+'</span>'))+(k.info?' <span title="info" style="cursor:help">ⓘ '+esc(k.info)+'</span>':'')+'</div>'+
  '<div class="kpi-spark" data-spark="'+i+'"></div>'+
  (k.tgt!=null?'<div class="kpi-tg"><div class="tr"><i class="'+(k.ach>=95?'':k.ach>=80?'warn':'bad')+'" style="width:'+achW+'%"></i></div><em>'+NWX.t('target')+' '+fmtKpi(k.tgt,k.u)+(k.ach!=null?' · '+k.ach+'%':'')+'</em></div>':'<div class="kpi-tg"><em style="color:var(--faint)">No target set</em></div>')+
  '</div>'; }).join('');
 host.querySelectorAll('.kpi').forEach((el,i)=>{ const k=cards[i];
  Charts.spark(el.querySelector('[data-spark]'),k.sp, k.st==='red'?'#ef4444':k.st==='amber'?'#f59e0b':k.st==='blue'?'#3b82f6':k.st==='green'?'#22c55e':'#8b93a3');
  if(k.url)el.style.cursor='pointer';
  el.onclick=()=>{ if(k.url){ location.href=k.url; return; } NWX.drill({title:k.t,label:NWX.scope().label,value:k.u==='GHS'?k.v/12:(k.u==='t'?k.v*200:1200000),fmt:k.u==='GHS'?'money':'num',dimension:'KPI • '+k.t}); }; });}

/* ---------------- Sections & panels ---------------- */
function sectionPanel(sec,modId){ const p=document.createElement('section'); p.className='panel '+(sec.cls||''); p.dataset.sid=sec.id; p.draggable=!!state.customize;
 p.innerHTML='<div class="panel-h" '+(state.customize?'style="cursor:grab"':'')+'><h3>'+esc(sec.ti)+'</h3>'+(sec.live?'<span class="live-tag">LIVE</span>':'')+'<span class="hint">'+(sec.hint||'')+'</span><span class="panel-t">'+
  (sec.k==='chart'?'<button class="iconbtn" data-t="csv" title="Export CSV">'+icon('download',14)+'</button><button class="iconbtn" data-t="png" title="Export PNG">'+icon('print',14)+'</button>':'')+
  '<button class="iconbtn" data-t="big" title="Full screen">'+icon('expand',14)+'</button>'+
  (state.customize?'<button class="iconbtn" data-t="hide" title="Remove widget">'+icon('x',14)+'</button>':'')+
  '</span></div><div class="panel-b"></div>';
 const body=p.querySelector('.panel-b'); NWX._charts=NWX._charts||{};
 if(sec.k==='chart'){ const holder=document.createElement('div'); body.appendChild(holder);
  sec.spec.title=sec.ti; sec.spec.el=holder; const inst=Charts.render(holder,sec.spec); NWX._charts[sec.id]={inst,spec:sec.spec,holder,sec};
 } else if(sec.k==='table'){ mountTable(body,sec.cfg);
 } else { const m=MOUNT[sec.id]; if(m)m(body,sec,modId); else body.innerHTML='<div class="empty-state">Panel unavailable</div>'; }
 p.querySelectorAll('.iconbtn').forEach(b=>b.onclick=e=>{e.stopPropagation();const t=b.dataset.t;
  if(t==='big')expandPanel(sec,modId);
  else if(t==='csv'){ const c=NWX._charts[sec.id]; if(c)exportChartCSV(c.spec,sec.ti); }
  else if(t==='png'){ const c=NWX._charts[sec.id]; if(c){const svg=c.holder.querySelector('svg'); if(svg)Charts.png(svg,sec.ti);} }
  else if(t==='hide'){ hideWidget(modId,sec.id); }
 }); return p;}
function expandPanel(sec,modId){ NWX.audit('Chart full-screen',sec.ti);
 const ov=document.createElement('div'); ov.className='modal-ov';
 ov.innerHTML='<div class="modal" style="width:min(1250px,97vw)"><div class="modal-h"><h3>'+esc(sec.ti)+'</h3><span class="hint" style="color:var(--faint);font-size:11px">'+esc(NWX.scope().label)+' · '+state.period+'</span><button class="iconbtn x">'+icon('x')+'</button></div><div class="modal-b"><div class="ck-zoom"></div></div><div class="modal-f"><button class="btn ghost mclose">Close</button><button class="btn" id="zt">Toggle table view</button><button class="btn pri" id="zcsv">'+icon('download',14)+' Export CSV</button></div></div>';
 document.body.appendChild(ov); const close=()=>ov.remove(); ov.querySelector('.x').onclick=close; ov.querySelector('.mclose').onclick=close; ov.onclick=e=>{if(e.target===ov)close();};
 const zb=ov.querySelector('.ck-zoom');
 if(sec.k==='chart'){ const spec=Object.assign({},sec.spec,{height:520,zoom:sec.spec.zoom}); delete spec.el; Charts.render(zb,spec);
  ov.querySelector('#zcsv').onclick=()=>exportChartCSV(sec.spec,sec.ti);
  ov.querySelector('#zt').onclick=()=>{ zb.innerHTML=''; const s=sec.spec;
   if(s.labels&&s.series){ let h='<table class="tbl"><thead><tr><th>'+esc(s.title||'')+'</th>'+s.series.map(x=>'<th class="num">'+esc(x.name)+'</th>').join('')+'</tr></thead><tbody>';
    s.labels.forEach((L,i)=>{h+='<tr><td>'+esc(L)+'</td>'+s.series.map(x=>'<td class="num">'+(x.data[i]==null?'—':(s.fmt==='money'?NWX.fmtM(x.data[i]):x.data[i].toLocaleString()))+'</td>').join('')+'</tr>';});
    zb.innerHTML=h+'</tbody></table>'; } };
 } else if(sec.k==='table'){ mountTable(zb,Object.assign({},sec.cfg,{page:24})); ov.querySelector('#zcsv').onclick=()=>tblCSV(sec.cfg);}
 else { const m=MOUNT[sec.id]; if(m)m(zb,sec,modId); }}
function exportChartCSV(spec,title){ const rows=[[title],[]];
 if(spec.labels&&spec.series){ rows.push(['Label'].concat(spec.series.map(s=>s.name)));
  spec.labels.forEach((L,i)=>rows.push([L].concat(spec.series.map(s=>s.data[i])))); }
 else if(spec.items){ rows.push(['Segment','Value']); spec.items.forEach(it=>rows.push([it.label,it.value])); }
 else if(spec.rows){ rows.push(['Row','Col','Value']); spec.rows.forEach((r,i)=>spec.cols.forEach((c,j)=>rows.push([r,c,spec.vals[i][j]]))); }
 Charts.csv((title||'chart').replace(/[^\w]+/g,'_')+'.csv',rows); NWX.audit('Chart exported',title);}
function hideWidget(modId,sid){ const lay=store('layout')||{}; lay[modId]=lay[modId]||{order:[],hidden:[]};
 lay[modId].hidden.push(sid); store('layout',lay); NWX.toast('Widget hidden','Restore via ⟲ Reset layout in customize mode.','blue'); renderModule(modId); NWX.audit('Widget removed',modId+'/'+sid);}

/* ---------------- Table engine ---------------- */
function mountTable(host,cfg){ const st={q:'',sort:null,dir:1,page:0,per:cfg.page||8,sel:new Set(), hide:(store('tv_'+cfg.id)||[])};
 const self={rerender:render};
 function cellVal(r,c){ return r[c.k]; }
 function render(){ let rows=cfg.rows.slice();
  if(st.q)rows=rows.filter(r=>cfg.cols.some(c=>String(r[c.k]).toLowerCase().includes(st.q)));
  if(st.sort)rows.sort((a,b)=>{const x=a[st.sort],y=b[st.sort];return (x===y?0:(x>y?1:-1))*st.dir;});
  const total=rows.length; const start=st.page*st.per; const pg=rows.slice(start,start+st.per);
  const cols=cfg.cols.filter(c=>!st.hide.includes(c.k));
  let h='<div class="tbl-tools">';
  if(cfg.search||cfg.search===undefined&&true)h+='<input type="search" placeholder="Search '+total+' records…" value="'+esc(st.q)+'">';
  h+='<div style="position:relative"><button class="btn sm ghost" data-x="cols">'+icon('grid',13)+' Columns</button></div>'+
   '<button class="btn sm ghost" data-x="csv">'+icon('download',13)+' Export</button>'+
   '<button class="btn sm ghost" data-x="print">'+icon('print',13)+' Print</button>';
  if(cfg.addRow)h+='<button class="btn sm pri" data-x="add" style="margin-left:auto">'+esc(cfg.addRow.label)+'</button>';
  else h+='<span style="margin-left:auto;font-size:11px;color:var(--faint)">'+total+' rows · role-filtered</span>';
  h+='</div>';
  if(cfg.bulk&&st.sel.size)h+='<div class="bulkbar"><span>'+st.sel.size+' selected</span>'+cfg.bulk.map(b=>'<button data-b="'+b[1]+'">'+b[0]+'</button>').join('')+'</div>';
  h+='<div class="tbl-wrap"><table class="tbl"><thead><tr>';
  if(cfg.bulk)h+='<th style="width:28px"><input type="checkbox" data-x="all"></th>';
  cols.forEach((c,i)=>h+='<th data-s="'+c.k+'" class="'+(i===0||c.freeze?'frozen':'')+'">'+c.l+' <span class="ar">'+(st.sort===c.k?(st.dir>0?'▲':'▼'):'↕')+'</span></th>');
  h+='</tr></thead><tbody>';
  pg.forEach((r,ri)=>{ h+='<tr data-ri="'+(start+ri)+'">';
   if(cfg.bulk)h+='<td><input type="checkbox" data-x="row" '+(st.sel.has(start+ri)?'checked':'')+'></td>';
   cols.forEach((c,ci)=>{ const v=r[c.k]; let td='';
    if(c.y==='badge')td='<span class="badge '+v+'">'+esc(r[c.k+'L']||v)+'</span>';
    else if(c.y==='select')td='<select class="inline-edit" data-edit="'+c.k+'" data-ri="'+(start+ri)+'">'+c.opts.map(o=>'<option '+(o===v?'selected':'')+'>'+o+'</option>').join('')+'</select>';
    else if(c.y==='link')td=r[c.k+'Url']?'<a href="'+esc(r[c.k+'Url'])+'" class="tbl-link">'+esc(v||'Open \u2192')+'</a>':'\u2014';
    else if(c.y==='decide')td=r.aid?'<span style="white-space:nowrap"><button class="btn sm pri" data-decide="approved" data-aid="'+r.aid+'" data-doc="'+esc(r.no||r.a||'')+'">\u2713 Approve</button> <button class="btn sm ghost" data-decide="rejected" data-aid="'+r.aid+'" data-doc="'+esc(r.no||r.a||'')+'">\u2715 Reject</button></span>':'\u2014';
    else if(c.y==='money')td=NWX.fmtM(v);
    else if(c.y==='pct')td=(v==null||v==='')?'—':v+'%';
    else if(c.y==='num')td=(v==null?'—':Number(v).toLocaleString());
    else td=esc(v);
    h+='<td class="'+((c.y==='money'||c.y==='num'||c.y==='pct')?'num':'')+(ci===0?' frozen':'')+'"'+(c.wide?' style="white-space:normal;min-width:280px"':'')+'>'+td+'</td>'; });
   h+='</tr>'; });
  if(!pg.length)h+='<tr><td colspan="20"><div class="empty-state">No records match. <a href="#" data-x="clear">Clear search</a></div></td></tr>';
  h+='</tbody>';
  if(cfg.totals){ h+='<tfoot><tr>'; if(cfg.bulk)h+='<td></td>';
   cols.forEach((c,ci)=>{ if(cfg.totals.includes(c.k)){ const sum=rows.reduce((a,r)=>a+(+r[c.k]||0),0);
     h+='<td class="num">'+(c.y==='money'?NWX.fmtM(sum):Math.round(sum*10)/10).toLocaleString()+'</td>'; }
    else h+='<td>'+(ci===0?'Total ('+total+')':'')+'</td>'; });
   h+='</tr></tfoot>'; }
  h+='</table></div><div class="tbl-pager"><button data-x="prev" '+(st.page===0?'disabled':'')+'>‹ Prev</button><span>Page '+(st.page+1)+' / '+Math.max(1,Math.ceil(total/st.per))+'</span><button data-x="next" '+((st.page+1)*st.per>=total?'disabled':'')+'>Next ›</button></div>';
  host.innerHTML=h;
  const inp=host.querySelector('[type=search]'); if(inp)inp.oninput=e=>{st.q=e.target.value.toLowerCase();st.page=0;render();
    const v=inp.value;inp.focus();inp.setSelectionRange(v.length,v.length);};
  host.querySelectorAll('th[data-s]').forEach(th=>th.onclick=()=>{const k=th.dataset.s; if(st.sort===k)st.dir*=-1;else{st.sort=k;st.dir=1;} render();});
  host.querySelectorAll('tbody tr[data-ri]').forEach(tr=>tr.onclick=e=>{ if(e.target.matches('select,input,a,button'))return; const r=rows[+tr.dataset.ri]; NWX.audit('Row drill-down',cfg.id);
   if(cfg.drill)cfg.drill(r); else NWX.rowDrill(cfg,r); });
  host.querySelectorAll('[data-edit]').forEach(sel=>sel.onchange=e=>{ const r=rows[+sel.dataset.ri]; r[sel.dataset.edit]=sel.value; NWX.toast('Updated','Status saved & workflow notified.','green'); NWX.audit('Inline status update',cfg.id); NWX.bumpBadges(); });
  host.querySelectorAll('[data-x]').forEach(b=>{ const x=b.dataset.x;
   if(x==='prev')b.onclick=()=>{st.page--;render();};
   if(x==='next')b.onclick=()=>{st.page++;render();};
   if(x==='clear')b.onclick=e=>{e.preventDefault();st.q='';render();};
   if(x==='csv')b.onclick=()=>tblCSV(cfg,st);
   if(x==='print')b.onclick=()=>tblPrint(cfg,st);
   if(x==='cols')b.onclick=e=>{e.stopPropagation();colMenu(b);};
   if(x==='all')b.onchange=()=>{pg.forEach((r,i)=>b.checked?st.sel.add(start+i):st.sel.delete(start+i));render();};
   if(x==='row')b.onchange=e=>{ const tr=b.closest('tr'); const ri=+tr.dataset.ri; b.checked?st.sel.add(ri):st.sel.delete(ri); render(); };
   if(x==='add')b.onclick=()=>addRowForm(); });
  host.querySelectorAll('[data-b]').forEach(b=>b.onclick=()=>{ const map={ack:'Acknowledged',res:'Resolved',esc:'Escalated'}; const sel=[...st.sel].map(i=>rows[i]).filter(Boolean);
   sel.forEach(r=>{ if(r.st!==undefined)r.st=map[b.dataset.b]||'Acknowledged'; });
   NWX.toast('Bulk action complete',sel.length+' records → '+b.textContent+' — notifications sent.','green'); st.sel.clear(); NWX.audit('Bulk action: '+b.textContent,cfg.id); render(); }); }
 function colMenu(btn){ const old=$('.colmenu'); if(old)old.remove();
  const m=document.createElement('div'); m.className='modal'; m.style.cssText='position:absolute;z-index:50;top:'+(btn.offsetTop+30)+'px;left:'+btn.offsetLeft+'px;width:230px;padding:10px;background:var(--card);border:1px solid var(--border-2);border-radius:10px;box-shadow:var(--sh)';
  m.className='';m.style.cssText='position:absolute;z-index:60;top:34px;left:0;width:230px;padding:10px;background:var(--card);border:1px solid var(--border-2);border-radius:10px;box-shadow:var(--sh)';
  m.innerHTML='<b style="font-size:12px">Columns · saved view</b>'+cfg.cols.map(c=>'<label style="display:flex;gap:8px;align-items:center;padding:5px 2px;font-size:12px;cursor:pointer"><input type="checkbox" data-k="'+c.k+'" '+(st.hide.includes(c.k)?'':'checked')+'> '+c.l+'</label>').join('')+'<button class="btn sm ghost" style="width:100%;margin-top:6px" data-x="resetview">Reset saved view</button>';
  btn.parentElement.appendChild(m);
  m.querySelectorAll('input').forEach(ch=>ch.onchange=()=>{ const k=ch.dataset.k; const i=st.hide.indexOf(k); ch.checked?i>=0&&st.hide.splice(i,1):(i<0&&st.hide.push(k)); store('tv_'+cfg.id,st.hide); render(); m.remove(); });
  m.querySelector('[data-x=resetview]').onclick=()=>{st.hide=[];store('tv_'+cfg.id,[]);render();};
  setTimeout(()=>{document.addEventListener('click',function h2(ev){if(!m.contains(ev.target)&&ev.target!==btn){m.remove();document.removeEventListener('click',h2);}});},10); }
 function addRowForm(){ const f=cfg.addRow; NWX.modal(f.label,'<div class="form-grid">'+f.form.map(x=>'<div class="full"><label>'+x[0]+'</label><input type="'+x[1]+'" data-f="'+x[0]+'"></div>').join('')+'</div>'+
   '<p style="font-size:11px;color:var(--muted)">Effective-dated & versioned — superseded rows are never deleted (audit).</p>',b2=>{const ok=b2.parentElement.querySelector('.mok'); if(ok)ok.onclick=null;}, 'Save (effective-dated)');
  setTimeout(()=>{ const mks=$$('.modal-ov .mok').pop(); if(mks)mks.addEventListener('click',()=>{ cfg.rows.unshift({a:'NEW-'+Date.now().toString(36).toUpperCase(),b:'(new entry)',c:new Date().toISOString().slice(0,10),d:'—',e:'blue',eL:'Draft'}); NWX.audit('Record created',cfg.id); render(); }); },50); }
 render(); }
function tblRows(cfg,st){ let rows=cfg.rows.slice(); if(st&&st.q)rows=rows.filter(r=>cfg.cols.some(c=>String(r[c.k]).toLowerCase().includes(st.q))); return rows; }
function tblCSV(cfg,st){ const rows=tblRows(cfg,st); Charts.csv(cfg.id+'.csv',[cfg.cols.map(c=>c.l)].concat(rows.map(r=>cfg.cols.map(c=>c.y==='decide'?'':(c.y==='badge'?(r[c.k+'L']||r[c.k]):(c.y==='link'?(r[c.k]||''):r[c.k])))))); NWX.audit('Table exported',cfg.id);}
function tblPrint(cfg,st){ const rows=tblRows(cfg,st).slice(0,60);
 NWX.printHTML(cfg.id.replace(/_/g,' ').toUpperCase(),'<table><thead><tr>'+cfg.cols.map(c=>'<th>'+c.l+'</th>').join('')+'</tr></thead><tbody>'+rows.map(r=>'<tr>'+cfg.cols.map(c=>'<td>'+(c.y==='decide'?'':(c.y==='badge'?(r[c.k+'L']||r[c.k]):(c.y==='link'?(r[c.k]||''):c.y==='money'?NWX.fmtM(r[c.k]):r[c.k])))+'</td>').join('')+'</tr>').join('')+'</tbody></table>'); }

/* ---------------- Customize (drag & drop widgets) ---------------- */
function bindCustomize(grid,modId){ if(!state.customize)return;
 let dragEl=null;
 grid.querySelectorAll('.panel').forEach(p=>{ p.setAttribute('draggable','false');
  const h=p.querySelector('.panel-h');
  h.addEventListener('mousedown',()=>p.setAttribute('draggable','true'));
  p.addEventListener('dragstart',e=>{dragEl=p;e.dataTransfer.effectAllowed='move';p.classList.add('dropover');});
  p.addEventListener('dragend',()=>{p.classList.remove('dropover');p.setAttribute('draggable','false');
   const lay=store('layout')||{};lay[modId]=lay[modId]||{order:[],hidden:[]};
   lay[modId].order=Array.from(grid.children).map(x=>x.dataset.sid);store('layout',lay);NWX.audit('Dashboard layout changed',modId);});
  p.addEventListener('dragover',e=>{e.preventDefault();const over=e.currentTarget; if(over===dragEl)return;
   const r=grid.querySelectorAll('.panel');const arr=Array.from(r);
   const di=arr.indexOf(dragEl),oi=arr.indexOf(over); if(di<oi)grid.insertBefore(dragEl,over.nextSibling);else grid.insertBefore(dragEl,over); }); });}

/* ---------------- Module renderer ---------------- */
function renderModule(id){ state.mod=id; LIVE_FNS=[]; NWX._charts={};
 const roleObj=role(); const def=D.MODULES_DEF.find(m=>m[0]===id)||D.MODULES_DEF[0];
 const mod=MODULES[id]?MODULES[id]():{title:id,sub:'',cards:[],sections:[]};
 window._mod=mod;
 // recents
 state.recent=[id].concat((state.recent||[]).filter(x=>x!==id)).slice(0,6); save();
 $('#crumb').textContent='/ '+NWX.t(id==='exec'?'dash':id);
 document.title='Nowills SQC ERP — '+def[1];
 const layout=(store('layout')||{})[id]||{order:[],hidden:[]};
 let secs=mod.sections.filter(s=>!layout.hidden.includes(s.id));
 if(layout.order.length)secs.sort((a,b)=>{const x=layout.order.indexOf(a.id),y=layout.order.indexOf(b.id);return (x<0?99:x)-(y<0?99:y);});
 const pg=$('#page');
 pg.innerHTML='<div class="offlinebar">'+icon('wifi',16)+'<div><b>Offline mode</b> — showing cached data · last sync <span id="obSync">—</span>. Online approvals are suspended; actions you take are queued for safe sync.</div><button class="btn sm" style="margin-left:auto" id="obResync">Retry sync</button></div>'+
 '<div class="page-head"><div><h1>'+esc(NWX.t(id==='exec'?'dash':id))+'</h1><div class="sub">'+esc(mod.sub||'')+' · '+esc(NWX.scope().label)+' · '+esc(state.period)+' · updated <span id="liveClock">just now</span> <span id="grainbtn"></span></div></div>'+
 '<div class="ph-tools">'+(id==='exec'?'<button class="btn ghost" id="bExport">'+icon('download',15)+' Export ▸</button>':'')+
 '<button class="btn ghost" id="bRefresh">'+icon('refresh',15)+' Refresh</button>'+
 '<button class="btn ghost '+(state.customize?'pri':'')+'" id="bCust">'+icon('grid',15)+(state.customize?' Done':' Customize')+'</button>'+
 '<button class="btn pri" id="bQuick2">'+icon('plus',15)+' '+NWX.t('quick')+'</button></div></div>'+
 '<div class="kpi-grid" id="kpis"></div><div class="grid" id="grid"></div>';
 renderCards($('#kpis'),mod.cards);
 const grid=$('#grid'); secs.forEach(s=>grid.appendChild(sectionPanel(s,id)));
 bindCustomize(grid,id); renderSidebar(); NWX.bumpBadges();
 const obs=$('#obResync'); if(obs)obs.onclick=()=>NWX.setOffline(false,true);
 if(state.offline)$('#obSync').textContent=lastSync();
 $('#bRefresh').onclick=()=>{NWX.toast('Refreshed','Live summaries re-queried via read-replica (38 ms).','blue');renderModule(id);};
 $('#bQuick2').onclick=quickCreate;
 $('#bCust').onclick=()=>{state.customize=!state.customize;document.body.classList.toggle('customize',state.customize);save();renderModule(id);if(state.customize)NWX.toast('Customize mode','Drag panels to reorder · ✕ hides a widget · changes auto-save.','blue');};
 const be=$('#bExport'); if(be)be.onclick=exportMenu;
 NWX.audit('Dashboard viewed',def[1]+' · '+NWX.scope().label);
 $('#main').scrollTop=0;}

/* ---------------- Topbar ---------------- */
function renderTopbar(){ const tb=$('#tbSel');
 function opt(arr,v,getid,getn){return arr.map(x=>{const id=getid?getid(x):x;const n=getn?getn(x):x;return '<option value="'+id+'"'+(id===v?' selected':'')+'>'+n+'</option>';}).join('');}
 const SL=(window.__SERVER||{}).scopeLists||{};
 const COS=SL.companies&&SL.companies.length?SL.companies:D.COMPANIES, QYS=SL.quarries&&SL.quarries.length?SL.quarries:D.QUARRIES,
  PRS=SL.projects&&SL.projects.length?SL.projects:[], BRS=SL.branches&&SL.branches.length?SL.branches:D.BRANCHES;
 tb.innerHTML='<select class="sel" id="sCo" title="Company">'+opt(COS,state.co,x=>x.id,x=>x.n)+'</select>'+
 '<select class="sel" id="sQry" title="Quarry">'+opt(QYS,state.qry,x=>x.id,x=>x.n)+'</select>'+
 '<select class="sel hidemd" id="sPrj" title="Project"><option value="all">All Projects</option>'+(PRS.length?opt(PRS,state.prj,x=>x.id,x=>x.n):opt(D.PROJECTS,state.prj,x=>x.id,x=>x.id))+'</select>'+
 '<select class="sel hidemd" id="sBr" title="Branch">'+opt(BRS,state.br,x=>x.id,x=>x.n)+'</select>'+
 '<select class="sel hidemd" id="sPer" title="Reporting period">'+opt(D.PERIODS,state.period)+'</select>'+
 '<select class="sel" id="sCur" title="Currency">'+opt(D.CURRENCIES,state.cur,x=>x.c,x=>x.c)+'</select>';
 [['sCo','co'],['sQry','qry'],['sPrj','prj'],['sBr','br'],['sPer','period'],['sCur','cur']].forEach(x=>{ $('#'+x[0]).onchange=e=>{ state[x[1]]=e.target.value; save();
   NWX.audit('Scope changed',x[0]+' → '+e.target.value); NWX.toast('Scope updated','All cards, charts and tables re-filtered to your selection.','blue'); renderModule(state.mod); }; });}

/* ---------------- Drawers content ---------------- */
function openFilters(){ NWX.audit('Global filter opened');
 const grp=(l,inner)=>'<div class="f-group"><label>'+l+'</label>'+inner+'</div>';
 const sel=(arr,v,gid,gn)=>'<select id="'+gid+'">'+arr.map(x=>'<option>'+x+'</option>').join('')+'</select>';
 const ms=(arr,key)=>'<div class="checks">'+arr.map(x=>{const id=x.id||x;const lbl=x.n||x;return '<span class="chip '+(state.cmp[key].includes(id)?'on':'')+'" data-cm="'+key+'" data-v="'+id+'">'+esc(lbl)+'</span>';}).join('')+'</div>';
 const presets=(state.presets||[]);
 NWX.drawer('filters','Global Filter Panel','filter',
  grp('Company',sel(D.COMPANIES.map(c=>c.n)))+grp('Quarry',sel(D.QUARRIES.map(c=>c.n)))+grp('Construction Project',sel(['All'].concat(D.PROJECTS.map(p=>p.id+' — '+p.n))))+grp('Site / Branch',sel(['All','Accra HQ — Head Office','Tema Yard','Kumasi Branch'],'fbr'))+
  grp('Department','<select><option>All</option>'+D.DEPTS.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Cost Centre','<select><option>All</option>'+D.COSTC.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Profit Centre','<select><option>All</option><option>Quarry Ops</option><option>Construction</option><option>Haulage</option><option>Equipment Hire</option><option>Materials</option></select>')+
  grp('Client','<select><option>All</option>'+D.CLIENTS.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Supplier / Subcontractor','<select><option>All</option>'+D.SUPPLIERS.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Product / Material','<select><option>All</option>'+D.PRODUCTS.map(d=>'<option>'+d.n+'</option>').join('')+'</select>')+
  grp('Equipment & Vehicle','<select><option>All</option>'+D.EQUIPMENT.slice(0,8).map(d=>'<option>'+d[0]+' '+d[1]+'</option>').join('')+D.FLEET.slice(0,6).map(d=>'<option>'+d[0]+'</option>').join('')+'</select>')+
  grp('Shift','<select><option>All</option>'+D.SHIFTS.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Region / District','<select><option>All regions</option>'+D.REGIOND.map(d=>'<option>'+d+'</option>').join('')+'</select>')+
  grp('Date range','<div style="display:flex;gap:8px"><input type="date" value="2026-08-01"><input type="date" value="2026-08-10"></div>')+
  grp('Week / Month / Quarter / Fiscal Year','<div class="checks"><span class="chip">W32</span><span class="chip on">August</span><span class="chip">Q3</span><span class="chip">FY2026</span></div>')+
  grp('Currency','<select id="fCur">'+D.CURRENCIES.map(c=>'<option'+(c.c===state.cur?' selected':'')+'>'+c.c+'</option>').join('')+'</select>')+
  grp('Status','<div class="checks">'+['Open','Approved','Pending','Overdue','Closed','Archived'].map(s=>'<span class="chip"data-st="'+s+'">'+s+'</span>').join('')+'</div>')+
  '<div class="f-group"><label>Compare side-by-side</label>'+
   '<div style="font-size:11px;color:var(--muted);margin:4px 0">Companies</div>'+ms(D.COMPANIES.slice(1),'co')+
   '<div style="font-size:11px;color:var(--muted);margin:6px 0">Quarries</div>'+ms(D.QUARRIES.slice(1),'qry')+
   '<div style="font-size:11px;color:var(--muted);margin:6px 0">Projects</div>'+ms(D.PROJECTS.slice(0,5).map(p=>({id:p.id,n:p.id})),'prj')+'</div>'+
  '<div class="f-group"><label>Saved presets & shared views</label>'+(presets.length?presets.map((p,i)=>'<div class="hrow"><span class="nm" style="width:auto;flex:1">'+esc(p.n)+'</span><span class="vv">'+esc(p.scope)+'</span><button class="btn sm" data-pp="'+i+'">Load</button></div>').join(''):'<div class="empty-state" style="padding:14px">No presets yet — save your current combination.</div>')+'</div>',b=>{
   b.querySelectorAll('[data-cm]').forEach(ch=>ch.onclick=()=>{const k=ch.dataset.cm,v=ch.dataset.v;const arr=state.cmp[k];const i=arr.indexOf(v);i>=0?arr.splice(i,1):arr.push(v);ch.classList.toggle('on');});
   b.querySelectorAll('[data-pp]').forEach(bb=>bb.onclick=()=>{const p=state.presets[+bb.dataset.pp];const rr=confirm? null:null; Object.assign(state,p.s||{});save();closeDrawer();renderModule(state.mod);NWX.toast('Preset loaded',p.n,'blue');}); });
 const foot=document.createElement('div');foot.className='drawer-f';
 foot.innerHTML='<button class="btn pri" id="fApply">'+icon('check',14)+' '+(NWX.t('apply'))+'</button><button class="btn" id="fSave">'+NWX.t('save')+'</button><button class="btn" id="fShare">'+icon('share',14)+' '+NWX.t('share')+'</button><button class="btn ghost" id="fReset">'+NWX.t('reset')+'</button><button class="btn ghost" id="fExp">'+icon('download',14)+' Export</button>';
 openDrawer.appendChild(foot);
 $('#fApply').onclick=()=>{closeDrawer();NWX.toast('Filters applied','Every KPI, chart, table and map now reflects your selection.','green');NWX.audit('Filter applied',NWX.scope().label);renderModule(state.mod);};
 $('#fSave').onclick=()=>{const n='View — '+new Date().toLocaleDateString('en-GB',{day:'numeric',month:'short'})+' '+new Date().toTimeString().slice(0,5);state.presets.push({n:n,scope:'Group',s:{co:state.co,qry:state.qry,br:state.br,prj:state.prj,cur:state.cur,period:state.period}});save();NWX.toast('Preset saved',n,'green');openFilters();};
 $('#fShare').onclick=()=>{const url=location.origin+location.pathname+'#/'+state.mod+'?co='+state.co+'&q='+state.qry+'&cur='+state.cur; if(navigator.clipboard)navigator.clipboard.writeText(url); NWX.toast('View link copied','Recipients inherit the same filters & permissions are enforced per login.','blue');NWX.audit('Filter view shared',url);};
 $('#fReset').onclick=()=>{state.co='grp';state.qry='all';state.br='all';state.prj='all';state.cmp={co:[],qry:[],prj:[]};save();renderTopbar();closeDrawer();renderModule(state.mod);NWX.toast('Filters reset','Back to group consolidated view.','blue');};
 $('#fExp').onclick=()=>{Charts.csv('filtered-executive-summary.csv',[['KPI','Value','Scope','Period'],...((window._mod?window._mod.cards:[]).slice(0,12).map(k=>[k.t,k.v,NWX.scope().label,state.period]))]);NWX.audit('Filtered results exported');};}
function openNotifs(){ const rows=(window.NWA||[]).slice(0,14);
 NWX.drawer('notifs',NWX.t('notif')+' & Alert Exception Centre','bell',
  '<div class="pill-tabs">'+['All','Critical','High','Medium','Resolved'].map((s,i)=>'<span class="chip '+(i===0?'on':'')+'" data-f="'+s+'">'+s+'</span>').join('')+'<span style="margin-left:auto;font-size:11px;color:var(--faint)">'+rows.length+' live</span></div>'+
  rows.map(a=>{const sev=a.sev;const col=sev==='critical'||sev==='high'?'red':sev==='medium'?'amber':sev==='low'?'grey':'blue';
  return '<div class="notif unread" data-no="'+a.no+'"><div class="ni" style="background:var(--'+col+'-d);color:var(--'+col+')">'+icon('bolt',16)+'</div><div style="flex:1"><h5>'+a.src+' <span class="badge '+col+'" style="margin-left:4px">'+sev+'</span></h5><p>'+a.desc+'</p><p style="margin-top:3px">'+a.mod+' · '+a.site+' · owner '+a.own+' · due '+a.due+' · <b>action:</b> '+({critical:'Immediate containment & exec call','high':'Assign owner within 1h','medium':'Remediate before due date'}[sev]||'Review when convenient')+'</p><time>'+a.t.toLocaleString('en-GB',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'})+' · '+a.no+'</time></div><span class="sev badge grey">'+a.st+'</span></div>';}).join('')
  +'<div style="display:flex;gap:8px;margin-top:10px"><button class="btn" style="flex:1" id="nAll">Mark all read</button><button class="btn pri" style="flex:1.4" id="nCentre">Open exception centre (Executive)</button></div>',b=>{
  b.querySelectorAll('.notif').forEach(n=>{n.onclick=()=>{n.classList.remove('unread');const a=NWA.find(x=>x.no===n.dataset.no);NWX.alertOpen(a);NWX.audit('Alert opened',a.no);};});
  b.querySelector('[data-f]')&&b.querySelectorAll('[data-f]').forEach(t=>t.onclick=()=>{b.querySelectorAll('[data-f]').forEach(x=>x.classList.remove('on'));t.classList.add('on');const f=t.dataset.f;
   b.querySelectorAll('.notif').forEach(n=>{const a=NWA.find(x=>x.no===n.dataset.no);n.style.display=(f==='All')?'':(f==='Resolved'?a.st==='Resolved'?'':'none':a.sev===f.toLowerCase()?'':'none');});});
  b.querySelector('#nAll').onclick=()=>{b.querySelectorAll('.notif').forEach(n=>n.classList.remove('unread'));NWX.toast('All read','Inbox cleared — alerts remain tracked in the exception centre.','blue');};
  b.querySelector('#nCentre').onclick=()=>{closeDrawer();go('exec');setTimeout(()=>{const el=$('[data-sid="alerts"]');if(el)el.scrollIntoView({behavior:'smooth'});},120);};});}
function openTasks(){ NWX.drawer('tasks',NWX.t('tasks'),'check','<div id="taskHost"></div>',b=>{ MOUNT.apprq(b.querySelector('#taskHost')); });}
function openAI(){ NWX.drawer('ai','Nia · '+NWX.t('assist'),'ai',
 '<div class="notif" style="cursor:default"><div class="ni" style="background:var(--blue-d);color:var(--blue)">'+icon('ai',17)+'</div><div><h5>Nia — Nowills Intelligence Assistant</h5><p>Ask about production, cash, alerts, projects or tax. Answers come from live ERP data with sources.</p></div></div>'+
 '<div class="checks" style="margin:6px 0 12px">'+['Production today?','Cash position & 13-week outlook','Top risk right now','Who owes us most?','Tax due this month'].map(q=>'<span class="chip" data-q="'+q+'">'+q+'</span>').join('')+'</div>'+
 '<div id="aiOut"></div><div style="display:flex;gap:8px;margin-top:auto;position:sticky;bottom:0;background:var(--card);padding-top:8px"><input type="text" id="aiIn" placeholder="Ask Nia anything…" style="flex:1;background:var(--inset);border:1px solid var(--border);border-radius:9px;color:var(--text);padding:10px 12px"><button class="btn pri" id="aiSend">Ask</button></div>',b=>{
  const out=b.querySelector('#aiOut');
  function answer(q){ q=(q||'').toLowerCase(); let a='';
   if(/prod/.test(q))a='<b>'+D.LIVE.prodToday.toLocaleString()+' t</b> crushed today across 3 quarries vs target 5,200 t ('+Math.round(D.LIVE.prodToday/5200*100)+'%). Crusher B is down (WO-7882) — est. gap 320 t by 22:00. <i>Sources: production ledger, plant telemetry.</i>';
   else if(/cash|outlook/.test(q))a='Cash & bank <b>'+NWX.fmtM(31.6e6)+'</b>. 13-week AI forecast dips to <b>'+NWX.fmtM(26.2e6)+'</b> in W36 (cp. covenant floor '+NWX.fmtM(20e6)+') then recovers. Biggest swing: P-2210 IPC-141 ('+NWX.fmtM(2.4e6)+') expected 22 Aug.';
   else if(/risk/.test(q))a='Top AI risk now: <b>duplicate weighbridge ticket pattern</b> (confidence 95%) — 3 tickets flagged at WB-2 overnight, est. exposure '+NWX.fmtM(18e3)+'. Recommended: freeze reprints pending audit. Then: Crusher A bearing P(fail ≤14d)=78%.';
   else if(/owe|debtor/.test(q))a='Largest debtor: <b>Goldline Civil Works</b> — '+NWX.fmtM(1.41e6)+' outstanding, '+NWX.fmtM(920e3)+' overdue (96% of limit). Followed by KMA / AshGold '+NWX.fmtM(1.02e6)+' (61–90d bucket).';
   else if(/tax|gra/.test(q))a='August statutory: WHT return 13 Aug ('+NWX.fmtM(312e3)+'), VAT+NHIL+GETFund+CHRL 31 Aug (<b>'+NWX.fmtM(1.42e6)+'</b>), mineral royalty 31 Aug ('+NWX.fmtM(586e3)+'). Compliance score 96%. E-invoice rejection 2.8% — 14 TINs need revalidation.';
   else a='I can see group revenue '+NWX.fmtM(48.2e6)+' MTD (▲12.4%), production at 94.7% of target, and 6 open safety items. Ask me about production, cash, debtors, tax or risks — or tap a suggestion.';
   return '<div class="notif" style="cursor:default;border-left:3px solid var(--blue)"><div class="ni" style="background:var(--blue-d);color:var(--blue)">'+icon('ai',16)+'</div><div><p style="font-size:12.5px;line-height:1.55;color:var(--text)">'+a+'</p><time>grounded in live data · advisory — verify before action</time></div></div>'; }
  function ask(q){ out.insertAdjacentHTML('afterbegin',answer(q)); NWX.audit('AI assistant query',q); b.querySelector('#aiIn').value=''; }
  b.querySelectorAll('[data-q]').forEach(ch=>ch.onclick=()=>ask(ch.dataset.q));
  b.querySelector('#aiSend').onclick=()=>ask(b.querySelector('#aiIn').value);
  b.querySelector('#aiIn').onkeydown=e=>{if(e.key==='Enter')ask(b.querySelector('#aiIn').value);}; });}
function openHelp(){ NWX.drawer('help',NWX.t('help'),'help',
 '<div class="setrow"><div class="si"><b>Keyboard shortcuts</b><small>Ctrl/⌘+K global search · T theme · B collapse sidebar · Esc close drawers</small></div></div>'+
 '<div class="setrow"><div class="si"><b>Getting started</b><small>Pick a scope in the top bar — every card, chart, table and map re-filters instantly.</small></div></div>'+
 '<div class="setrow"><div class="si"><b>Drill-down</b><small>Click any KPI, chart point, map pin or table row to reach the underlying records.</small></div></div>'+
 '<div class="setrow"><div class="si"><b>Offline mode</b><small>Site nodes capture production, weighbridge, fuel & safety offline and sync with conflict detection.</small></div></div>'+
 '<div class="setrow"><div class="si"><b>Support — Nowills Systems Desk</b><small>+233 302 000 000 · erp@nowills-sqc.com · SLA P1: 1h</small></div></div>'+
 '<button class="btn pri" style="width:100%;margin-top:10px" id="hTour">Take the 60-second tour</button>',b=>{
  b.querySelector('#hTour').onclick=()=>{closeDrawer();['Executive overview','Left rail: 30 governed modules','Top bar: scope, currency & language','Bell: live exception centre','Sparkle: Nia, your AI analyst','Yellow +: quick-create anything'].forEach((t,i)=>setTimeout(()=>NWX.toast('Tour '+(i+1)+'/6',t,'blue'),i*1100));NWX.audit('Help tour started');};});}
function openUser(){ const r=role();
 NWX.drawer('user',NWX.t('profile'),'user',
 '<div style="display:flex;gap:14px;align-items:center;margin-bottom:14px"><div class="av" style="width:52px;height:52px;border-radius:16px;background:linear-gradient(135deg,#f8c200,#b98a00);display:grid;place-items:center;font-weight:800;color:#16181d;font-size:18px">JN</div><div><b style="font-size:16px">Joseph K. Nowills</b><div style="color:var(--muted);font-size:12px">'+esc(state.roleName)+' · MFA ✓ · device trusted</div><div style="color:var(--faint);font-size:11px">Last sign-in 08:12 · Accra HQ</div></div></div>'+
 '<div class="f-group"><label>Switch role (permission preview)</label><select id="uRole">'+D.ROLES.map(x=>'<option'+(x.n===state.roleName?' selected':'')+'>'+x.n+'</option>').join('')+'</select></div>'+
 '<div class="f-group"><label>Language / Kasatow</label><div class="checks">'+[['en','English'],['fr','Français'],['tw','Twi']].map(l=>'<span class="chip '+(state.lang===l[0]?'on':'')+'" data-l="'+l[0]+'">'+l[1]+'</span>').join('')+'</div></div>'+
 '<div class="f-group"><label>Reporting defaults</label><div class="checks">'+D.PERIODS.slice(1,5).map(p=>'<span class="chip '+(state.period===p?'on':'')+'" data-p="'+p+'">'+p+'</span>').join('')+'</div></div>'+
 '<div class="setrow"><div class="si"><b>Push notifications</b><small>Critical alerts via push + SMS + WhatsApp</small></div><div class="toggle on"></div></div>'+
 '<div class="setrow"><div class="si"><b>Auto refresh</b><small>Live tiles every 4 seconds (WebSocket)</small></div><div class="toggle on"></div></div>'+
 '<div class="setrow"><div class="si"><b>Layout</b><small>Saved per device · admins can lock mandatory widgets</small></div><button class="btn sm" id="uReset">Restore default</button></div>'+
 '<button class="btn danger" style="width:100%" id="uOut">Sign out (logs session)</button>',b=>{
  b.querySelector('#uRole').onchange=e=>{if(window.__SERVER&&!window.__SERVER.canPreview){e.target.value=state.roleName;NWX.toast('Managed by IT','Roles are assigned by your administrator.','amber');return;}state.roleName=e.target.value;save();NWX.toast('Role switched','Dashboard rebuilt with «'+state.roleName+'» permissions.','blue');NWX.audit('Role switched',state.roleName);closeDrawer();renderSidebar();renderModule(role().mods.includes(state.mod)?state.mod:'exec');};
  b.querySelectorAll('[data-l]').forEach(ch=>ch.onclick=()=>{state.lang=ch.dataset.l;save();NWX.toast('Language / Kasa',ch.textContent+' ✓','green');renderSidebar();renderModule(state.mod);});
  b.querySelectorAll('[data-p]').forEach(ch=>ch.onclick=()=>{state.period=ch.dataset.p;save();renderModule(state.mod);});
  b.querySelector('#uReset').onclick=()=>{store('layout',{});NWX.toast('Layout reset','Default enterprise layout restored.','green');renderModule(state.mod);};
  b.querySelectorAll('.toggle').forEach(t=>t.onclick=()=>t.classList.toggle('on'));
  b.querySelector('#uOut').onclick=()=>{NWX.audit('User signed out');var S=window.__SERVER||{};if(!S.logout){NWX.toast('Signed out','Demo session closed.','blue');return;}fetch(S.logout,{method:'POST',headers:{'X-CSRF-TOKEN':S.csrf,'Accept':'application/json','X-Requested-With':'XMLHttpRequest'}}).finally(function(){location.href=S.login||'/login';});};});}

/* ---------------- Quick create ---------------- */
function quickCreate(){ NWX.drawer('qc',NWX.t('quick'),'plus','<div class="tiles" style="grid-template-columns:1fr 1fr;padding:0">'+[['Create Sales Order','tag'],['Weighbridge Ticket','scale'],['Record Production','quarry'],['Create Project','hardhat'],['Submit Site Report','site'],['Purchase Requisition','cart'],['Record Goods Receipt','wh'],['Issue Stock','pile'],['Maintenance Request','wrench'],['Create Work Order','wrench'],['Record Fuel Issue','fuel'],['Customer Invoice','doc'],['Record Customer Payment','cash'],['Supplier Invoice','truck'],['Payment Voucher','bank'],['Create Journal','budget'],['Budget Request','budget'],['Record Safety Incident','shield'],['Upload Document','docs'],['Generate Report','report'],['Approve Pending Request','check']].map(a=>'<div class="tile" data-a="'+a[0]+'">'+icon(a[1])+'<span>'+a[0]+'</span></div>').join('')+'</div>',b=>{
 b.querySelectorAll('.tile').forEach(t=>t.onclick=()=>{closeDrawer();NWX.quickForm(t.dataset.a);});});}
NWX.quickForm=function(a){ const forms={'Create Sales Order':[['Customer','select','GH Karim & Partners|Volta Infra|Kpone LP'],['Product','select',D.PRODUCTS.map(p=>p.n).join('|')],['Quantity (t)','number'],['Delivery site','text']],'Weighbridge Ticket':[['Vehicle','text'],['Customer','text'],['Product','select',D.PRODUCTS.map(p=>p.n).join('|')],['Gross (t)','number'],['Tare (t)','number']],'Record Production':[['Quarry','select','Shai Hills|Buoho|Nsawam South'],['Shift','select',D.SHIFTS.join('|')],['Product','select',D.PRODUCTS.map(p=>p.n).join('|')],['Tonnes','number']],'Record Fuel Issue':[['Asset','text'],['Litres','number'],['Issued by','text']],'Record Safety Incident':[['Site','text'],['Type','select','Near-miss|First aid|LTI|Property|Environmental'],['Description','text']],_def:[['Reference','text'],['Entity / project','text'],['Amount / qty','number'],['Notes','text']]};
 const f=forms[a]||forms._def;
 const html='<div class="form-grid">'+f.map(x=>'<div class="full"><label>'+x[0]+'</label>'+(x[1]==='select'?'<select>'+String(x[2]||'').split('|').map(o=>'<option>'+o+'</option>').join('')+'</select>':'<input type="'+x[1]+'">')+'</div>').join('')+'</div>'+
 (state.offline?'<p style="color:var(--amber);font-size:12px">⚠ Offline: this entry will be captured locally and synced with conflict detection (duplicate-safe).</p>':'<p style="color:var(--faint);font-size:11.5px">Validation, numbering & workflow run on the server; poster, time and GPS stamp attach automatically.</p>');
 NWX.modal(a,html,null,state.offline?'Queue for sync':'Submit');
 // every quick action enters the workflow engine as a live instance
 try{ const MAP={'Create Sales Order':'sales','Weighbridge Ticket':'weigh','Record Production':'extract','Create Project':'projects','Submit Site Report':'site','Purchase Requisition':'proc','Record Goods Receipt':'proc','Issue Stock':'inventory','Maintenance Request':'maint','Create Work Order':'maint','Record Fuel Issue':'fuel','Customer Invoice':'sales','Record Customer Payment':'treasury','Supplier Invoice':'suppliers','Payment Voucher':'treasury','Create Journal':'finance','Budget Request':'budget','Record Safety Incident':'hse','Upload Document':'docs','Generate Report':'reports'};
  const mod=MAP[a]||'exec'; const proc=(window.DATA2&&DATA2.WF[mod]&&DATA2.WF[mod][0]);
  if(window.WFI&&proc){ const si=Math.min(1,proc.stages.length-1);
   window.WFI.unshift({id:'WF-'+(61000+window.WFI.length*3),mod:mod,process:proc.n,stage:si,stageName:proc.stages[si][0],owner:proc.stages[si][2],age:'0 h',sla:proc.stages[si][3]+' h',st:'on track',by:'J. Nowills'});} }catch(e){}
 NWX.audit('Quick action performed',a); NWX.toast(state.offline?'Queued offline':'Workflow started',a+(state.offline?' — will sync automatically when online.':' — instance raised in the Workflow Engine, routed to first owner.'),state.offline?'amber':'green');};
NWX.runReport=function(name){ const ov=NWX.modal(name,'<div style="padding:30px;text-align:center"><div class="sk" style="height:14px;margin:10px 0"></div><div class="sk" style="height:14px;width:70%;margin:10px auto"></div><div style="color:var(--muted);font-size:12px;margin-top:12px">Running query across read replicas…</div></div>');
 setTimeout(()=>{ const b=ov.querySelector('.modal-b');
  b.innerHTML='<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap"><span class="chip on">Row-level security applied</span><span class="chip">'+NWX.scope().label+'</span><span class="chip">'+state.period+'</span><span class="chip">'+state.cur+'</span></div>'+
  '<div id="rrHost"></div><div style="display:flex;gap:8px;margin-top:12px"><button class="btn sm" id="rx">'+icon('download',13)+' Excel/CSV</button><button class="btn sm" id="rp">'+icon('print',13)+' PDF / Print</button><button class="btn sm" id="re">'+icon('mail',13)+' Email</button><button class="btn sm ghost" id="rs">'+icon('clock',13)+' Schedule</button></div>';
  mountTable(b.querySelector('#rrHost'),{id:'rr',page:6,cols:[{k:'a',l:'Line'},{k:'b',l:'This period',y:'money'},{k:'c',l:'Previous',y:'money'},{k:'d',l:'Δ %',y:'pct'},{k:'e',l:'Status',y:'badge'}],
   rows:[{a:'Quarry Operations',b:22.1e6,c:19.8e6,d:11.6,e:'green',eL:'On plan'},{a:'Construction',b:18.3e6,c:17.1e6,d:7,e:'green',eL:'On plan'},{a:'Haulage',b:4.1e6,c:4.5e6,d:-8.9,e:'amber',eL:'Watch'},{a:'Equipment hire',b:2.6e6,c:2.3e6,d:13,e:'green',eL:'On plan'},{a:'Materials supply',b:0.9e6,c:1.1e6,d:-18.2,e:'red',eL:'Review'}],totals:['b','c']});
  b.querySelector('#rx').onclick=()=>Charts.csv(name.replace(/\W+/g,'_')+'.csv',[['Line','This period','Previous','Δ%'],['Quarry Operations','22095000','19780000','11.6'],['Construction','18300000','17100000','7.0'],['Haulage','4100000','4500000','-8.9'],['Equipment hire','2600000','2300000','13.0'],['Materials','900000','1100000','-18.2']]);
  b.querySelector('#rp').onclick=()=>NWX.printHTML(name,b.querySelector('#rrHost').innerHTML.replace(/class="[^"]*"/g,''));
  b.querySelector('#re').onclick=()=>NWX.toast('Sent','Delivered to your mailbox + shared link (expires 72h).','green');
  b.querySelector('#rs').onclick=()=>NWX.toast('Scheduled','Daily 06:00 → you & 4 subscribers. Edit under Reports ▸ Scheduled.','blue');
  NWX.audit('Report generated',name); },700);};

/* ---------------- Export / Print / Board pack ---------------- */
function exportMenu(){ NWX.drawer('exp','Export & Board Pack','download',
 '<div class="tiles" style="grid-template-columns:1fr 1fr;padding:0">'+[['Dashboard PDF', 'print'],['Excel (KPI pack)','download'],['CSV bundle','download'],['PNG snapshot','print'],['Email delivery','mail'],['Schedule delivery','clock'],['Secure link sharing','share'],['Board pack','doc'],['Presentation mode','expand']].map(x=>'<div class="tile" data-x="'+x[0]+'">'+icon(x[1])+'<span>'+x[0]+'</span></div>').join('')+'</div>',b=>{
 b.querySelectorAll('.tile').forEach(t=>t.onclick=()=>{closeDrawer();exeExport(t.dataset.x);});});}
function exeExport(x){ NWX.audit('Export requested',x);
 if(x==='Dashboard PDF'||x==='PNG snapshot'){printDash();}
 else if(x==='Excel (KPI pack)'||x==='CSV bundle'){Charts.csv('nowills-executive-kpis.csv',[['KPI','Value','Previous','Δ%','Scope','Period']].concat((window._mod.cards||[]).map(k=>[k.t,k.v,k.prev,k.delta,NWX.scope().label,state.period])));NWX.toast('Exported','KPI pack downloaded with full scope metadata.','green');}
 else if(x==='Email delivery'){NWX.toast('Sent','Dashboard PDF emailed to you; recipients see permission-scoped variants.','green');}
 else if(x==='Schedule delivery'){NWX.toast('Scheduled','Daily 06:00 board digest · weekly Monday pack · monthly close pack.','blue');}
 else if(x==='Secure link sharing'){const u=location.href.split('#')[0]+'#/exec?share='+Math.random().toString(36).slice(2,9);if(navigator.clipboard)navigator.clipboard.writeText(u);NWX.toast('Secure link copied','Signed URL · 72 h expiry · watermark enforced · downloads logged.','blue');}
 else if(x==='Board pack'){boardPack();}
 else if(x==='Presentation mode'){NWX.toggleFull();NWX.toast('Presentation mode','Press Esc to exit. KPIs refresh live for the wall display.','blue');} }
function printHead(ti){ return '<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;border-bottom:3px solid #f8c200;padding-bottom:10px"><div style="width:44px;height:44px;background:#f8c200;border-radius:10px;display:grid;place-items:center;font-weight:800">NS</div><div><h1 style="margin:0">NOWILLS SQC ERP</h1><div style="color:#444">'+ti+'</div></div><div style="margin-left:auto;text-align:right;font-size:11px;color:#444" class="pz-meta">Scope: '+esc(NWX.scope().label)+'<br>Period: '+esc(state.period)+' · Currency: '+state.cur+'<br>Generated '+new Date().toLocaleString('en-GB')+' by J. Nowills ('+esc(state.roleName)+')<br><b>CONFIDENTIAL</b> · Verify: NW-'+Math.random().toString(36).slice(2,8).toUpperCase()+'</div></div>'; }
NWX.printHTML=function(ti,body){ $('#printzone').innerHTML=printHead(ti)+body+'<div style="margin-top:26px;font-size:10px;color:#666;border-top:1px solid #999;padding-top:8px">Generated from Nowills SQC ERP · hash-anchored export · reconciliation status: subledgers ✓ GL ✓ · This export is permission-scoped and watermarked per policy.</div>';
 setTimeout(()=>window.print(),60); NWX.audit('Print/PDF export',ti); };
function printDash(){ const cards=window._mod.cards||[];
 NWX.printHTML('Executive Dashboard — '+new Date().toLocaleDateString('en-GB',{day:'numeric',month:'long',year:'numeric'}),
 '<h2>Key Performance Indicators</h2><table><thead><tr><th>KPI</th><th>Current</th><th>Previous</th><th>Δ%</th><th>Target</th><th>Status</th></tr></thead><tbody>'+cards.map(k=>'<tr><td>'+esc(k.t)+'</td><td><b>'+fmtKpi(k.v,k.u)+'</b></td><td>'+fmtKpi(k.prev,k.u)+'</td><td>'+k.delta+'%</td><td>'+(k.tgt!=null?fmtKpi(k.tgt,k.u):'—')+'</td><td>'+k.st.toUpperCase()+'</td></tr>').join('')+'</tbody></table>'+
 '<h2>Scope & exceptions</h2><p>'+esc(NWX.scope().label)+' · '+esc(state.period)+'. Open critical/high alerts: '+NWA.filter(a=>a.st!=='Resolved'&&(a.sev==='critical'||a.sev==='high')).length+' · Pending approvals: '+(window.NWAPPROVE||[]).filter(a=>a.st==='Pending').length+'.</p>');}
function boardPack(){ const rows=(window._mod.cards||[]).slice(0,14);
 $('#printzone').innerHTML='<div class="pz-cover"><div style="width:72px;height:72px;background:#f8c200;border-radius:18px;margin:0 auto 18px;display:grid;place-items:center;font-size:26px;font-weight:800">NS</div><h1 style="font-size:30px;margin:0">NOWILLS SQC GROUP</h1><h2 style="font-weight:400;color:#333">Monthly Board Pack — Quarrying · Construction · Logistics</h2><div class="pz-meta" style="margin-top:18px">Reporting period: '+esc(state.period)+'<br>Consolidation: Group · all companies · GHS with FX memo<br>Prepared by Group FP&A · Reviewed by CFO · '+new Date().toLocaleDateString('en-GB',{day:'numeric',month:'long',year:'numeric'})+'<br><b>STRICTLY CONFIDENTIAL — BOARD USE ONLY</b> · Pack NW-BP-2026-08 · Verify NW-'+Math.random().toString(36).slice(2,8).toUpperCase()+'</div></div>'+
 printHead('Board pack — contents')+'<ol><li>Executive summary & KPIs</li><li>Quarry production & crushing</li><li>Projects portfolio & cash</li><li>Financial statements (summary)</li><li>Treasury & 13-week liquidity</li><li>Tax & statutory (Ghana)</li><li>HSE & environmental</li><li>Risks, exceptions & AI outlook</li><li>Approvals log & audit extracts</li></ol>'+
 '<h2>1. Executive summary KPIs</h2><table><thead><tr><th>KPI</th><th>Value</th><th>Δ%</th><th>Target</th><th>Status</th></tr></thead><tbody>'+rows.map(k=>'<tr><td>'+esc(k.t)+'</td><td><b>'+fmtKpi(k.v,k.u)+'</b></td><td>'+k.delta+'%</td><td>'+(k.tgt!=null?fmtKpi(k.tgt,k.u):'—')+'</td><td>'+k.st.toUpperCase()+'</td></tr>').join('')+'</tbody></table>'+
 '<h2>2–7. Operating & financial highlights</h2><p>Production 118.4k t MTD (94.7% of target); crushing yield 88.6%; weighbridge dispatches 96.4k t; portfolio completion 55.3%; certified '+NWX.fmtM(113.6e6)+'; cash & bank '+NWX.fmtM(31.6e6)+'; net VAT payable '+NWX.fmtM(1.23e6)+'; zero fatalities, LTIFR 0.82.</p>'+
 '<h2>8. Risks & AI outlook</h2><p>Top items: Crusher A bearing P(fail ≤14d)=78% — PM scheduled; duplicate weighbridge pattern under audit; P-2226 EAC drift −4.2pts. AI Sep forecast: production 124.2k t, revenue '+NWX.fmtM(22.1e6)+', closing cash '+NWX.fmtM(38.2e6)+'.</p>'+
 '<h2>9. Governance</h2><p>All figures reconcile to GL (hash-chained audit trail '+NWLOG.length+' entries today). Approvals within delegated authority only; anomalies none.</p>';
 setTimeout(()=>window.print(),80); NWX.audit('Board pack generated'); }

/* ---------------- Global search palette ---------------- */
function openSearch(){ const idx=[];
 D.MODULES_DEF.forEach(m=>{if(role().mods.includes(m[0]))idx.push({t:m[1],k:'module',id:m[0]});});
 D.PROJECTS.forEach(p=>idx.push({t:p.id+' — '+p.n,k:'project',id:p.id}));
 D.CLIENTS.forEach(c=>idx.push({t:c,k:'customer'})); D.SUPPLIERS.forEach(c=>idx.push({t:c,k:'supplier'}));
 D.EQUIPMENT.forEach(e=>idx.push({t:e[0]+' '+e[1],k:'equipment'})); D.FLEET.forEach(e2=>idx.push({t:e2[0]+' '+e2[1],k:'vehicle'}));
 (window.NWA||[]).slice(0,8).forEach(a=>idx.push({t:a.no+' '+a.src,k:'alert',id:a.no}));
 const ov=document.createElement('div'); ov.className='modal-ov'; ov.style.alignItems='flex-start'; ov.style.paddingTop='12vh';
 ov.innerHTML='<div class="modal" style="width:min(620px,94vw)"><div class="modal-b" style="padding:12px"><input id="gsIn" placeholder="'+esc(NWX.t('search'))+'" autocomplete="off" style="width:100%;background:var(--inset);border:1px solid var(--border-2);border-radius:10px;color:var(--text);padding:12px 14px;font-size:15px;outline:none"><div id="gsOut" style="margin-top:8px;max-height:46vh;overflow:auto"></div></div></div>';
 document.body.appendChild(ov); const inp=ov.querySelector('#gsIn'),out=ov.querySelector('#gsOut'); let sel=0; let cur=[];
 function draw(q){ cur=q?idx.filter(x=>x.t.toLowerCase().includes(q)).slice(0,12):idx.slice(0,9);
  out.innerHTML=cur.map((x,i)=>'<div class="notif '+(i===sel?'unread':'')+'" data-i="'+i+'" style="padding:9px;margin-bottom:5px"><div class="ni" style="width:30px;height:30px;background:var(--blue-d);color:var(--blue)">'+icon(x.k==='module'?'grid':x.k==='alert'?'bell':'doc',15)+'</div><div><h5>'+esc(x.t)+'</h5><p>'+x.k+(x.k==='module'?' — open module':x.k==='alert'?' — open alert':' — open record')+'</p></div></div>').join('')||(q?'<div class="empty-state">No matches</div>':'');
  out.querySelectorAll('.notif').forEach(n=>n.onclick=()=>pick(+n.dataset.i)); }
 function pick(i){ const x=cur[i]; if(!x)return; ov.remove();
  if(x.k==='module')go(x.id); else if(x.k==='alert'){const a=NWA.find(a2=>a2.no===x.id);NWX.alertOpen(a);} else if(x.k==='project'){go('projects');NWX.entity('project',{label:x.t});} else NWX.entity(x.k,{label:x.t}); NWX.audit('Global search used',x.t); }
 inp.oninput=()=>{sel=0;draw(inp.value.toLowerCase());};
 inp.onkeydown=e=>{ if(e.key==='Escape')ov.remove(); if(e.key==='ArrowDown'){sel=Math.min(cur.length-1,sel+1);draw(inp.value.toLowerCase());}
  if(e.key==='ArrowUp'){sel=Math.max(0,sel-1);draw(inp.value.toLowerCase());} if(e.key==='Enter')pick(sel); };
 ov.onclick=e=>{if(e.target===ov)ov.remove();}; draw(''); inp.focus(); }

/* ---------------- Offline engine ---------------- */
NWX.setSec=(k,v)=>{state.sec[k]=v?1:0;save();document.querySelector('.watermark').classList.toggle('on',!!(state.sec.wm&&v===undefined)||!!state.sec.wm);
 $('.watermark').classList.toggle('on',!!state.sec.wm); NWX.toast('Security control','“'+k.toUpperCase()+'” '+(v?'enabled':'disabled')+' — policy versioned & broadcast to all sessions.','blue'); NWX.audit('Security setting changed',k+'→'+(v?'on':'off'));};
NWX.setOffline=(v,silent)=>{ state.offline=v; document.body.classList.toggle('offline',v);
 const ns=$('#btnNet'); ns.querySelector('b')&&(ns.querySelector('b').textContent=v?NWX.t('offline'):NWX.t('online'));
 ns.classList.toggle('off',v); $('#sbFoot').classList.toggle('off',v);
 $('#sbFootTxt').innerHTML=v?'OFFLINE · queue 0<br>last sync '+lastSync():'ONLINE · cluster acc-erp-01<br>sync '+lastSync()+' · <span style="color:var(--green)">healthy</span>';
 if(v){NWX.toast('You are offline','Cached dashboards remain available; permitted captures queue locally; online approvals suspended.','amber');NWX.audit('Went offline');}
 else if(!silent){NWX.toast('Back online','Syncing queued captures… conflict check 0 duplicates · 4 records uploaded ✓','green');setTimeout(()=>NWX.toast('Sync complete','All nodes healthy · last sync just now.','green'),1600);NWX.audit('Sync completed');}
 renderModule(state.mod); };
function lastSync(){ return new Date().toTimeString().slice(0,5); }

/* ---------------- Tick / live engine ---------------- */
let tickN=0;
function tick(){ tickN++;
 D.LIVE.prodToday=Math.max(0,D.LIVE.prodToday+Math.round((Math.random()-0.38)*18));
 D.LIVE.soldToday=Math.max(0,D.LIVE.soldToday+Math.round((Math.random()-0.42)*14));
 D.LIVE.fuelToday=Math.max(0,D.LIVE.fuelToday+Math.round(Math.random()*6));
 try{Charts.tick();}catch(e){}
 LIVE_FNS.forEach(fn=>{try{fn();}catch(e){}});
 if(tickN%4===0){ const lc=$('#liveClock'); if(lc)lc.textContent='just now (live)';
  const sc=$('#syncTxt'); if(sc)sc.textContent='Synced '+(tickN%8?'a few seconds':'just now')+' ago';}
 if(tickN%9===0){ const a=NWA[Math.floor(Math.random()*NWA.length)]; const nb=$('#bNotif'); if(nb){nb.textContent=Math.min(99,(+nb.textContent||0)+1); nb.style.display='';}
  if(tickN%27===0)NWX.toast(a.src,a.desc,a.sev==='critical'||a.sev==='high'?'red':'amber'); }
 if(tickN%5===0&&document.querySelector('#kpis')&&['exec','weigh','qsite'].includes(state.mod)){ renderCards($('#kpis'),window._mod.cards); window._mod.cards=MODULES[state.mod]().cards; renderCards($('#kpis'),window._mod.cards); }}

/* ---------------- Fullscreen (real API → vendor prefixes → pseudo-fullscreen fallback) ---------------- */
/* Sandboxed iframes (and some mobile browsers) refuse the Fullscreen API with a silent rejection —
   so the button ALWAYS falls back to a CSS pseudo-fullscreen (app shell pinned over the viewport)
   instead of doing nothing. Esc exits both modes. Button state is synced on change events. */
NWX.toggleFull=function(){
 const docEl=document.documentElement, root=document;
 const inNative=!!(root.fullscreenElement||root.webkitFullscreenElement||root.mozFullScreenElement||root.msFullscreenElement);
 const inPseudo=docEl.classList.contains('pseudo-fs');
 const syncUI=(on,mode)=>{ const b=$('#bFull'); if(b){ b.classList.toggle('on',!!on); b.title=on?'Exit full screen':'Full screen'; }
   if(on)NWX.toast('Full screen', mode==='native'?'OS fullscreen on — press Esc to exit.':'Window-fit fullscreen (browser embed blocks OS fullscreen) — press Esc or click again to exit.','blue'); };
 if(inNative){ const ex=root.exitFullscreen||root.webkitExitFullscreen||root.mozCancelFullScreen||root.msExitFullscreen; if(ex)Promise.resolve(ex.call(root)).catch(()=>{}); return; }
 if(inPseudo){ docEl.classList.remove('pseudo-fs'); syncUI(false); return; }
 const rq=docEl.requestFullscreen||docEl.webkitRequestFullscreen||docEl.mozRequestFullScreen||docEl.msRequestFullscreen;
 if(!rq){ docEl.classList.add('pseudo-fs'); syncUI(true,'pseudo'); return; }
 try{
   const p=rq.call(docEl,{navigationUI:'hide'});
   if(p&&p.then)p.then(()=>syncUI(true,'native')).catch(()=>{ docEl.classList.add('pseudo-fs'); syncUI(true,'pseudo'); });
   else syncUI(true,'native');
 }catch(e){ docEl.classList.add('pseudo-fs'); syncUI(true,'pseudo'); }
};
['fullscreenchange','webkitfullscreenchange','mozfullscreenchange','MSFullscreenChange'].forEach(ev=>document.addEventListener(ev,()=>{ const on=!!(document.fullscreenElement||document.webkitFullscreenElement); const b=$('#bFull'); if(b&&!document.documentElement.classList.contains('pseudo-fs'))b.classList.toggle('on',on); }));
document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&document.documentElement.classList.contains('pseudo-fs')){ document.documentElement.classList.remove('pseudo-fs'); const b=$('#bFull'); if(b)b.classList.remove('on'); } });

/* ---------------- Boot ---------------- */
function boot(){
 document.documentElement.dataset.theme=state.theme;
 if(!window.NWA)window.NWA=D.makeAlerts();
 state.mod=(location.hash.match(/#\/(\w+)/)||[])[1]||'exec';
 // topbar static parts
 $('#tbUserName').textContent=state.roleName;
 $('#bTheme').onclick=()=>{ state.theme=state.theme==='dark'?'light':'dark'; document.documentElement.dataset.theme=state.theme; save(); renderModule(state.mod); NWX.toast('Theme',state.theme==='dark'?'Dark mode — quarry night shift charcoal.':'Light mode — site-office daylight.'); };
 $('#bFull').onclick=()=>NWX.toggleFull();
 $('#bHelp').onclick=openHelp; $('#bUser').onclick=openUser; $('#bNotif2').onclick=openNotifs; $('#bTasks2').onclick=openTasks; $('#bAI').onclick=openAI; $('#bFilters').onclick=openFilters;
 $('#bQuick').onclick=quickCreate; $('#bSearch').onclick=openSearch;
 $('#btnNet').onclick=()=>NWX.setOffline(!state.offline);
 if(state.offline)NWX.setOffline(true,true);
 document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'){e.preventDefault();openSearch();}
  if(!e.ctrlKey&&!e.metaKey&&e.target.tagName!=='INPUT'&&e.target.tagName!=='TEXTAREA'&&e.target.tagName!=='SELECT'){ if(e.key==='t')$('#bTheme').click(); if(e.key==='b')$('#sbCollapse').click(); if(e.key==='/'){e.preventDefault();openSearch();} }
  if(e.key==='Escape')closeDrawer(); });
 window.onhashchange=()=>{ const id=(location.hash.match(/#\/(\w+)/)||[])[1]; if(id&&id!==state.mod){ if(role().mods.includes(id))renderModule(id); else location.hash='#/'+state.mod; } };
 window.addEventListener('resize',()=>{ clearTimeout(window.__rz); window.__rz=setTimeout(()=>{ if(NWX._charts)Object.values(NWX._charts).forEach(c=>{try{c.inst.draw();}catch(e){}}); },300); });
 // bottom nav
 $('#bottomnav').innerHTML=[['exec','dash','Home'],['quarry','quarry','Quarry'],['projects','hardhat','Projects'],['notif','bell','Alerts'],['more','menu','More']].map(b=>'<div class="bni" data-b="'+b[0]+'">'+icon(b[1],19)+'<span>'+b[2]+'</span>'+(b[0]==='notif'?'<span class="pb" id="bnn">1</span>':'')+'</div>').join('');
 $$('#bottomnav .bni').forEach(el=>el.onclick=()=>{ const b=el.dataset.b; if(b==='more'){document.body.classList.add('sb-open');} else if(b==='notif')openNotifs(); else go(b); });
 renderSidebar(); renderTopbar(); renderModule(state.mod);
 setInterval(tick,4000);
 // PWA
 if('serviceWorker' in navigator&&/^https?/.test(location.protocol)){navigator.serviceWorker.register('sw.js').catch(()=>{});}
 NWX.audit('Session started','UI boot · '+state.lang+' · '+state.cur);
}
document.addEventListener('DOMContentLoaded',boot);
window.NWX=NWX;
})();
