342 lines
19 KiB
HTML
342 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>WEVIA Command Center — Web-IA Orchestration</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;700&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#08080c;--panel:rgba(18,18,26,0.55);--panel-hover:rgba(28,28,40,0.78);
|
||
--border:rgba(255,255,255,0.06);--border-hover:rgba(246,213,114,0.4);
|
||
--ink:#ebe6d8;--ink-dim:#9a9384;--ink-faint:#5a5650;
|
||
--gold:#f6d572;--gold-deep:#b8923c;--coral:#ff6b5e;--mint:#7fffd4;--azure:#8ecfff;
|
||
--font-display:'Cormorant Garamond',serif;--font-mono:'JetBrains Mono',ui-monospace,monospace;
|
||
--ease:cubic-bezier(.23,1,.32,1);
|
||
}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html,body{background:var(--bg);color:var(--ink);font-family:var(--font-mono);min-height:100vh;overflow-x:hidden;font-weight:300;letter-spacing:.02em}
|
||
body::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 20% 10%,rgba(246,213,114,0.08) 0%,transparent 55%),radial-gradient(ellipse 60% 80% at 85% 90%,rgba(142,207,255,0.06) 0%,transparent 55%)}
|
||
body::after{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.035;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E")}
|
||
|
||
main{position:relative;z-index:1;min-height:100vh;padding:40px 48px 96px;max-width:1680px;margin:0 auto}
|
||
|
||
.masthead{display:grid;grid-template-columns:1fr auto;align-items:end;gap:40px;margin-bottom:44px;padding-bottom:28px;border-bottom:1px solid var(--border)}
|
||
.eyebrow{display:flex;align-items:center;gap:14px;color:var(--gold);font-size:10px;font-weight:500;letter-spacing:.42em;text-transform:uppercase;margin-bottom:16px}
|
||
.eyebrow::before{content:"";width:28px;height:1px;background:var(--gold)}
|
||
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--mint);box-shadow:0 0 12px var(--mint);animation:pulse 2s ease infinite}
|
||
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(1.2)}}
|
||
|
||
h1{font-family:var(--font-display);font-weight:300;font-size:clamp(40px,5vw,72px);line-height:.95;letter-spacing:-.03em}
|
||
h1 em{font-style:italic;font-weight:400;color:var(--gold)}
|
||
.subtitle{max-width:560px;margin-top:18px;font-size:13px;line-height:1.75;color:var(--ink-dim)}
|
||
|
||
.kpis{display:flex;gap:36px}
|
||
.kpi{text-align:right}
|
||
.kpi-value{font-family:var(--font-display);font-size:44px;font-weight:300;line-height:1;color:var(--gold);font-variant-numeric:tabular-nums}
|
||
.kpi-label{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-faint);margin-top:6px}
|
||
|
||
.command-bar{display:flex;align-items:center;gap:12px;padding:14px 18px;background:var(--panel);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:2px;margin-bottom:36px;font-size:11px}
|
||
.cb-prompt{color:var(--gold);font-weight:500;letter-spacing:.12em}
|
||
.cb-path{color:var(--ink-dim);flex:1}
|
||
.cb-btn{padding:7px 14px;background:transparent;border:1px solid var(--border);color:var(--ink-dim);font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;cursor:pointer;text-decoration:none;transition:all .3s var(--ease)}
|
||
.cb-btn:hover{border-color:var(--border-hover);color:var(--gold)}
|
||
.cb-btn.primary{background:var(--gold);color:#0a0a0f;border-color:var(--gold);font-weight:600}
|
||
|
||
.section-label{display:flex;align-items:center;gap:16px;font-size:10px;letter-spacing:.42em;text-transform:uppercase;color:var(--ink-faint);margin:28px 0 20px;font-weight:500}
|
||
.section-label::before,.section-label::after{content:"";flex:1;height:1px;background:var(--border)}
|
||
.section-label span{color:var(--gold)}
|
||
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}
|
||
|
||
.card{position:relative;padding:24px 26px;background:var(--panel);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:3px;overflow:hidden;opacity:0;animation:slideUp .8s var(--ease) forwards;transition:border-color .4s var(--ease),background .4s var(--ease)}
|
||
.card:nth-child(1){animation-delay:.05s}.card:nth-child(2){animation-delay:.1s}.card:nth-child(3){animation-delay:.15s}.card:nth-child(4){animation-delay:.2s}.card:nth-child(5){animation-delay:.25s}.card:nth-child(6){animation-delay:.3s}.card:nth-child(7){animation-delay:.35s}.card:nth-child(8){animation-delay:.4s}
|
||
@keyframes slideUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
|
||
.card:hover{border-color:var(--border-hover);background:var(--panel-hover)}
|
||
|
||
.card-head{display:flex;align-items:baseline;gap:14px;margin-bottom:6px}
|
||
.card-mono{font-family:var(--font-display);font-size:40px;font-weight:400;line-height:.85;color:var(--gold);font-style:italic;letter-spacing:-.03em}
|
||
.card-title{flex:1}
|
||
.card-name{font-family:var(--font-display);font-size:24px;font-weight:400;letter-spacing:-.01em;line-height:1.1}
|
||
.card-url{font-size:10px;color:var(--ink-dim);margin-top:3px;letter-spacing:.02em}
|
||
.card-badge{font-size:9px;letter-spacing:.18em;text-transform:uppercase;padding:3px 8px;border-radius:2px;border:1px solid var(--border);color:var(--ink-faint);font-weight:500}
|
||
|
||
.card-status{display:flex;align-items:center;gap:10px;padding:9px 12px;margin:16px 0 14px;background:rgba(255,255,255,0.02);border:1px solid var(--border);border-radius:2px;font-size:10px;letter-spacing:.1em;text-transform:uppercase}
|
||
.dot{width:6px;height:6px;border-radius:50%;background:var(--ink-faint);flex-shrink:0;transition:all .3s var(--ease)}
|
||
.dot.ok{background:var(--mint);box-shadow:0 0 8px var(--mint);animation:pulse 2.4s ease infinite}
|
||
.dot.warn{background:var(--gold);box-shadow:0 0 8px var(--gold);animation:pulse 1.8s ease infinite}
|
||
.dot.bad{background:var(--coral);box-shadow:0 0 8px var(--coral)}
|
||
.status-label{flex:1;color:var(--ink-dim)}
|
||
.status-label.ok{color:var(--mint)}.status-label.warn{color:var(--gold)}.status-label.bad{color:var(--coral)}
|
||
.status-meta{color:var(--ink-faint);font-size:10px}
|
||
|
||
/* Prompt area */
|
||
.prompt-row{display:flex;gap:8px;margin-bottom:12px}
|
||
.prompt-input{flex:1;background:rgba(0,0,0,0.3);border:1px solid var(--border);border-radius:2px;padding:10px 12px;color:var(--ink);font-family:var(--font-mono);font-size:12px;font-weight:300;letter-spacing:0;transition:border-color .3s var(--ease);resize:vertical;min-height:48px;max-height:200px}
|
||
.prompt-input::placeholder{color:var(--ink-faint);font-style:italic}
|
||
.prompt-input:focus{outline:none;border-color:var(--gold)}
|
||
|
||
.actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
|
||
.btn{padding:10px 12px;background:transparent;border:1px solid var(--border);color:var(--ink-dim);font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;cursor:pointer;transition:all .3s var(--ease);text-decoration:none;text-align:center;border-radius:2px;font-weight:500}
|
||
.btn:hover{background:rgba(246,213,114,0.08);border-color:var(--gold);color:var(--gold)}
|
||
.btn.send{background:var(--gold);color:#0a0a0f;border-color:var(--gold);font-weight:600}
|
||
.btn.send:hover{background:var(--ink);border-color:var(--ink)}
|
||
.btn:disabled{opacity:.35;cursor:not-allowed}
|
||
|
||
.card-result{margin-top:12px;padding:14px 16px;background:rgba(0,0,0,0.35);border:1px solid var(--border);border-radius:2px;max-height:280px;overflow-y:auto;font-size:12px;line-height:1.65;color:var(--ink);white-space:pre-wrap;word-wrap:break-word;display:none;position:relative}
|
||
.card-result.shown{display:block;animation:fadeIn .4s var(--ease)}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
||
.card-result::before{content:"RESPONSE";position:sticky;top:0;display:block;margin-bottom:10px;font-size:8px;letter-spacing:.42em;color:var(--gold);font-weight:600;opacity:.7}
|
||
.card-result-meta{display:flex;justify-content:space-between;margin-top:12px;padding-top:10px;border-top:1px solid var(--border);font-size:9px;letter-spacing:.18em;color:var(--ink-faint);text-transform:uppercase}
|
||
.card-result-meta a{color:var(--gold);text-decoration:none;border-bottom:1px solid rgba(246,213,114,0.3)}
|
||
|
||
/* Progress line */
|
||
.progress{height:2px;background:var(--border);overflow:hidden;margin:12px 0;border-radius:1px;display:none}
|
||
.progress.shown{display:block}
|
||
.progress::before{content:"";display:block;height:100%;background:linear-gradient(90deg,transparent 0%,var(--gold) 50%,transparent 100%);width:40%;animation:progSlide 1.2s ease infinite}
|
||
@keyframes progSlide{0%{transform:translateX(-100%)}100%{transform:translateX(350%)}}
|
||
|
||
/* Toasts */
|
||
.toast-stack{position:fixed;bottom:32px;right:32px;z-index:9999;display:flex;flex-direction:column-reverse;gap:10px}
|
||
.toast{min-width:320px;padding:14px 18px;background:rgba(18,18,26,0.95);backdrop-filter:blur(24px);border:1px solid var(--border);border-left:2px solid var(--gold);border-radius:2px;font-size:12px;color:var(--ink-dim);box-shadow:0 20px 40px -12px rgba(0,0,0,0.6);animation:toastIn .5s var(--ease) forwards}
|
||
.toast-title{color:var(--gold);font-size:10px;letter-spacing:.28em;text-transform:uppercase;margin-bottom:5px;font-weight:500}
|
||
.toast.success{border-left-color:var(--mint)}.toast.success .toast-title{color:var(--mint)}
|
||
.toast.error{border-left-color:var(--coral)}.toast.error .toast-title{color:var(--coral)}
|
||
@keyframes toastIn{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}
|
||
|
||
footer{margin-top:72px;padding-top:28px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:10px;letter-spacing:.12em;color:var(--ink-faint)}
|
||
footer a{color:var(--ink-dim);text-decoration:none}footer a:hover{color:var(--gold)}
|
||
|
||
@media(max-width:900px){main{padding:28px 20px 72px}.masthead{grid-template-columns:1fr}.kpis{justify-content:flex-start}}
|
||
</style>
|
||
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143941 -->
|
||
<style id="doctrine60-ux-direct">
|
||
|
||
/* DOCTRINE-60-UX-ENRICH injected-direct */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0; left: 0; width: 100vw; height: 100vh;
|
||
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
|
||
pointer-events: none;
|
||
z-index: -1;
|
||
}
|
||
.card, .kpi, .panel, .btn {
|
||
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
|
||
}
|
||
.card:hover, .kpi:hover, .panel:hover {
|
||
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
|
||
border-color: rgba(100,180,255,0.5);
|
||
}
|
||
@keyframes pulseD60 {
|
||
0%,100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.7; transform: scale(1.05); }
|
||
}
|
||
.pulse, .live-indicator, .active, .online {
|
||
animation: pulseD60 3s ease-in-out infinite;
|
||
}
|
||
.modal, .chat, .speech, .overlay {
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
}
|
||
.enter-stagger {
|
||
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
|
||
}
|
||
@keyframes enterStagD60 {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
|
||
<header class="masthead">
|
||
<div>
|
||
<div class="eyebrow"><span class="live-dot"></span> Doctrine 163 / 164 — Orchestration Live</div>
|
||
<h1>Web-IA<br><em>Command</em> Center</h1>
|
||
<p class="subtitle">Envoi de prompts aux 8 Web-IA gratuits via sessions persistantes. Selenium CDP attach — inférence illimitée sans coût API.</p>
|
||
</div>
|
||
<div class="kpis">
|
||
<div class="kpi"><div class="kpi-value" id="kpi-ok">0</div><div class="kpi-label">Sessions</div></div>
|
||
<div class="kpi"><div class="kpi-value" id="kpi-prompts">0</div><div class="kpi-label">Prompts</div></div>
|
||
<div class="kpi"><div class="kpi-value">8</div><div class="kpi-label">Providers</div></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="command-bar">
|
||
<span class="cb-prompt">wevia://</span>
|
||
<span class="cb-path">/web-ia/command-center</span>
|
||
<a class="cb-btn primary" href="/vnc-picker.html">VNC Picker</a>
|
||
<a class="cb-btn" href="/novnc/vnc.html" target="_blank">noVNC</a>
|
||
<a class="cb-btn" href="/wevia-audit.html">Audit</a>
|
||
<a class="cb-btn" href="javascript:refreshAll()">Refresh All</a>
|
||
</div>
|
||
|
||
<div class="section-label"><span>Providers · Send Prompts</span></div>
|
||
<div class="grid" id="grid"></div>
|
||
|
||
<footer>
|
||
<div>WEVIA AUTONOMY — v1.7 · DOCTRINES 146–164</div>
|
||
<div><a href="/wevia-audit.html">Audit</a> · <a href="/vnc-picker.html">Login</a> · <a href="https://weval-consulting.com">Home</a></div>
|
||
</footer>
|
||
|
||
</main>
|
||
<div class="toast-stack" id="toast-stack"></div>
|
||
|
||
<script>
|
||
const PROVIDERS=[
|
||
{slug:'openai',name:'ChatGPT',mono:'C',url:'https://chat.openai.com/'},
|
||
{slug:'anthropic',name:'Claude.ai',mono:'A',url:'https://claude.ai/'},
|
||
{slug:'google',name:'Gemini',mono:'G',url:'https://gemini.google.com/'},
|
||
{slug:'deepseek',name:'DeepSeek',mono:'D',url:'https://chat.deepseek.com/'},
|
||
{slug:'mistral',name:'Mistral',mono:'M',url:'https://chat.mistral.ai/'},
|
||
{slug:'poe',name:'Poe',mono:'P',url:'https://poe.com/'},
|
||
{slug:'perplexity',name:'Perplexity',mono:'Px',url:'https://www.perplexity.ai/'},
|
||
{slug:'hf',name:'HuggingFace',mono:'H',url:'https://huggingface.co/chat/'},
|
||
];
|
||
let promptsCount=0;
|
||
const state={};PROVIDERS.forEach(p=>state[p.slug]={status:'idle',response:null,busy:false,meta:''});
|
||
|
||
function toast(title,msg,kind){
|
||
const s=document.getElementById('toast-stack');
|
||
const t=document.createElement('div');
|
||
t.className='toast'+(kind?' '+kind:'');
|
||
t.innerHTML=`<div class="toast-title">${title}</div><div>${msg}</div>`;
|
||
s.appendChild(t);
|
||
setTimeout(()=>{t.style.opacity='0';t.style.transform='translateX(24px)';t.style.transition='all .4s';setTimeout(()=>t.remove(),400)},4600);
|
||
}
|
||
|
||
function render(){
|
||
const g=document.getElementById('grid');
|
||
g.innerHTML=PROVIDERS.map((p,i)=>{
|
||
const s=state[p.slug];
|
||
let dotCls='bad',lblCls='bad',lbl='No run yet',meta='idle';
|
||
if(s.status==='ok'){dotCls='ok';lblCls='ok';lbl='Session active';meta=s.meta||'ok'}
|
||
else if(s.status==='not_logged'){dotCls='warn';lblCls='warn';lbl='Login required';meta='not logged'}
|
||
else if(s.status==='running'){dotCls='warn';lblCls='warn';lbl='Running…';meta='in progress'}
|
||
else if(s.status==='error'){dotCls='bad';lblCls='bad';lbl='Error';meta=s.meta||'failed'}
|
||
const resShown=s.response?'shown':'';
|
||
return `<article class="card" data-slug="${p.slug}">
|
||
<div class="card-head">
|
||
<div class="card-mono">${p.mono}</div>
|
||
<div class="card-title">
|
||
<div class="card-name">${p.name}</div>
|
||
<div class="card-url">${p.url.replace('https://','')}</div>
|
||
</div>
|
||
<div class="card-badge">${String(i+1).padStart(2,'0')}/08</div>
|
||
</div>
|
||
<div class="card-status">
|
||
<div class="dot ${dotCls}"></div>
|
||
<div class="status-label ${lblCls}">${lbl}</div>
|
||
<div class="status-meta">${meta}</div>
|
||
</div>
|
||
<textarea class="prompt-input" id="pr-${p.slug}" placeholder="Type your prompt here…" rows="2"></textarea>
|
||
<div class="progress ${s.busy?'shown':''}" id="pg-${p.slug}"></div>
|
||
<div class="actions">
|
||
<a class="btn" href="/vnc-picker.html">Login</a>
|
||
<button class="btn send" onclick="send('${p.slug}')" ${s.busy?'disabled':''}>${s.busy?'Sending…':'Send'}</button>
|
||
<button class="btn" onclick="checkLatest('${p.slug}')">Check</button>
|
||
</div>
|
||
${s.response?`<div class="card-result shown" id="rs-${p.slug}">${escapeHtml(s.response.text)}<div class="card-result-meta"><span>${s.response.length||0} chars · ${s.response.mode||'?'}</span><a href="${s.response.proofs_url||'#'}" target="_blank">Proofs →</a></div></div>`:''}
|
||
</article>`;
|
||
}).join('');
|
||
updateKpis();
|
||
}
|
||
function escapeHtml(s){return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||
|
||
function updateKpis(){
|
||
const ok=Object.values(state).filter(s=>s.status==='ok').length;
|
||
document.getElementById('kpi-ok').textContent=String(ok).padStart(2,'0');
|
||
document.getElementById('kpi-prompts').textContent=String(promptsCount).padStart(2,'0');
|
||
}
|
||
|
||
async function send(slug){
|
||
const el=document.getElementById('pr-'+slug);
|
||
const prompt=(el?.value||'').trim();
|
||
if(!prompt){toast('Empty','Prompt required','error');return}
|
||
const prov=PROVIDERS.find(p=>p.slug===slug);
|
||
state[slug].busy=true;state[slug].status='running';state[slug].meta='starting…';render();
|
||
toast('Sending',`${prov.name} · ${prompt.length} chars`);
|
||
promptsCount++;updateKpis();
|
||
try{
|
||
const r=await fetch(`/api/wevia-autowire-trigger.php?action=send-prompt&provider=${slug}&prompt=${encodeURIComponent(prompt)}`);
|
||
const d=await r.json();
|
||
if(d.ok){
|
||
state[slug].meta='run '+d.run_id;render();
|
||
setTimeout(()=>checkLatest(slug,true),25000);
|
||
setTimeout(()=>checkLatest(slug,true),45000);
|
||
setTimeout(()=>checkLatest(slug,true),65000);
|
||
}else{
|
||
state[slug].busy=false;state[slug].status='error';state[slug].meta=d.err||'fail';render();
|
||
toast('Failed',d.err||'launch failed','error');
|
||
}
|
||
}catch(e){
|
||
state[slug].busy=false;state[slug].status='error';state[slug].meta='network';render();
|
||
toast('Network error',e.message,'error');
|
||
}
|
||
}
|
||
|
||
async function checkLatest(slug,auto){
|
||
const prov=PROVIDERS.find(p=>p.slug===slug);
|
||
try{
|
||
const r=await fetch('/api/wevia-autowire-trigger.php?action=send-prompt-latest&provider='+slug);
|
||
const d=await r.json();
|
||
if(!d.ok||!d.result){state[slug].status='idle';state[slug].meta='no runs';render();return}
|
||
const res=d.result;
|
||
if(d.status==='in_progress'){state[slug].status='running';state[slug].busy=true;state[slug].meta='running…';render();return}
|
||
state[slug].busy=false;
|
||
if(res.ok){
|
||
state[slug].status='ok';
|
||
state[slug].meta=(res.response_length||0)+' chars';
|
||
state[slug].response={text:res.response_text||'',length:res.response_length||0,mode:res.mode||'fresh',proofs_url:res.proofs_url};
|
||
if(!auto) toast('Response','+' +(res.response_length||0)+' chars','success');
|
||
}else if(res.err==='not_logged_in'){
|
||
state[slug].status='not_logged';state[slug].meta='login required';
|
||
if(!auto) toast('Login required',`${prov.name} · VNC picker`,'error');
|
||
}else{
|
||
state[slug].status='error';state[slug].meta=(res.err||'failed').slice(0,36);
|
||
}
|
||
render();
|
||
}catch(e){toast('Network',e.message,'error')}
|
||
}
|
||
|
||
function refreshAll(){PROVIDERS.forEach(p=>checkLatest(p.slug,true));toast('Refresh','Polling 8 providers…')}
|
||
|
||
render();
|
||
refreshAll();
|
||
setInterval(refreshAll,20000);
|
||
|
||
// Cursor tilt on cards
|
||
document.addEventListener('mousemove',e=>{
|
||
document.querySelectorAll('.card').forEach(c=>{
|
||
const r=c.getBoundingClientRect();
|
||
const inside=e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom;
|
||
if(inside){
|
||
const x=(e.clientX-r.left)/r.width-.5;const y=(e.clientY-r.top)/r.height-.5;
|
||
c.style.transform=`perspective(900px) rotateX(${-y*1.2}deg) rotateY(${x*1.2}deg)`;
|
||
}else{c.style.transform=''}
|
||
});
|
||
},{passive:true});
|
||
</script>
|
||
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
|
||
|
||
// DOCTRINE-60-UX-JS staggered entrance
|
||
(function(){
|
||
if (!('IntersectionObserver' in window)) return;
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach((e, i) => {
|
||
if (e.isIntersecting) {
|
||
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
|
||
obs.unobserve(e.target);
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
|
||
})();
|
||
|
||
</script>
|
||
</body></html>
|