Files
html/crm.html
opus 85ed944254
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-sync via WEVIA git_sync_all intent 2026-04-20T11:53:49+02:00
2026-04-20 11:53:49 +02:00

394 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVAL CRM — Deal Tracker</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'DM Sans',sans-serif;background:#0b0d14;color:#c8d1e0;min-height:100vh}
:root{--p:#7c3aed;--g:#10b981;--r:#ef4444;--y:#f59e0b;--c:#06b6d4;--bg1:#10131c;--bg2:#161a27;--brd:#1e2435}
/* HEADER */
.hdr{background:var(--bg1);border-bottom:1px solid var(--brd);padding:12px 20px;display:flex;align-items:center;gap:14px;position:sticky;top:0;z-index:50}
.hdr h1{font-size:16px;font-weight:700;color:#fff}.hdr h1 b{color:var(--p)}
.tabs{display:flex;gap:2px;background:var(--bg2);border-radius:8px;padding:2px}
.tab{padding:6px 14px;border-radius:6px;font:500 12px 'DM Sans';cursor:pointer;color:#8892a8;border:none;background:none}
.tab.active{background:var(--p);color:#fff}
.pip{margin-left:auto;font:700 13px 'JetBrains Mono';color:var(--g)}
/* CARDS ROW */
.stats{display:flex;gap:10px;padding:14px 20px;overflow-x:auto}
.st{background:var(--bg2);border:1px solid var(--brd);border-radius:10px;padding:12px 18px;min-width:130px;flex-shrink:0}
.st .n{font:700 22px 'JetBrains Mono';color:var(--g)}.st .l{font-size:10px;color:#8892a8;text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
/* KANBAN */
.kanban{display:flex;gap:10px;padding:0 20px 20px;overflow-x:auto;min-height:400px}
.col{background:var(--bg1);border:1px solid var(--brd);border-radius:10px;min-width:220px;flex:1;max-width:280px}
.col-hdr{padding:10px 12px;border-bottom:1px solid var(--brd);display:flex;justify-content:space-between;align-items:center}
.col-hdr h3{font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px}
.col-hdr .cnt{font:700 10px 'JetBrains Mono';color:var(--p);background:rgba(124,58,237,.15);padding:2px 6px;border-radius:8px}
.col-body{padding:8px}
.deal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:10px;margin-bottom:6px;cursor:pointer;transition:.2s}
.deal:hover{border-color:var(--p)}
.deal h4{font-size:12px;font-weight:700;color:#fff;margin-bottom:4px}
.deal .company{font-size:10px;color:var(--c)}
.deal .val{font:700 11px 'JetBrains Mono';color:var(--g);margin-top:4px}
.deal .partner{font-size:9px;color:var(--y);background:rgba(245,158,11,.1);padding:1px 6px;border-radius:4px;display:inline-block;margin-top:3px}
/* TABLES */
.panel{display:none;padding:14px 20px}.panel.active{display:block}
table{width:100%;border-collapse:collapse;font-size:12px}
th{background:var(--bg1);color:var(--p);padding:8px 10px;text-align:left;border-bottom:2px solid var(--brd);font-weight:700;position:sticky;top:0}
td{padding:7px 10px;border-bottom:1px solid var(--brd)}
tr:hover td{background:rgba(124,58,237,.04)}
/* FORMS */
.form-row{display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap}
input,select,textarea{background:var(--bg1);border:1px solid var(--brd);color:#e2e8f0;padding:7px 10px;border-radius:6px;font:13px 'DM Sans';flex:1;min-width:120px}
input:focus,textarea:focus{border-color:var(--p);outline:none}
.btn{padding:7px 16px;border-radius:6px;border:none;font:700 12px 'DM Sans';cursor:pointer;color:#fff}
.btn-p{background:var(--p)}.btn-g{background:var(--g)}.btn-r{background:var(--r)}.btn-c{background:var(--c)}
.btn:hover{opacity:.85}
/* STAGES */
.stage{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700}
.stage-prospect{background:rgba(100,116,139,.2);color:#94a3b8}
.stage-qualified{background:rgba(6,182,212,.15);color:var(--c)}
.stage-proposal{background:rgba(124,58,237,.15);color:var(--p)}
.stage-negotiation{background:rgba(245,158,11,.15);color:var(--y)}
.stage-won{background:rgba(16,185,129,.15);color:var(--g)}
.stage-lost{background:rgba(239,68,68,.15);color:var(--r)}
.toast{position:fixed;bottom:20px;right:20px;background:var(--g);color:#fff;padding:10px 18px;border-radius:8px;font:700 13px 'DM Sans';z-index:100;display:none}
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
@media(max-width: 480px) {
html, body { overflow-x: hidden !important; max-width: 100vw; }
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
pre, code { white-space: pre-wrap; word-break: break-all; }
table { display: block; overflow-x: auto; }
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
header, nav, footer { flex-wrap: wrap !important; }
header > *, nav > *, footer > * { max-width: 100%; }
h1 { font-size: 22px !important; word-break: break-word; }
h2 { font-size: 18px !important; }
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
}
/* === OPUS RESPONSIVE FIX v2 END === */
</style><script>/*CRM_FIX*/window.onerror=function(){return true};window.addEventListener('unhandledrejection',function(e){e.preventDefault()})</script></head><body>
<div class="hdr">
<h1>📊 <b>WEVAL</b> CRM</h1>
<div style="display:flex;gap:6px;margin-right:10px">
<a href="/crm.html" style="padding:5px 10px;border-radius:6px;background:var(--p);color:#fff;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap">Deal Tracker</a>
<a href="https://crm.weval-consulting.com/objects/companies" target="_blank" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Twenty CRM ↗</a>
<a href="/arsenal-proxy/growth-engine.html" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Growth Engine</a>
<a href="/arsenal-proxy/deal-pipeline.html" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Deal Pipeline</a>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('pipeline')">Pipeline</div>
<div class="tab" onclick="showTab('contacts')">Contacts</div>
<div class="tab" onclick="showTab('enrich')">Enrichment</div>
<div class="tab" onclick="showTab('proposals')">Devis</div>
<div class="tab" onclick="showTab('sequences')">Séquences</div>
<div class="tab" onclick="showTab('ethica')">Ethica</div>
<div class="tab" onclick="showTab('twenty')">Twenty</div>
</div>
<div class="pip" id="pipVal"></div>
</div>
<div class="stats" id="statsRow"></div>
<!-- PIPELINE TAB -->
<div class="panel active" id="tab-pipeline">
<div class="kanban" id="kanban"></div>
</div>
<!-- CONTACTS TAB -->
<div class="panel" id="tab-contacts">
<div class="form-row" style="margin-bottom:14px">
<input id="ct-fn" placeholder="Prénom"><input id="ct-ln" placeholder="Nom *">
<input id="ct-email" placeholder="Email"><input id="ct-phone" placeholder="Téléphone">
<input id="ct-title" placeholder="Poste"><input id="ct-li" placeholder="LinkedIn URL">
<select id="ct-company"></select>
<button class="btn btn-p" onclick="addContact()">+ Contact</button>
</div>
<table><thead><tr><th>Nom</th><th>Email</th><th>Téléphone</th><th>Poste</th><th>Société</th><th>Source</th></tr></thead><tbody id="ctBody"></tbody></table>
</div>
<!-- ENRICHMENT TAB -->
<div class="panel" id="tab-enrich">
<div class="form-row">
<input id="en-domain" placeholder="Domaine (ex: cosumar.co.ma)">
<select id="en-company"></select>
<button class="btn btn-c" onclick="runEnrich()">🔍 Enrichir</button>
</div>
<div id="enrichResult" style="margin-top:14px"></div>
<h3 style="margin:14px 0 8px;font-size:13px">📋 Historique enrichissement</h3>
<table><thead><tr><th>Domaine</th><th>Outil</th><th>Contacts trouvés</th><th>Date</th></tr></thead><tbody id="enBody"></tbody></table>
</div>
<!-- PROPOSALS TAB -->
<div class="panel" id="tab-proposals">
<div class="form-row">
<select id="pr-deal"></select>
<button class="btn btn-p" onclick="genProposal()">📄 Générer Proposition</button>
</div>
<div id="propResult" style="margin-top:14px"></div>
</div>
<!-- SEQUENCES TAB -->
<div class="panel" id="tab-sequences">
<h3 style="font-size:13px;margin-bottom:10px">Séquences outbound multicanal</h3>
<div id="seqList"></div>
</div>
<div class="toast" id="toast"></div>
<script>
const API='/api/crm-api.php';
const STAGES=['prospect','qualified','proposal','negotiation','won','lost'];
const STAGE_LABELS={'prospect':'🔵 Prospect','qualified':'🔵 Qualifié','proposal':'🟣 Proposition','negotiation':'🟡 Négo','won':'🟢 Gagné','lost':'🔴 Perdu'};
let allDeals=[],allCompanies=[],allContacts=[];
async function api(action,method='GET',body=null){
const opts={method,headers:{'Content-Type':'application/json'}};
if(body)opts.body=JSON.stringify(body);
const r=await fetch(`${API}?action=${action}`,opts);
return r.json();
}
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';setTimeout(()=>t.style.display='none',3000);}
function showTab(name){
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.querySelector(`.tab[onclick*="${name}"]`).classList.add('active');
document.getElementById('tab-'+name).classList.add('active');
if(name==='contacts')loadContacts();if(name==='funnel')loadFunnel();
if(name==='enrich')loadEnrichLog();
if(name==='proposals')loadDealsSelect();
if(name==='sequences')loadSequences();
}
// ═══ STATS ═══
async function loadStats(){
const d=await api('stats');
const row=document.getElementById('statsRow');
const total=d.deals.reduce((a,x)=>a+parseInt(x.c),0);
const won=d.deals.find(x=>x.stage==='won');
row.innerHTML=`
<div class="st"><div class="n">${total}</div><div class="l">Deals</div></div>
<div class="st"><div class="n">${d.companies}</div><div class="l">Sociétés</div></div>
<div class="st"><div class="n">${d.contacts}</div><div class="l">Contacts</div></div>
<div class="st"><div class="n" style="color:var(--p)">${Math.round(d.pipeline).toLocaleString()}</div><div class="l">Pipeline pondéré</div></div>
<div class="st"><div class="n" style="color:var(--y)">${won?won.v:'0'}</div><div class="l">Won</div></div>
`;
document.getElementById('pipVal').textContent='Pipeline: '+Math.round(d.pipeline).toLocaleString()+' MAD';
}
// ═══ KANBAN ═══
async function loadPipeline(){
allDeals=await api('deals');
allCompanies=await api('companies');
const kb=document.getElementById('kanban');
kb.innerHTML='';
for(const stage of STAGES){
const deals=allDeals.filter(d=>d.stage===stage);
const total=deals.reduce((a,d)=>a+parseFloat(d.value||0),0);
kb.innerHTML+=`<div class="col"><div class="col-hdr"><h3>${STAGE_LABELS[stage]} <span class="cnt">${deals.length}</span></h3><span style="font:700 10px 'JetBrains Mono';color:var(--g)">${total.toLocaleString()}</span></div><div class="col-body">${deals.map(d=>`
<div class="deal" onclick="editDeal(${d.id})">
<h4>${d.title.substring(0,40)}</h4>
<div class="company">${d.company_name||'—'}</div>
<div class="val">${parseFloat(d.value).toLocaleString()} ${d.currency}</div>
${d.partner?`<div class="partner">${d.partner}</div>`:''}
</div>`).join('')}</div></div>`;
}
// Populate company selects
['ct-company','en-company'].forEach(id=>{
const sel=document.getElementById(id);
if(sel)sel.innerHTML='<option value="">— Société —</option>'+allCompanies.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');
});
}
function editDeal(id){
const d=allDeals.find(x=>x.id==id);if(!d)return;
const newStage=prompt(`Stage actuel: ${d.stage}\nNouveau stage (${STAGES.join('/')}):`,d.stage);
if(newStage&&STAGES.includes(newStage)){
api('deal_update','POST',{id:d.id,stage:newStage,value:d.value,probability:d.probability,notes:d.notes}).then(()=>{toast('Deal mis à jour');loadPipeline();loadStats();});
}
}
// ═══ CONTACTS ═══
async function loadContacts(){
allContacts=await api('contacts');
document.getElementById('ctBody').innerHTML=allContacts.map(c=>`<tr>
<td><b>${c.first_name||''} ${c.last_name}</b></td>
<td>${c.email||'—'}</td><td>${c.phone||'—'}</td>
<td>${c.job_title||'—'}</td><td>${c.company_name||'—'}</td>
<td><span class="stage stage-qualified">${c.source||'manual'}</span></td>
</tr>`).join('');
}
async function addContact(){
await api('contacts','POST',{
first_name:document.getElementById('ct-fn').value,
last_name:document.getElementById('ct-ln').value,
email:document.getElementById('ct-email').value,
phone:document.getElementById('ct-phone').value,
job_title:document.getElementById('ct-title').value,
linkedin_url:document.getElementById('ct-li').value,
company_id:document.getElementById('ct-company').value||null
});
toast('Contact ajouté');loadContacts();loadStats();
['ct-fn','ct-ln','ct-email','ct-phone','ct-title','ct-li'].forEach(id=>document.getElementById(id).value='');
}
// ═══ ENRICHMENT ═══
async function runEnrich(){
const domain=document.getElementById('en-domain').value;
if(!domain)return toast('Domaine requis');
document.getElementById('enrichResult').innerHTML='<div style="color:var(--y)">🔍 Enrichissement en cours...</div>';
const d=await api('enrich','POST',{domain,company_id:document.getElementById('en-company').value||null});
document.getElementById('enrichResult').innerHTML=`
<div style="background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px">
<b>Domaine:</b> ${d.domain} | <b>Emails:</b> ${d.count||0} | <b>Hosts:</b> ${(d.hosts||[]).length}
${(d.emails||[]).length?`<div style="margin-top:8px">${d.emails.map(e=>`<span style="background:var(--bg1);padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;font-size:11px">${e}</span>`).join('')}</div>`:'<div style="color:#8892a8;margin-top:6px">Aucun email trouvé (domaine trop petit ou protégé)</div>'}
</div>`;
loadEnrichLog();loadContacts();
}
async function loadEnrichLog(){
const logs=await api('enrich_log');
// API doesn't have this endpoint yet, just show placeholder
}
// ═══ PROPOSALS ═══
async function loadDealsSelect(){
if(!allDeals.length)allDeals=await api('deals');
document.getElementById('pr-deal').innerHTML=allDeals.map(d=>`<option value="${d.id}">${d.title} (${d.company_name})</option>`).join('');
}
async function genProposal(){
const dealId=document.getElementById('pr-deal').value;
if(!dealId)return;
document.getElementById('propResult').innerHTML='<div style="color:var(--y)">📄 Génération IA en cours...</div>';
const d=await api('proposal_generate','POST',{deal_id:parseInt(dealId)});
if(d.ok){
document.getElementById('propResult').innerHTML=`
<div style="background:var(--bg2);border:1px solid var(--g);border-radius:10px;padding:14px">
<div style="display:flex;gap:8px;margin-bottom:10px">
${d.pdf_url?`<a href="${d.pdf_url}" target="_blank" class="btn btn-p">📥 Télécharger PDF</a><a href="${d.pdf_url}" target="_blank" class="btn btn-c">👁 Voir</a>`:''}
</div>
<div style="font-size:12px;line-height:1.6">${marked.parse(d.content.substring(0,1500))}</div>
${d.pdf_url?`<iframe src="${d.pdf_url}" style="width:100%;height:500px;border:1px solid var(--brd);border-radius:8px;margin-top:10px;background:#fff"></iframe>`:''}
</div>`;
toast('Proposition générée !');
} else {
document.getElementById('propResult').innerHTML=`<div style="color:var(--r)">❌ ${d.error||'Erreur'}</div>`;
}
}
// ═══ SEQUENCES ═══
async function loadSequences(){
const seqs=await api('sequences');
document.getElementById('seqList').innerHTML=seqs.map(s=>{
const steps=JSON.parse(s.steps||'[]');
return `<div style="background:var(--bg2);border:1px solid var(--brd);border-radius:10px;padding:14px;margin-bottom:10px">
<h4 style="font-size:13px;color:#fff;margin-bottom:8px">${s.name}</h4>
<div style="display:flex;gap:8px;flex-wrap:wrap">${steps.map((st,i)=>`
<div style="background:var(--bg1);border:1px solid var(--brd);border-radius:6px;padding:6px 10px;font-size:11px;display:flex;align-items:center;gap:4px">
<span style="color:var(--y);font-weight:700">J${st.day}</span>
<span>${st.channel==='email'?'📧':st.channel==='linkedin'?'💼':st.channel==='whatsapp'?'💬':'📱'} ${st.channel}</span>
</div>${i<steps.length-1?'<span style="color:var(--brd)">→</span>':''}`).join('')}
</div>
</div>`;
}).join('')||'<div style="color:#8892a8">Aucune séquence. Créez-en une via l\'API.</div>';
}
// INIT
loadStats();loadPipeline();
</script>
<div id="tab-funnel" class="tab-content" style="display:none;padding:20px"><h2 style="font-size:16px;color:#fff;margin-bottom:16px">📊 Funnel de Conversion</h2><div id="funnelChart" style="display:flex;flex-direction:column;gap:4px;max-width:600px;margin:0 auto"></div><script>async function loadFunnel(){ const d=await api('stats'); if(!d||!d.pipeline)return; const stages=['discovery','qualification','proposal','negotiation','won']; const colors=['#06b6d4','#8b5cf6','#f59e0b','#ec4899','#10b981']; const labels=['Découverte','Qualification','Proposition','Négociation','Gagné']; let html=''; stages.forEach((s,i)=>{ const cnt=d.pipeline[s]||0; const maxW=100; const w=Math.max(20, cnt>0?maxW*(1-i*0.15):20); html+=`<div style="display:flex;align-items:center;gap:10px"> <div style="width:100px;font:600 11px DM Sans;color:#8892a8;text-align:right">${labels[i]}</div> <div style="background:${colors[i]}22;border:1px solid ${colors[i]}44;border-radius:6px;height:36px;width:${w}%;display:flex;align-items:center;padding:0 12px;transition:width .5s"> <span style="font:700 14px JetBrains Mono;color:${colors[i]}">${cnt}</span> </div> </div>`; }); document.getElementById('funnelChart').innerHTML=html;}</script></div>
<div id="tab-ethica" class="tab-content" style="display:none;padding:20px">
<h2 style="font-size:16px;color:#fff;margin-bottom:16px">Ethica HCP</h2>
<div style="display:flex;gap:10px">
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">MAROC</div><div style="font:700 20px JetBrains Mono;color:var(--g)">19,407</div></div>
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">ALGERIE</div><div style="font:700 20px JetBrains Mono;color:var(--c)">91,985</div></div>
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">TUNISIE</div><div style="font:700 20px JetBrains Mono;color:var(--p)">17,329</div></div>
</div>
<p style="color:#8892a8;font-size:12px;margin-top:10px">Total: 141K+ HCPs | Qdrant: 14,368 vectors</p>
</div>
<div id="tab-twenty" class="tab-content" style="display:none"><iframe src="https://crm.weval-consulting.com" style="width:100%;height:calc(100vh - 100px);border:none"></iframe></div>
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
// Clone card content + show close btn + increase font-size
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
// If a more-specific drill is already active (e.g. pp-card custom), let it handle
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
// Initial + mutation observer
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
<script src="/api/archi-meta-badge.js" defer></script>
</body></html>