const ODA_SETTINGS_DEFAULT = {
sound:true, vol:.6, tickAll:false, alarms:true,
reminder:true, interval:15, tqWarn:true, tqWarnMin:90, ecgAudio:true,
showAmmo:true, showFormation:true, showLast4:true, showFeed:true, sortBy:'pos', compactStrip:false, autoSelectRed:false,
redact:false, dtgZulu:true, tempUnit:'C', confirmClear:true,
voiceWake:false, voiceChime:true, voiceLang:'en-US', voiceHud:true,
};
let __odaAC = null;
function odaAudio(){ if(!__odaAC){ try{ __odaAC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(__odaAC&&__odaAC.state==='suspended') __odaAC.resume(); return __odaAC; }
function odaSnd(kind, st, force){
if(!force && !(st&&st.sound)) return; const ac=odaAudio(); if(!ac) return; const t=ac.currentTime, v=(st&&st.vol!=null?st.vol:.6);
const tone=(f,at,dur,type='triangle',vol=1,f2)=>{ const o=ac.createOscillator(), g=ac.createGain(); o.type=type; o.frequency.setValueAtTime(f,t+at); if(f2) o.frequency.exponentialRampToValueAtTime(f2,t+at+dur); g.gain.setValueAtTime(.0001,t+at); g.gain.exponentialRampToValueAtTime(v*vol*.22,t+at+.005); g.gain.exponentialRampToValueAtTime(.0001,t+at+dur); o.connect(g); g.connect(ac.destination); o.start(t+at); o.stop(t+at+dur+.03); };
const click=(at=0,vol=1,hp=2500)=>{ const n=700, buf=ac.createBuffer(1,n,ac.sampleRate), d=buf.getChannelData(0); for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/n,3); const s=ac.createBufferSource(), f=ac.createBiquadFilter(), g=ac.createGain(); s.buffer=buf; f.type='highpass'; f.frequency.value=hp; g.gain.value=v*vol*.7; s.connect(f); f.connect(g); g.connect(ac.destination); s.start(t+at); };
switch(kind){
case 'on': click(0,1); tone(620,.01,.07); tone(930,.07,.11); break;
case 'off': click(0,1); tone(620,.01,.07); tone(410,.07,.11); break;
case 'tick': click(0,.8,3000); tone(1400,0,.03,'sine',.35); break;
case 'seg': click(0,.7,2000); tone(760,0,.05,'square',.25); break;
case 'log': click(0,.9); tone(880,0,.07,'sine'); tone(1320,.08,.13,'sine'); tone(1760,.18,.16,'sine',.7); break;
case 'alarm': for(let i=0;i<3;i++) tone(1180,i*.17,.11,'square',.55); break;
case 'open': click(0,1,1800); tone(520,0,.06,'sine'); tone(780,.05,.09,'sine'); break;
case 'close': click(0,1,1800); tone(780,0,.06,'sine'); tone(520,.05,.09,'sine'); break;
case 'slide': tone(300+v*900,0,.04,'sine',.5); break;
case 'export': click(0,1); tone(660,0,.08,'sine'); tone(990,.09,.1,'sine'); tone(1320,.19,.18,'sine',.8); break;
}
}
const redactName = (mb, on) => on ? `${mb.role} (${mb.id})` : `${mb.name} / ${mb.last4}`;
function tqLines(mb){ return Object.entries(mb.tourniquets||{}).filter(([,t])=>t.active).map(([k,t])=>`${({rArm:'R ARM',lArm:'L ARM',rLeg:'R LEG',lLeg:'L LEG'})[k]} ${t.type||'TQ'}${t.position?' '+t.position:''} @${t.time||'----'}`); }
function buildDD1380(mb, opts, S){
const o = opts, L=[], tbsa=estimateBurnTBSA(mb.injuries, mb.burnedRegions||[]);
const st = memberStatus(mb);
L.push(`TACTICAL COMBAT CASUALTY CARE CARD (DD 1380)  ·  ${S.opName}  ·  ${o.dtg}`);
L.push('='.repeat(72));
L.push(`NAME: ${redactName(mb, o.redact)}   ROLE: ${mb.role} ${mb.title}   SEX: ${mb.gender}   WT: ${mb.weight} kg`);
L.push(`SVC/UNIT: ${mb.service} / ${mb.unit}   BLOOD: ${mb.bloodType}   ALLERGIES: ${mb.allergies||'—'}`);
L.push(`EVAC: ${(mb.evac||'—').toUpperCase()}   TRIAGE: ${(mb.triage||'—').toUpperCase()}   READINESS: ${STATUS_LABEL[st].toUpperCase()}   AVPU: ${mb.avpu||'—'}`);
L.push(`MECHANISM: ${mb.mechanism.length?mb.mechanism.join(', '):'—'}`);
if(mb.acuteFindings.length) L.push(`ACUTE FINDINGS: ${mb.acuteFindings.join(', ')}`);
const inj=(mb.injuries||[]).reduce((a,i)=>{a[i.type]=(a[i.type]||0)+1;return a;},{});
L.push(`INJURIES: ${Object.entries(inj).map(([k,n])=>`${n}× ${k.toUpperCase()}`).join(', ')||'—'}${tbsa>0?`  BURN ${tbsa.toFixed(1)}% TBSA`:''}`);
const tq=tqLines(mb); if(tq.length) L.push(`TOURNIQUETS: ${tq.join(' | ')}`);
if(o.vitals && mb.vitals.length){ L.push(''); L.push('SIGNS & SYMPTOMS'); L.push(`  TIME  HR   BP       RR  SpO2  PAIN`); mb.vitals.forEach(v=>L.push(`  ${v.time.padEnd(5)} ${String(v.hr).padEnd(4)} ${(v.sbp+'/'+v.dbp).padEnd(8)} ${String(v.rr).padEnd(3)} ${String(v.spo2).padEnd(5)} ${v.pain}`)); }
if(o.tx && mb.interventions.length){ L.push(''); L.push('TREATMENTS (MARCH-PAWS)'); const by={}; mb.interventions.forEach(s=>{const [k,l]=s.split(':'); (by[k]=by[k]||[]).push(l);}); Object.entries(by).forEach(([k,ls])=>L.push(`  ${k.replace('2','')}: ${ls.join('; ')}`)); }
if(o.meds && (mb.medsGiven.length||mb.fluidsGiven.length)){ L.push(''); L.push('MEDS / FLUIDS'); mb.medsGiven.forEach(x=>L.push(`  ${x.time}  ${x.name}  ${x.dose}`)); mb.fluidsGiven.forEach(x=>L.push(`  ${x.time}  ${x.type}  ${x.vol?x.vol+' mL':''}`)); }
if(o.burn && tbsa>0){ const b=mb.burn||{}; L.push(''); L.push('BURN'); L.push(`  TBSA ${tbsa.toFixed(1)}%  formula ${b.formula||'parkland'}  time of burn ${b.burnTime||'—'}  inhalation ${b.inhal?'Y':'N'}  circumferential ${b.circ?'Y':'N'}`); if((b.uop||[]).length) L.push(`  UOP: ${b.uop.map(u=>`${u.t} ${u.ml}mL`).join(', ')}`); }
if(o.tbi && mb.tbi && Object.values(mb.tbi).some(Boolean)) L.push(`TBI SCREEN: ${Object.entries(mb.tbi).filter(([,v])=>v).map(([k])=>k.toUpperCase()).join(', ')}`);
if(o.notes && mb.notes){ L.push(''); L.push('NOTES'); L.push('  '+mb.notes.replace(/\n/g,'\n  ')); }
L.push(''); L.push(`FIRST RESPONDER: ${S.medic}   TEAM SGT: ${S.teamSgt}`);
return L.join('\n');
}
function buildMISTText(mb, o){ const m=buildMIST(mb); return `MIST — ${redactName(mb,o.redact)} (${mb.role})\nM: ${m.M}\nI: ${m.I}\nS: ${m.S}\nT: ${m.T}`; }
function buildTeamBoard(team, o){
const L=[`${o.boardTitle||'STATUS BOARD'}  ·  ${o.dtg}`, '-'.repeat(72), 'POS ROLE  NAME               STAT    TRIAGE     EVAC      BLD  TQ  AMMO  EQP  LAST VS'];
[...team].sort((a,b)=>a.pos-b.pos).forEach(mb=>{ const lv=mb.vitals[mb.vitals.length-1]; L.push(`${String(mb.pos).padStart(2)}  ${mb.role.padEnd(5)} ${(o.redact?mb.id:mb.name).padEnd(18)} ${STATUS_LABEL[memberStatus(mb)].padEnd(7)} ${(mb.triage||'—').padEnd(10)} ${(mb.evac||'—').padEnd(9)} ${mb.bloodType.padEnd(4)} ${String(Object.values(mb.tourniquets).filter(t=>t.active).length).padEnd(3)} ${mb.ammo.padEnd(5)} ${mb.equip.padEnd(4)} ${lv?lv.time:'—'}`); });
const c={green:0,amber:0,red:0,black:0}; team.forEach(mb=>c[memberStatus(mb)]++);
L.push('-'.repeat(72)); L.push(`UP ${c.green}  WOUNDED ${c.amber}  URGENT ${c.red}  KIA/EXP ${c.black}`);
return L.join('\n');
}
function buildCSV(team, scope, o){
const rows=[['role','name','last4','time','hr','sbp','dbp','rr','spo2','pain','avpu']];
team.filter(scope).forEach(mb=>mb.vitals.forEach(v=>rows.push([mb.role, o.redact?mb.id:mb.name, o.redact?'':mb.last4, v.time,v.hr,v.sbp,v.dbp,v.rr,v.spo2,v.pain,mb.avpu])));
return rows.map(r=>r.map(x=>`"${String(x??'').replace(/"/g,'""')}"`).join(',')).join('\n');
}
function download(name, text, mime='text/plain'){ const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([text],{type:mime})); a.download=name; document.body.appendChild(a); a.click(); setTimeout(()=>{URL.revokeObjectURL(a.href); a.remove();},500); }
function printText(title, text){ const w=window.open('','_blank'); if(!w) return; w.document.write(`<title>${title}</title><pre style="font:11px/1.45 ui-monospace,Menlo,monospace;white-space:pre-wrap;margin:24px">${text.replace(/</g,'&lt;')}</pre>`); w.document.close(); w.focus(); setTimeout(()=>w.print(),200); }
function Drawer({ title, icon, sub, onClose, children, foot, accent }){
return <>
<div className="dz-scrim" onClick={onClose}></div>
<div className="dz" style={{'--dz':accent||'var(--accent)'}} role="dialog" aria-label={title}>
<div className="dz-h"><span className="dz-ico">{icon}</span><b>{title}</b>{sub&&<span className="dz-sub">· {sub}</span>}<span style={{flex:1}}></span>{foot}<button className="dz-x" onClick={onClose}>×</button></div>
<div className="dz-body">{children}</div>
</div>
</>;
}
const Sec = ({title, col, children}) => <><div className="dz-sec"><span style={{background:col||'var(--dz)'}}></span>{title}</div>{children}</>;
function Toggle({ label, desc, on, onChange, danger }){
return <div className={'dz-row'+(on?' on':'')+(danger?' danger':'')} onClick={()=>onChange(!on)}>
<div><div className="t">{label}</div>{desc&&<div className="d">{desc}</div>}</div>
<div className="sw"><i></i><em>{on?'✓':''}</em></div>
</div>;
}
function SegRow({ label, desc, opts, val, onPick }){
return <div className="dz-row static"><div><div className="t">{label}</div>{desc&&<div className="d">{desc}</div>}</div>
<div className="dz-seg">{opts.map(([k,l])=><button key={k} className={val===k?'active':''} onClick={()=>onPick(k)}>{l}</button>)}</div></div>;
}
function SettingsDrawer({ st, setSt, onClose, team, S, onResetTeam, onBlankTeam, onResetSettings, profile = {} }){
const tog=(k,label,desc,extra)=><Toggle key={k} label={label} desc={desc} on={!!st[k]} onChange={v=>{ odaSnd(v?'on':'off',st); setSt({[k]:v}); if(extra) extra(v); }}/>;
const seg=(k,label,desc,opts)=><SegRow key={k} label={label} desc={desc} opts={opts} val={st[k]} onPick={v=>{ odaSnd('seg',st); setSt({[k]:v}); }}/>;
const onN = Object.keys(ODA_SETTINGS_DEFAULT).filter(k=>typeof ODA_SETTINGS_DEFAULT[k]==='boolean'&&st[k]).length;
const cas = team.filter(mb=>memberStatus(mb)!=='green').length;
return <Drawer title="SETTINGS" icon={<span className="spin">⚙</span>} sub="saved locally" onClose={onClose}
foot={<button className="dz-btn" onClick={()=>odaSnd('log',st,true)}>♪ TEST</button>}>
<Sec title="Sound" col="var(--op)">
<Toggle label="Master sound" desc="Every sound on the dashboard: UI clicks, alarms, voice chimes and the ECG pulse tone. Same as the ♪ SND button in the top bar." on={!!st.sound} onChange={v=>{ if(v) odaSnd('on',{...st,sound:true},true); setSt({sound:v}); }}/>
{tog('tickAll','Click on everything','Also tick on chips, team tiles, map tools and drip controls — not just settings and vitals.')}
{tog('alarms','Critical-value alarm','Three-beep alert when a logged vital falls in the red band, a vitals set is overdue, or a TQ passes its warning mark.')}
<div className="dz-row static"><div><div className="t">Volume</div><div className="d">{Math.round(st.vol*100)} %</div></div><input type="range" min="0" max="1" step=".05" value={st.vol} onChange={e=>{ setSt({vol:+e.target.value}); odaSnd('slide',{...st,vol:+e.target.value}); }} style={{width:150}}/></div>
{tog('ecgAudio','ECG pulse tone','Audible pip on each QRS in the pulse tile. Follows master sound; the speaker button on the tile is a per-tile override.')}
</Sec>
<Sec title="Vitals & timers" col="oklch(.75 .18 50)">
{tog('reminder','Reassessment reminder',`Pulse "VITALS DUE" for any casualty whose last set is older than ${st.interval} min.`)}
{seg('interval','Reassess every','Applies to every casualty (non-green) on the team.',[[5,'5'],[10,'10'],[15,'15'],[30,'30 min']])}
{tog('tqWarn','TQ conversion warning',`Alarm and flag the team tile when a tourniquet reaches ${st.tqWarnMin} min — before the 2 h conversion window closes.`)}
{seg('tqWarnMin','TQ warning at','',[[60,'60'],[90,'90'],[110,'110 min']])}
{seg('tempUnit','Temperature entry','Stored in °C.',[['C','°C'],['F','°F']])}
</Sec>
<Sec title="Team strip" col="var(--info)">
{seg('sortBy','Order tiles by','Position follows the order of movement; status floats casualties left.',[['pos','Position'],['status','Status'],['role','Role']])}
{tog('showAmmo','Ammo / equipment pills','Team-sergeant readiness row under each man. Off = medical-only view.')}
{tog('showFormation','Order-of-movement line','Dot diagram in the summary cell.')}
{tog('showFeed','Mini live feed on tiles','Scrolling ECG trace paced to the last heart rate plus HR · BP · SpO₂ on every man; dashed when no vitals are recorded.')}
{tog('showLast4','Show last-4','Hide for OPSEC on shared screens; names stay.')}
{tog('compactStrip','Compact tiles','Role, name and status only — frees vertical room for the casualty view.')}
{tog('autoSelectRed','Auto-select new URGENT','Jump the casualty view to any man whose readiness flips to red.')}
</Sec>
<Sec title="Voice" col="oklch(.7 .18 300)">
{tog('voiceWake','Wake word “medic”','Only act on phrases that start with “medic” (e.g. “medic, HR 110”). Off = every final phrase is parsed.')}
{tog('voiceChime','Confirmation chime','Short chime when a command is applied; low tone when nothing was understood.')}
{tog('voiceHud','Show voice HUD','Live transcript and command log panel while listening.')}
{seg('voiceLang','Recognition language','',[['en-US','US'],['en-GB','UK'],['en-AU','AU']])}
</Sec>
<Sec title="Export defaults" col="var(--amber)">
{tog('redact','Redact PII in exports','Names and last-4 replaced with role + slot id in every generated document.')}
{tog('dtgZulu','Zulu DTG in exports','Off = local time stamps.')}
</Sec>
<Sec title="Data" col="var(--crit)">
{tog('confirmClear','Confirm before clearing','Ask before CLEAR / RESET actions wipe casualty data.')}
<div className="dz-row static"><div><div className="t">Storage</div><div className="d">{team.length} members · {cas} casualties · {Math.round(JSON.stringify(S).length/1024)} kB autosaved</div></div><button className="dz-btn" onClick={()=>{ odaSnd('off',st); onResetSettings(); }}>DEFAULTS</button></div>
<div className="dz-row static"><div><div className="t">{profile.blankTitle || 'Blank roster'}</div><div className="d">{profile.blankDesc}</div></div><button className="dz-btn" onClick={onBlankTeam}>NEW {profile.unitShort || 'UNIT'}</button></div>
<div className="dz-row static danger"><div><div className="t">Reset to sample roster</div><div className="d">Restore the sample {profile.unitShort || 'team'} and wipe all casualty data.</div></div><button className="dz-btn warn" onClick={onResetTeam}>RESET</button></div>
</Sec>
<div className="dz-foot">{onN} options on · v1 {profile.unitShort || ''}</div>
</Drawer>;
}
function ExportDrawer({ st, S, team, teamAll, mb, onClose, onImport, profile = {} }){
const [scope,setScope]=React.useState('sel');
const [fmt,setFmt]=React.useState('dd1380');
const [inc,setInc]=React.useState({vitals:true,tx:true,meds:true,burn:true,tbi:true,notes:true,board:true,nine:true});
const [redact,setRedact]=React.useState(!!st.redact);
const [copied,setCopied]=React.useState(false);
const fileRef=React.useRef(null);
const now=new Date(), z=n=>String(n).padStart(2,'0');
const dtg = st.dtgZulu ? `${now.toISOString().slice(0,10).replace(/-/g,'')} ${z(now.getUTCHours())}${z(now.getUTCMinutes())}Z` : `${now.toISOString().slice(0,10).replace(/-/g,'')} ${z(now.getHours())}${z(now.getMinutes())}L`;
const o={...inc, redact, dtg, boardTitle: profile.boardTitle};
const multi = teamAll && teamAll.length > team.length;
const pool = scope==='all' ? teamAll : team;
const scopeFn = scope==='sel'?(x=>x.id===mb.id):scope==='cas'?(x=>memberStatus(x)!=='green'):(()=>true);
const list = pool.filter(scopeFn);
let text='', ext='txt', mime='text/plain';
if(fmt==='dd1380'){ text=[inc.board&&scope!=='sel'?buildTeamBoard(pool,o):null, ...list.map(x=>buildDD1380(x,o,S))].filter(Boolean).join('\n\n\n'); }
else if(fmt==='mist'){ text=list.map(x=>buildMISTText(x,o)).join('\n\n'); if(inc.nine&&S.nine){ const n=S.nine; text+=`\n\n9-LINE  L1 ${n.l1||'____'} · L2 ${n.l2||'____'}/${n.l2cs||'____'} · L4 ${n.l4||'A'} · L6 ${n.l6||'N'} · L7 ${n.l7||'C'} · L8 ${n.l8||'A'} · L9 ${n.l9||'____'}`; } }
else if(fmt==='board'){ text=buildTeamBoard(pool,o); }
else if(fmt==='csv'){ text=buildCSV(pool,scopeFn,o); ext='csv'; mime='text/csv'; }
else if(fmt==='json'){ const data = scope==='team'?S:{...S, team:list}; text=JSON.stringify(redact?{...data, team:data.team.map(x=>({...x,name:x.id,last4:''}))}:data,null,2); ext='json'; mime='application/json'; }
const fname=`TCCC360_${fmt}_${scope==='sel'?mb.role+'_'+(redact?mb.id:mb.name.split(',')[0]):scope}_${dtg.replace(/[ :]/g,'')}.${ext}`;
const lines=text.split('\n').length;
const act=(k)=>{ odaSnd('export',st); if(k==='copy'){ navigator.clipboard&&navigator.clipboard.writeText(text); setCopied(true); setTimeout(()=>setCopied(false),1500); } else if(k==='dl') download(fname,text,mime); else if(k==='print') printText(fname,text); };
const T=(k,l,d)=><Toggle key={k} label={l} desc={d} on={!!inc[k]} onChange={v=>{ odaSnd(v?'on':'off',st); setInc({...inc,[k]:v}); }}/>;
return <Drawer title="EXPORT" icon="⇪" sub={`${list.length} ${list.length===1?'record':'records'}`} onClose={onClose} accent="var(--amber)"
foot={<span className="dz-sub">{dtg}</span>}>
<Sec title="Scope">
<SegRow label="Who" opts={[['sel',`${mb.role} only`],['cas','Casualties'],['team',multi?`Whole ${profile.unitShort||'unit'}`:'Whole team'],...(multi?[['all','Whole Team']]:[])]} val={scope} onPick={v=>{odaSnd('seg',st); setScope(v);}}/>
</Sec>
<Sec title="Format">
<div className="dz-fmts">{[['dd1380','DD 1380','Full TCCC card text per casualty'],['mist','MIST','Radio-ready handover + 9-Line'],['board','Status board','Team sergeant one-pager'],['csv','Vitals CSV','Spreadsheet-ready trend log'],['json','JSON backup','Restorable full state']].map(([k,l,d])=><div key={k} className={'dz-fmt'+(fmt===k?' on':'')} onClick={()=>{odaSnd('seg',st); setFmt(k);}}><b>{l}</b><span>{d}</span></div>)}</div>
</Sec>
<Sec title="Include">
{fmt==='dd1380' && <>{T('vitals','Vitals log')}{T('tx','MARCH-PAWS treatments')}{T('meds','Meds & fluids')}{T('burn','Burn resuscitation')}{T('tbi','TBI screen')}{T('notes','Narrative notes')}{scope!=='sel'&&T('board','Team status board header')}</>}
{fmt==='mist' && T('nine','Append 9-Line')}
{(fmt==='board'||fmt==='csv'||fmt==='json') && <div className="dz-note">Nothing to configure for this format.</div>}
<Toggle label="Redact PII" desc="Role + slot id instead of name / last-4." on={redact} onChange={v=>{odaSnd(v?'on':'off',st); setRedact(v);}}/>
</Sec>
<Sec title="Preview" col="var(--fg-2)">
<div className="dz-prev"><div className="dz-prev-h"><span>{fname}</span><span>{lines} lines · {(text.length/1024).toFixed(1)} kB</span></div><pre>{text}</pre></div>
</Sec>
<div className="dz-actions">
<button className="dz-btn big" onClick={()=>act('copy')}>{copied?'✓ COPIED':'⧉ COPY'}</button>
<button className="dz-btn big primary" onClick={()=>act('dl')}>⤓ DOWNLOAD .{ext.toUpperCase()}</button>
<button className="dz-btn big" onClick={()=>act('print')}>⎙ PRINT / PDF</button>
</div>
<Sec title="Restore" col="var(--info)">
<div className="dz-row static"><div><div className="t">Import JSON backup</div><div className="d">Replaces the current team state with a previously exported JSON file.</div></div><button className="dz-btn" onClick={()=>fileRef.current&&fileRef.current.click()}>⤒ IMPORT</button><input ref={fileRef} type="file" accept=".json,application/json" style={{display:'none'}} onChange={e=>{ const f=e.target.files[0]; if(!f) return; const r=new FileReader(); r.onload=()=>{ try{ const d=JSON.parse(r.result); if(d&&Array.isArray(d.team)){ onImport(d); odaSnd('log',st); onClose(); } else alert('Not a TCCC 360 team backup.'); }catch(err){ alert('Could not parse file.'); } }; r.readAsText(f); e.target.value=''; }}/></div>
</Sec>
</Drawer>;
}
const THEMES = [
{ v:'tactical', l:'Tactical', d:'Night ops · dark, mono caps', bg:'#0a0c0d', fg:'#eef1f3', ln:'#262c31' },
{ v:'day', l:'Day', d:'Bright neutral for daylight', bg:'#f1f3f5', fg:'#14181c', ln:'#c9d0d6' },
{ v:'paper', l:'DD 1380 Paper', d:'Cream form · black rules', bg:'#f4efe1', fg:'#0a0a0a', ln:'#0a0a0a' },
];
const ACCENTS = [['op','Operational Green','oklch(.74 .14 152)'],['amber','Caution Amber','oklch(.8 .15 80)'],['crit','Critical Red','oklch(.66 .2 25)'],['info','Tactical Blue','oklch(.72 .1 230)']];
function DisplayDrawer({ t, setTweak, st, onClose }){
const pick=(k,v)=>{ odaSnd('seg',st); setTweak(k,v); };
const acc = ACCENTS.find(a=>a[0]===t.accent) || ACCENTS[0];
return <Drawer title="DISPLAY" icon="◐" sub={`${THEMES.find(x=>x.v===t.theme)?.l||''} · ${acc[1]}`} onClose={onClose} accent={acc[2]}>
<Sec title="Theme">
<div className="dz-themes">{THEMES.map(th=><div key={th.v} className={'dz-theme'+(t.theme===th.v?' on':'')} onClick={()=>pick('theme',th.v)}>
<div className="dz-thumb" style={{background:th.bg,borderColor:th.ln}}><i style={{background:th.ln}}></i><i style={{background:th.ln,width:'55%'}}></i><b style={{background:acc[2]}}></b><i style={{background:th.ln,width:'70%'}}></i><span style={{color:th.fg}}>Aa</span></div>
<b>{th.l}</b><span>{th.d}</span></div>)}</div>
</Sec>
<Sec title="Accent">
<div className="dz-swatches">{ACCENTS.map(([v,l,c])=><div key={v} className={'dz-sw'+(t.accent===v?' on':'')} onClick={()=>pick('accent',v)} title={l}><i style={{background:c,boxShadow:t.accent===v?`0 0 14px ${c}`:'none'}}></i><span>{l}</span></div>)}</div>
</Sec>
<Sec title="Layout">
<SegRow label="Density" desc="Padding and section label size." opts={[['compact','Compact'],['regular','Regular'],['roomy','Roomy']]} val={t.density} onPick={v=>pick('density',v)}/>
<Toggle label="Grid backdrop" desc="Faint tactical grid behind the panels (dark themes only)." on={!!t.showGrid} onChange={v=>{odaSnd(v?'on':'off',st); setTweak('showGrid',v);}}/>
<Toggle label="Vital sparklines" desc="Trend lines on the vitals tiles." on={!!t.showSparklines} onChange={v=>{odaSnd(v?'on':'off',st); setTweak('showSparklines',v);}}/>
</Sec>
<Sec title="Time">
<SegRow label="Clock display" desc="Status bar and DTG." opts={[['local','Local'],['zulu','Zulu'],['both','Both']]} val={t.timeMode} onPick={v=>pick('timeMode',v)}/>
</Sec>
<div className="dz-foot">Display preferences are saved with this dashboard</div>
</Drawer>;
}
Object.assign(window, { ODA_SETTINGS_DEFAULT, odaSnd, SettingsDrawer, ExportDrawer, DisplayDrawer, buildDD1380 });