trade-copilot / static /index.html
utkarshpathak48's picture
Sync website settings (account/risk/top/leverage) to Telegram alert loop
3e4e49f
Raw
History Blame Contribute Delete
215 kB
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<script>/* Theme restore — must run before first paint to prevent flash-of-wrong-theme */
(function(){try{var s=localStorage.getItem('tc_theme');if(s==='light'||s==='dark')document.documentElement.setAttribute('data-theme',s);}catch(e){}})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Trade Copilot — Deterministic Crypto Scanner · Zero Hallucination</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=Instrument+Serif:ital@1&family=JetBrains+Mono:wght@400;500;600;700&display=swap" onload="this.rel='stylesheet'">
<noscript><link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=Instrument+Serif:ital@1&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"></noscript>
<style>
/* ═══════════════════════════════════════════════
TOKENS
═══════════════════════════════════════════════ */
:root, [data-theme="dark"] {
color-scheme: dark;
/* ── Obsidian Glass core ── */
--bg: #06090f;
--bg2: #0a0f1a;
--white: rgba(255,255,255,0.06); /* legacy "solid surface" → subtle glass on dark */
--glass: rgba(255,255,255,0.04);
--glass2: rgba(255,255,255,0.065);
--glassbdr: rgba(255,255,255,0.085);
--gb: rgba(255,255,255,0.085); /* glass border */
--rim: rgba(255,255,255,0.2); /* top rim highlight */
--track: rgba(255,255,255,0.09); /* bar tracks / nested wells */
--shadow: 0 24px 60px -18px rgba(0,0,0,0.6);
--glare: rgba(255,255,255,0.09);
/* ── Typography ── */
--font-display: 'Sora', 'Inter', -apple-system, sans-serif;
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--font-serif: 'Instrument Serif', Georgia, serif;
/* Text — 4 levels */
--tx: #e9eef7;
--tx2: #98a3b8;
--tx3: #7a86a0;
--t1: var(--tx);
--t2: var(--tx2);
--t3: var(--tx3);
--t4: #5a6580;
--muted: var(--tx3);
/* ── Palette ── */
--mint: #3ddc97; --mints: rgba(61,220,151,0.11);
--red: #ff6b7a; --reds: rgba(255,107,122,0.10);
--amber: #f5b52a; --ambers: rgba(245,181,42,0.10);
--cyan: #4cc9f0; --cyans: rgba(76,201,240,0.10);
--violet: #9b7bff; --violets: rgba(155,123,255,0.12);
--grad: linear-gradient(120deg,#4cc9f0,#9b7bff 55%,#f072b6);
/* ── Legacy aliases — JS templates + component CSS depend on these ── */
--teal: var(--cyan); --teal2: var(--cyan); --teal3: var(--cyans); --teal-glow: var(--cyans);
--blue: var(--cyan); --blue2: var(--cyan); --blue3: var(--cyans);
--profit: var(--mint); --profit2: var(--mint); --profit3: var(--mints); --profit4: rgba(61,220,151,0.06);
--loss: var(--red); --loss2: var(--red); --loss3: var(--reds); --loss4: rgba(255,107,122,0.06);
--warn: var(--amber);--warn2: var(--amber);--warn3: var(--ambers);--warn4: rgba(245,181,42,0.06);
--green: var(--mint); --green2: var(--mint); --green3: var(--mints);
--red2: var(--red); --red3: var(--reds);
--amber2: var(--amber);--amber3: var(--ambers);
--purple: var(--violet); --purple2: var(--violet);
/* Radii */
--r: 10px;
--r2: 18px;
--r3: 24px;
/* Motion */
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
[data-theme="light"] {
color-scheme: light;
--bg: #edf0f6;
--bg2: #e3e8f1;
--white: #ffffff;
--glass: rgba(255,255,255,0.5);
--glass2: rgba(255,255,255,0.72);
--glassbdr: rgba(15,23,42,0.08);
--gb: rgba(15,23,42,0.08);
--rim: rgba(255,255,255,0.95);
--track: rgba(15,23,42,0.08);
--shadow: 0 24px 60px -26px rgba(30,41,66,0.22);
--glare: rgba(255,255,255,0.35);
--tx: #0f1729;
--tx2: #4e5d78;
--tx3: #5e6c88;
--t4: #9aa6bf;
--mint: #0b9e72; --mints: rgba(11,158,114,0.10);
--red: #dd4249; --reds: rgba(221,66,73,0.08);
--amber: #a1750a; --ambers: rgba(161,117,10,0.10);
--cyan: #0e7fa8; --cyans: rgba(14,127,168,0.09);
--violet: #6d4de0; --violets: rgba(109,77,224,0.09);
--grad: linear-gradient(120deg,#0e7fa8,#6d4de0 55%,#c2439a);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--t1);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
transition: background 0.5s, color 0.5s;
padding-bottom: calc(38px + env(safe-area-inset-bottom));
}
/* ═══════════════════════════════════════════════
AURORA BACKGROUND + GRAIN (Obsidian Glass)
═══════════════════════════════════════════════ */
.aurora { position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none; }
.a-orb {
position: absolute; border-radius: 50%;
filter: blur(50px); opacity: 0.33;
will-change: transform;
animation: auroraDrift 28s ease-in-out infinite alternate;
}
.a1 { width: 52vw; height: 52vw; left: -12vw; top: -18vh; background: radial-gradient(circle, #0f3e52, transparent 65%); }
.a2 { width: 44vw; height: 44vw; right: -10vw; top: 8vh; background: radial-gradient(circle, #341a6b, transparent 65%); animation-delay: -9s; }
.a3 { width: 42vw; height: 42vw; left: 26vw; bottom: -22vh; background: radial-gradient(circle, #0c4234, transparent 65%); animation-delay: -17s; }
[data-theme="light"] .a-orb { opacity: 0.5; }
[data-theme="light"] .a1 { background: radial-gradient(circle, #bcdffc, transparent 65%); }
[data-theme="light"] .a2 { background: radial-gradient(circle, #d8ccff, transparent 65%); }
[data-theme="light"] .a3 { background: radial-gradient(circle, #bff1dd, transparent 65%); }
@keyframes auroraDrift {
0% { transform: translate(0,0) scale(1); }
50% { transform: translate(6vw,-4vh) scale(1.1); }
100% { transform: translate(-4vw,5vh) scale(0.96); }
}
.grain {
position: fixed; inset: 0; z-index: -1; pointer-events: none;
opacity: 0.045; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ═══════════════════════════════════════════════
NAVBAR
═══════════════════════════════════════════════ */
nav {
position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
z-index: 300;
width: calc(100% - 40px); max-width: 1400px;
display: flex; align-items: center; gap: 10px;
padding: 9px 16px;
background: var(--glass);
backdrop-filter: blur(22px) saturate(160%);
-webkit-backdrop-filter: blur(22px) saturate(160%);
border: 1px solid var(--gb);
border-radius: 18px;
box-shadow: 0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04), inset 0 1px 0 var(--rim);
animation: navDrop 0.7s var(--ease) both;
}
@keyframes navDrop {
from { opacity:0; transform:translateX(-50%) translateY(-20px); }
to { opacity:1; transform:translateX(-50%) translateY(0); }
}
.brand { display:flex; align-items:center; gap:9px; text-decoration:none; flex-shrink:0; }
.brand-mark {
width:34px; height:34px;
background: linear-gradient(135deg, var(--teal), var(--blue2));
border-radius:10px;
display:grid; place-items:center;
font-size:17px;
box-shadow: 0 4px 14px rgba(76,201,240,0.4), inset 0 1px 0 var(--rim);
transition: transform 0.2s var(--spring), box-shadow 0.2s;
}
.brand-mark:hover { transform: scale(1.1) rotate(-5deg); box-shadow: 0 8px 20px rgba(76,201,240,0.5); }
.brand-name { font-size:15px; font-weight:700; letter-spacing:-0.4px; color:var(--t1); font-family:var(--font-display); }
#status-pill {
display:inline-flex; align-items:center; gap:5px;
font-size:10px; font-weight:700; letter-spacing:0.3px;
padding:4px 10px; border-radius:99px;
background:var(--glass2); border:1px solid var(--gb);
color:var(--t3); transition:all 0.4s var(--ease);
}
#status-pill.ok { color:var(--green); background:var(--mints); border-color:rgba(61,220,151,0.2); }
#status-pill.err { color:var(--red); background:var(--reds); border-color:rgba(255,107,122,0.2); }
.live-dot { width:5px; height:5px; border-radius:50%; background:currentColor; flex-shrink:0; }
.ok .live-dot { animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{box-shadow:0 0 0 0 rgba(61,220,151,0.5)} 50%{box-shadow:0 0 0 5px rgba(61,220,151,0)} }
/* Fear & Greed pill */
#fng-pill {
display:inline-flex; align-items:center; gap:5px;
font-size:10px; font-weight:700; letter-spacing:0.3px;
padding:4px 10px; border-radius:99px;
background:var(--glass2); border:1px solid var(--gb);
cursor:default; transition:all 0.4s var(--ease);
white-space:nowrap;
}
#fng-pill.fng-fear { color:var(--loss); background:var(--reds); border-color:rgba(255,107,122,0.2); }
#fng-pill.fng-xfear { color:var(--loss); background:var(--reds); border-color:rgba(255,107,122,0.3); font-weight:800; }
#fng-pill.fng-greed { color:var(--warn); background:var(--ambers); border-color:rgba(245,181,42,0.2); }
#fng-pill.fng-xgreed { color:var(--warn); background:var(--ambers); border-color:rgba(245,181,42,0.3); font-weight:800; }
#fng-pill.fng-neutral { color:var(--t2); background:var(--glass2); border-color:var(--gb); }
/* News sentiment badge on card */
.badge-news { font-size:10px; font-weight:700; border-radius:6px; padding:3px 8px; }
.badge-news.news-bullish { color:var(--profit); background:var(--mints); border:1px solid rgba(61,220,151,0.2); }
.badge-news.news-bearish { color:var(--loss); background:var(--reds); border:1px solid rgba(255,107,122,0.2); }
.badge-news.news-neutral { color:var(--t3); background:var(--track); border:1px solid var(--gb); }
.badge-news.news-nokey { color:var(--t4); background:var(--track); border:1px dashed var(--gb); }
.nav-spacer { flex:1; }
.nav-controls { display:flex; align-items:center; gap:6px; flex-wrap:nowrap; }
.chip {
display:flex; flex-direction:column; gap:2px; align-items:flex-start;
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:5px 10px;
transition:border-color 0.2s, box-shadow 0.2s, background 0.2s, transform 0.2s;
box-shadow: inset 0 1px 0 var(--rim);
}
.chip:focus-within {
border-color:rgba(76,201,240,0.4);
box-shadow:0 0 0 3px rgba(76,201,240,0.1), inset 0 1px 0 var(--rim);
background:var(--glass2);
transform:translateY(-1px);
}
.chip label { font-size:8px; font-weight:800; letter-spacing:0.9px; text-transform:uppercase; color:var(--t3); white-space:nowrap; user-select:none; }
.chip input, .chip select {
background:none; border:none; outline:none;
color:var(--t1); font-family:var(--font-mono); font-size:13px; font-weight:700;
-moz-appearance:textfield;
}
.chip input::-webkit-inner-spin-button,
.chip input::-webkit-outer-spin-button { -webkit-appearance:none; }
.chip input { width:56px; text-align:right; }
.chip select {
cursor:pointer; width:auto; appearance:none; padding-right:16px;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%2398a3b8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 3px center;
}
#scan-btn {
display:inline-flex; align-items:center; gap:7px;
background:linear-gradient(135deg, var(--teal) 0%, #22a5cc 100%);
color:#fff; border:none;
padding:10px 20px; border-radius:12px;
font-size:13px; font-weight:800; font-family:var(--font); letter-spacing:-0.2px;
cursor:pointer; white-space:nowrap;
transition:transform 0.2s var(--spring), box-shadow 0.2s, opacity 0.2s;
box-shadow:0 4px 18px rgba(76,201,240,0.4), inset 0 1px 0 var(--rim);
position:relative; overflow:hidden;
}
#scan-btn::after {
content:''; position:absolute; inset:0;
background:linear-gradient(135deg, rgba(255,255,255,0.15), transparent);
border-radius:12px;
}
#scan-btn:hover { transform:translateY(-1px) scale(1.02); box-shadow:0 8px 28px rgba(76,201,240,0.5), inset 0 1px 0 var(--rim); }
#scan-btn:active { transform:scale(0.97); }
#scan-btn:disabled { opacity:0.5; cursor:not-allowed; transform:none; box-shadow:none; }
/* countdown */
#refresh-countdown {
display:none; font-size:11px; font-weight:700; letter-spacing:0.2px; color:var(--t3);
background:var(--glass2); border:1px solid var(--gb);
border-radius:8px; padding:5px 9px; white-space:nowrap;
}
#refresh-countdown.urgent { color:var(--amber); border-color:rgba(245,181,42,0.2); background:var(--amber3); }
/* Countdown ring — conic-gradient dot showing progress to next auto-refresh */
#refresh-countdown { position:relative; padding-left:22px; }
#refresh-countdown::before {
content:''; position:absolute; left:7px; top:50%; transform:translateY(-50%);
width:11px; height:11px; border-radius:50%;
background:conic-gradient(var(--teal) calc(var(--pct,0)*1%), var(--track) 0);
transition:background 0.3s linear;
}
#refresh-countdown.urgent::before { background:conic-gradient(var(--amber) calc(var(--pct,0)*1%), var(--track) 0); }
/* Idle-life engagement pulse (ADHD-friendly layer) */
.idle-pulse { animation: idleGlow 2.4s var(--ease-out); }
@keyframes idleGlow {
0%,100% { filter:none; }
50% { filter:drop-shadow(0 0 8px currentColor) brightness(1.3); }
}
@media (prefers-reduced-motion: reduce) {
.idle-pulse { animation:none; }
}
/* ═══════════════════════════════════════════════
HERO
═══════════════════════════════════════════════ */
.hero {
position:relative; z-index:1;
padding:130px 24px 70px;
text-align:center; max-width:860px; margin:0 auto;
}
.eyebrow {
display:inline-flex; align-items:center; gap:8px;
font-size:10.5px; font-weight:700; letter-spacing:.18em; text-transform:uppercase;
color:var(--teal);
background:var(--cyans); border:1px solid rgba(76,201,240,0.2);
padding:8px 17px; border-radius:99px; margin-bottom:26px;
animation:slideUp 0.6s var(--ease) 0.3s both;
}
.eyebrow-dot { width:5px; height:5px; border-radius:50%; background:var(--teal); animation:pulse 2s infinite; }
.hero-title {
font-size:clamp(44px,6.5vw,82px);
font-family: var(--font-display);
font-weight:800; letter-spacing:-.045em; line-height:1.0;
color:var(--t1); margin-bottom:20px;
animation:slideUp 0.6s var(--ease) 0.4s both;
}
.hero-title .line2 {
display:block;
/* Teal → Blue only — on-brand, conveys trust + innovation, no purple distraction */
background:linear-gradient(135deg, var(--teal) 0%, var(--teal2) 35%, var(--blue2) 70%, var(--blue) 100%);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
background-size:200% 200%; animation:gradShift 5s ease infinite;
}
@keyframes gradShift { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
.hero-sub {
/* Body text: Inter, 16px minimum, 1.7 line-height, secondary text color */
font-size:16px; line-height:1.75; color:var(--t2); font-weight:400;
max-width:500px; margin:0 auto 38px; letter-spacing:0.01em;
animation:slideUp 0.6s var(--ease) 0.5s both;
}
.hero-btns {
display:flex; justify-content:center; gap:10px; flex-wrap:wrap;
animation:slideUp 0.6s var(--ease) 0.6s both;
}
.btn-hero {
display:inline-flex; align-items:center; gap:9px;
background:linear-gradient(135deg, var(--teal), #22a5cc);
color:#fff; border:none; padding:16px 36px; border-radius:16px;
font-size:16px; font-weight:800; font-family:var(--font); letter-spacing:-0.3px;
cursor:pointer; position:relative; overflow:hidden;
transition:transform 0.2s var(--spring), box-shadow 0.2s;
box-shadow:0 8px 32px rgba(76,201,240,0.4), inset 0 1px 0 var(--rim);
}
.btn-hero::before {
content:''; position:absolute; top:-50%; left:-60%; width:40%; height:200%;
background:rgba(255,255,255,0.22); transform:skewX(-20deg);
transition:left 0.5s var(--ease);
}
.btn-hero:hover { transform:translateY(-2px); box-shadow:0 16px 48px rgba(76,201,240,0.5); }
.btn-hero:hover::before { left:130%; }
.btn-hero:active { transform:scale(0.97); }
.btn-hero:disabled { opacity:0.45; cursor:not-allowed; transform:none; }
.btn-ghost2 {
display:inline-flex; align-items:center; gap:8px;
background:var(--glass2); border:1px solid var(--gb);
color:var(--t2); padding:16px 28px; border-radius:16px;
font-size:16px; font-weight:700; font-family:var(--font);
cursor:pointer;
transition:transform 0.2s var(--spring), box-shadow 0.2s, border-color 0.2s;
box-shadow:0 2px 12px rgba(0,0,0,0.06), inset 0 1px 0 var(--rim);
}
.btn-ghost2:hover { transform:translateY(-1px); border-color:rgba(76,201,240,0.3); box-shadow:0 8px 24px rgba(0,0,0,0.08); }
/* Stats row */
.hero-stats {
display:flex; justify-content:center; gap:10px; flex-wrap:wrap;
margin-top:52px;
animation:slideUp 0.6s var(--ease) 0.75s both;
}
.stat-glass {
background:var(--glass); border:1px solid var(--glassbdr); border-radius:18px;
padding:16px 24px; min-width:120px; text-align:center;
box-shadow:0 4px 24px rgba(0,0,0,0.06), inset 0 1px 0 var(--rim);
transition:transform 0.25s var(--spring), box-shadow 0.25s;
cursor:default;
}
.stat-glass:hover { transform:translateY(-4px); box-shadow:0 12px 36px rgba(0,0,0,0.1), inset 0 1px 0 var(--rim); }
.stat-num { font-size:26px; font-weight:700; letter-spacing:-1px; color:var(--t1); line-height:1; font-family:var(--font-display); font-variant-numeric:tabular-nums; }
.stat-lbl { font-size:10px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; color:var(--t3); margin-top:5px; }
/* Ticker tape */
.ticker-wrap {
position:fixed; left:0; right:0; bottom:0; z-index:90;
overflow:hidden; height:calc(38px + env(safe-area-inset-bottom));
padding-bottom:env(safe-area-inset-bottom);
background:var(--glass2);
border-top:1px solid var(--gb);
box-shadow:inset 0 1px 0 var(--rim);
}
.ticker-inner {
display:flex; align-items:center; gap:0;
white-space:nowrap; height:100%; width:max-content;
animation:ticker 30s linear infinite;
mask-image:linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
-webkit-mask-image:linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
}
.ticker-inner:hover { animation-play-state:paused; }
@keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} }
.tick-item {
display:inline-flex; align-items:center; gap:6px;
padding:0 24px; font-size:12px; font-weight:700; letter-spacing:0.1px;
border-right:1px solid var(--gb);
}
.tick-sym { color:var(--t3); font-weight:600; letter-spacing:0.3px; }
.tick-price { color:var(--t1); font-family:var(--font-display); font-variant-numeric:tabular-nums; font-weight:600; }
.tick-chg { font-size:11px; font-variant-numeric:tabular-nums; }
.tick-chg.up { color:var(--green); }
.tick-chg.dn { color:var(--red); }
@keyframes slideUp {
from { opacity:0; transform:translateY(30px); }
to { opacity:1; transform:translateY(0); }
}
/* ═══════════════════════════════════════════════
SIGNALS SECTION
═══════════════════════════════════════════════ */
#signals-section {
position:relative; z-index:1;
max-width:1500px; margin:0 auto;
padding:32px 24px 8px;
}
.dot-amber { background:var(--warn) !important; animation:pulseAmber 2s infinite !important; }
@keyframes pulseAmber { 0%,100%{box-shadow:0 0 0 0 rgba(245,181,42,0.5)} 50%{box-shadow:0 0 0 6px rgba(245,181,42,0)} }
.signals-meta-txt { font-size:12px; color:var(--t3); font-weight:600;
background:var(--glass2); border:1px solid var(--gb);
border-radius:99px; padding:4px 12px; }
.signals-refresh-btn {
display:inline-flex; align-items:center; justify-content:center;
width:30px; height:30px; border-radius:8px; border:1px solid var(--gb);
background:var(--glass2); color:var(--t3); cursor:pointer;
transition:color 0.2s, transform 0.3s var(--spring);
}
.signals-refresh-btn:hover { color:var(--teal); transform:rotate(180deg); }
.signals-refresh-btn.spinning svg { animation:spin 0.8s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
@keyframes ticker-scroll { 0%{transform:translateX(0)} 100%{transform:translateX(-50%)} }
.signals-loader {
display:flex; align-items:center; gap:12px;
padding:20px; color:var(--t3); font-size:13px; font-weight:500;
}
.signals-spinner {
width:18px; height:18px; border-radius:50%;
border:2px solid var(--ambers); border-top-color:var(--warn);
animation:spin 0.8s linear infinite; flex-shrink:0;
}
.signals-empty {
display:flex; align-items:center; gap:10px;
padding:20px; color:var(--t3); font-size:13px; font-weight:500;
background:var(--glass2); border-radius:14px;
border:1px dashed var(--gb);
}
/* Signal cards strip — horizontal scroll */
.signals-grid {
display:flex;
gap:14px;
overflow-x:auto;
padding:4px 2px 14px;
margin-top:4px;
scrollbar-width:none;
}
.signals-grid::-webkit-scrollbar { display:none; }
/* Individual signal card */
.sig-card {
background:var(--glass2);
will-change:transform;
border:1px solid var(--gb);
border-radius:18px;
padding:18px;
box-shadow:0 4px 20px rgba(0,0,0,0.07), inset 0 1px 0 var(--rim);
animation:cardIn 0.4s var(--ease) both;
position:relative; overflow:hidden;
min-width:270px; flex:0 0 auto;
}
.sig-card::before {
content:''; position:absolute; top:0; left:0; right:0; height:3px;
border-radius:18px 18px 0 0;
}
.sig-card.urgency-critical::before { background:var(--red); }
.sig-card.urgency-high::before { background:linear-gradient(90deg, var(--warn), var(--warn2)); }
.sig-card.urgency-medium::before { background:linear-gradient(90deg, var(--teal), var(--teal2)); }
/* Critical cards pulse border */
.sig-card.urgency-critical {
box-shadow:0 4px 20px rgba(255,107,122,0.12), 0 0 0 1px rgba(255,107,122,0.1),
inset 0 1px 0 var(--rim);
}
.sig-header { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:12px; }
.sig-sym { font-size:22px; font-weight:700; font-family:var(--font-display); letter-spacing:-0.5px; color:var(--t1); }
.sig-sym .q { font-size:13px; color:var(--t3); font-weight:500; margin-left:2px; }
.sig-dir {
font-size:11px; font-weight:800; letter-spacing:0.5px;
padding:4px 10px; border-radius:8px;
}
.sig-dir.long { background:var(--mints); color:var(--profit); border:1px solid rgba(61,220,151,0.2); }
.sig-dir.short { background:var(--reds); color:var(--loss); border:1px solid rgba(255,107,122,0.2); }
.sig-dir.neutral{ background:var(--track); color:var(--t3); border:1px solid var(--gb); }
/* Move type badges */
.sig-types { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
.sig-type-badge {
font-size:10px; font-weight:700; letter-spacing:0.3px;
padding:3px 8px; border-radius:6px;
}
.sig-type-badge.breakout { background:var(--cyans); color:var(--blue); border:1px solid rgba(76,201,240,0.15); }
.sig-type-badge.acceleration { background:var(--cyans); color:var(--teal); border:1px solid rgba(76,201,240,0.15); }
.sig-type-badge.reversal { background:var(--ambers); color:var(--warn); border:1px solid rgba(245,181,42,0.15); }
.sig-type-badge.news { background:var(--violets); color:var(--violet); border:1px solid rgba(155,123,255,0.15); }
/* Score bar */
.sig-score-row { display:flex; align-items:center; gap:8px; margin-bottom:12px; }
.sig-score-lbl { font-size:10px; font-weight:800; letter-spacing:0.6px; text-transform:uppercase; color:var(--t3); white-space:nowrap; }
.sig-score-track { flex:1; height:6px; background:var(--track); border-radius:3px; overflow:hidden; }
.sig-score-bar { height:100%; border-radius:3px; transition:width 1s var(--ease) 0.3s; width:0; }
.sig-score-bar.urgency-critical { background:var(--red); }
.sig-score-bar.urgency-high { background:linear-gradient(90deg, var(--warn), var(--warn2)); }
.sig-score-bar.urgency-medium { background:linear-gradient(90deg, var(--teal), var(--teal2)); }
.sig-score-num { font-size:12px; font-weight:800; font-family:var(--font-display); color:var(--t1); white-space:nowrap; }
/* Reasons list */
.sig-reasons { display:flex; flex-direction:column; gap:4px; }
.sig-reason {
font-size:11px; color:var(--t2); font-weight:500; line-height:1.4;
padding:4px 8px; background:var(--track); border-radius:6px;
border-left:2px solid var(--gb);
}
.sig-reason.warn { border-left-color:var(--warn2); color:var(--warn); }
.sig-reason.bull { border-left-color:var(--profit2); }
.sig-reason.bear { border-left-color:var(--loss2); }
/* Trade it button */
.sig-cta {
display:inline-flex; align-items:center; gap:6px;
margin-top:12px; width:100%;
background:linear-gradient(135deg, var(--teal), #22a5cc);
color:#fff; border:none; border-radius:10px;
padding:9px 14px; font-size:12px; font-weight:700;
cursor:pointer; justify-content:center;
transition:transform 0.2s var(--spring), box-shadow 0.2s;
box-shadow:0 3px 12px rgba(76,201,240,0.35);
}
.sig-cta:hover { transform:translateY(-1px); box-shadow:0 6px 18px rgba(76,201,240,0.45); }
/* ═══════════════════════════════════════════════
SCAN SECTION
═══════════════════════════════════════════════ */
#scan-section {
position:relative; z-index:1;
max-width:1500px; margin:0 auto;
padding:32px 24px 100px;
}
.section-header {
display:flex; align-items:center; justify-content:space-between;
margin-bottom:20px; flex-wrap:wrap; gap:10px;
}
/* ── Cycle Tracker — Section Card ── */
#cycle-section {
background:var(--white);
border:1px solid var(--bg2);
border-radius:16px;
padding:24px;
isolation:isolate;
}
/* ── Cycle Tracker — Tabs ── */
.cycle-tabs-wrap {
display:flex; gap:6px; margin-bottom:16px;
border-bottom:2px solid var(--bg2); padding-bottom:0;
}
.cycle-tab {
padding:9px 22px; border-radius:8px 8px 0 0;
border:1.5px solid transparent; border-bottom:none;
background:transparent; color:var(--t3); font-size:13px; font-weight:600;
cursor:pointer; transition:all .18s ease; font-family:var(--font-display);
margin-bottom:-2px; position:relative;
}
.cycle-tab:hover { color:var(--blue2); background:var(--track); }
.cycle-tab.active {
background:var(--white); color:var(--blue2);
border-color:var(--bg2); border-bottom-color:var(--white);
box-shadow:0 -2px 8px rgba(76,201,240,.08);
}
/* ── Cycle Tracker — Banner (on-system blue) ── */
.cycle-banner {
border-radius:14px; padding:20px 24px; margin-bottom:14px;
background:linear-gradient(135deg,#1e3a5f 0%,#1e40af 100%);
color:#fff;
display:flex; align-items:center; justify-content:space-between;
flex-wrap:wrap; gap:12px;
}
.cycle-banner-label { font-size:10px; font-weight:700; letter-spacing:.08em; opacity:.65; text-transform:uppercase; }
.cycle-banner-phase { font-size:22px; font-weight:800; font-family:var(--font-display); margin-top:3px; }
.cycle-banner-sub { font-size:12px; opacity:.75; margin-top:4px; }
.cycle-banner-count { font-size:28px; font-weight:800; font-family:var(--font-display); color:#f5b52a; }
.cycle-banner-hint { font-size:11px; opacity:.65; margin-top:3px; }
/* SOL banner — amber since limited data */
.cycle-banner.sol { background:linear-gradient(135deg,#78350f 0%,#b45309 100%); }
/* ── Cycle Tracker — Progress bar ── */
.cycle-progress-wrap {
background:var(--track); border:1px solid var(--bg2); border-radius:14px;
padding:16px 20px; margin-bottom:12px;
}
.cycle-progress-labels {
display:flex; justify-content:space-between; margin-top:6px;
}
.cycle-progress-labels span { font-size:11px; color:var(--t3); }
/* ── Cycle Tracker — Stat cards ── */
.cycle-stats-row {
display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:10px; margin-bottom:12px;
}
.cycle-stat {
background:var(--track); border:1px solid var(--bg2); border-radius:12px; padding:16px;
}
.cycle-stat-lbl { font-size:10px; font-weight:700; color:var(--t3); letter-spacing:.06em; text-transform:uppercase; margin-bottom:6px; }
.cycle-stat-val { font-size:20px; font-weight:800; font-family:var(--font-display); color:var(--t1); }
.cycle-stat-val.loss { color:var(--loss); }
.cycle-stat-val.warn { color:var(--warn); }
.cycle-stat-sub { font-size:11px; color:var(--t3); margin-top:3px; line-height:1.4; }
/* ── Cycle Tracker — Table ── */
.cycle-table-wrap {
background:var(--track); border:1px solid var(--bg2); border-radius:14px;
overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch;
margin-bottom:14px;
}
.cycle-table-hdr { padding:13px 18px; border-bottom:1px solid var(--bg2); font-size:12px; font-weight:700; color:var(--t2); white-space:nowrap; }
.cycle-table { width:100%; min-width:460px; border-collapse:collapse; font-size:12px; }
.cycle-table thead tr { background:var(--bg2); }
.cycle-table th { padding:9px 14px; text-align:right; color:var(--t3); font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; }
.cycle-table th:first-child { text-align:left; }
.cycle-table td { padding:11px 14px; text-align:right; border-top:1px solid var(--bg2); font-variant-numeric:tabular-nums; }
.cycle-table td:first-child { text-align:left; font-weight:600; color:var(--t1); }
.cycle-table tr.alt { background:var(--white); }
.cycle-table tr.now-row { background:rgba(245,181,42,.07); }
.cycle-table tr.now-row td { border-top:2px dashed var(--warn3); }
.cycle-table tr.now-row td:first-child { color:var(--warn); font-weight:700; }
.badge-bear { background:var(--loss3); color:var(--loss); padding:3px 9px; border-radius:6px; font-weight:700; white-space:nowrap; }
.badge-bull { background:var(--profit3); color:var(--profit); padding:3px 9px; border-radius:6px; font-weight:700; white-space:nowrap; }
.badge-now { background:rgba(245,181,42,.15); color:var(--warn); padding:3px 9px; border-radius:6px; font-weight:700; white-space:nowrap; }
.loss-txt { color:var(--loss); font-weight:600; }
.profit-txt { color:var(--profit); font-weight:600; }
.warn-txt { color:var(--warn); font-weight:600; }
/* ── Cycle Tracker — Disclaimer note ── */
.cycle-note {
border-left:3px solid var(--warn);
background:rgba(245,181,42,.07);
border-radius:0 10px 10px 0;
padding:12px 16px;
font-size:12px; color:var(--t2); line-height:1.6;
}
.cycle-note strong { color:var(--t1); }
/* ── Cycle Tracker — SOL warning banner ── */
.cycle-sol-warning {
background:rgba(255,107,122,.07); border:1px solid rgba(255,107,122,.2);
border-radius:12px; padding:14px 16px; margin-bottom:12px;
font-size:12px; color:var(--loss); line-height:1.5;
display:flex; gap:10px; align-items:flex-start;
}
.cycle-sol-warning strong { color:var(--loss); }
@media(max-width:720px){
.cycle-tabs-wrap { gap:4px; }
.cycle-tab { padding:8px 14px; font-size:12px; }
.cycle-banner { flex-direction:column; gap:8px; }
.cycle-banner > div:last-child { text-align:left; }
.cycle-stats-row { grid-template-columns:1fr 1fr; }
.cycle-table { font-size:11px; }
.cycle-table th, .cycle-table td { padding:8px 8px; }
}
.section-title {
font-size:20px; font-weight:700; letter-spacing:-0.4px; color:var(--t1);
font-family:var(--font-display);
display:flex; align-items:center; gap:10px;
}
.section-title .dot { width:8px; height:8px; border-radius:50%; background:var(--teal); animation:pulse 2s infinite; }
#meta {
font-size:12px; color:var(--t3); font-weight:600;
background:var(--glass2); border:1px solid var(--gb);
border-radius:99px; padding:4px 12px;
}
#error-banner {
display:none; background:var(--red3); border:1px solid rgba(255,107,122,0.2);
border-radius:14px; padding:14px 20px; color:var(--red);
font-size:13px; font-weight:600; margin-bottom:20px;
animation:slideUp 0.3s var(--ease);
}
/* ═══════════════════════════════════════════════
LOADER — PREMIUM SCANNING UI
═══════════════════════════════════════════════ */
#loader {
display:none; flex-direction:column; align-items:center;
padding:56px 24px 72px; gap:0;
animation:slideUp 0.4s var(--ease);
}
/* ── Morphing orb ── */
.scan-orb-wrap {
position:relative; width:160px; height:160px;
margin-bottom:36px;
display:grid; place-items:center;
}
/* Outer glow halo */
.scan-orb-halo {
position:absolute; inset:-24px; border-radius:50%;
background:radial-gradient(circle, rgba(76,201,240,0.14) 0%, transparent 70%);
animation:haloPulse 2.5s ease-in-out infinite;
}
@keyframes haloPulse {
0%,100% { transform:scale(1); opacity:0.7; }
50% { transform:scale(1.18); opacity:1; }
}
/* The morphing blob */
.scan-blob {
position:absolute; inset:0;
background:linear-gradient(135deg, var(--teal) 0%, var(--blue2) 50%, var(--purple) 100%);
background-size:200% 200%;
border-radius:60% 40% 55% 45% / 50% 60% 40% 50%;
animation:blobMorph 4s ease-in-out infinite, gradShift 3s ease infinite;
filter:blur(1px);
opacity:0.9;
}
@keyframes blobMorph {
0% { border-radius:60% 40% 55% 45% / 50% 60% 40% 50%; }
25% { border-radius:50% 50% 40% 60% / 60% 40% 60% 40%; }
50% { border-radius:40% 60% 60% 40% / 45% 55% 45% 55%; }
75% { border-radius:55% 45% 45% 55% / 55% 45% 55% 45%; }
100%{ border-radius:60% 40% 55% 45% / 50% 60% 40% 50%; }
}
/* Inner frosted disc */
.scan-orb-core {
position:relative; z-index:2;
width:80px; height:80px; border-radius:50%;
background:var(--glass2);
border:1px solid var(--gb);
box-shadow:0 8px 32px rgba(0,0,0,0.12), inset 0 1px 0 var(--rim);
display:grid; place-items:center;
}
/* Custom SVG-style scan icon — pure CSS bars */
.scan-icon-bars {
display:flex; align-items:flex-end; gap:3px; height:28px;
}
.sib {
width:4px; border-radius:3px;
background:linear-gradient(180deg, var(--teal), var(--blue2));
}
.sib:nth-child(1) { height:12px; animation:barDance 1.2s ease-in-out 0.0s infinite alternate; }
.sib:nth-child(2) { height:22px; animation:barDance 1.2s ease-in-out 0.15s infinite alternate; }
.sib:nth-child(3) { height:28px; animation:barDance 1.2s ease-in-out 0.3s infinite alternate; }
.sib:nth-child(4) { height:18px; animation:barDance 1.2s ease-in-out 0.45s infinite alternate; }
.sib:nth-child(5) { height:10px; animation:barDance 1.2s ease-in-out 0.6s infinite alternate; }
@keyframes barDance {
from { transform:scaleY(0.35); opacity:0.5; }
to { transform:scaleY(1); opacity:1; }
}
/* Orbiting dot */
.scan-orbit {
position:absolute; inset:0; animation:spinRing 2.2s linear infinite; z-index:3;
}
.scan-orbit-dot {
position:absolute; top:4px; left:50%; transform:translateX(-50%);
width:10px; height:10px; border-radius:50%;
background:white;
box-shadow:0 0 0 2px rgba(76,201,240,0.4), 0 2px 8px rgba(76,201,240,0.6);
}
.scan-orbit-dot::after {
content:''; position:absolute; inset:-3px; border-radius:50%;
background:var(--glass2);
animation:orbitPing 2.2s ease-out infinite;
}
@keyframes orbitPing {
0% { transform:scale(1); opacity:0.8; }
100% { transform:scale(2.5); opacity:0; }
}
@keyframes spinRing { to{transform:rotate(360deg)} }
/* ── Headline ── */
.scan-headline {
font-size:26px; font-weight:700; letter-spacing:-0.8px; color:var(--t1);
font-family:var(--font-display);
text-align:center; margin-bottom:6px;
animation:slideUp 0.5s var(--ease) 0.1s both;
}
.scan-subline {
font-size:13px; color:var(--t3); font-weight:500; text-align:center;
margin-bottom:32px;
animation:slideUp 0.5s var(--ease) 0.2s both;
}
.scan-symbol-stream {
font-family:var(--font-mono); font-size:12px; color:var(--t3);
margin-top:-14px; margin-bottom:24px; min-height:18px; text-align:center;
transition:opacity 0.12s;
animation:slideUp 0.5s var(--ease) 0.45s both;
}
.scan-symbol-stream .sym-cur { color:var(--teal); font-weight:700; }
/* Loader exit — crossfades with the first cards revealing underneath */
#loader.exiting { animation:loaderExit 0.32s var(--ease) forwards; }
@keyframes loaderExit { to { opacity:0; transform:scale(0.97) translateY(-8px); } }
@media (prefers-reduced-motion: reduce) {
.scan-symbol-stream { transition:none; }
#loader.exiting { animation:none; opacity:0; }
}
/* ── Step pipeline ── */
.scan-pipeline {
width:100%; max-width:520px;
display:flex; flex-direction:column; gap:8px;
margin-bottom:28px;
animation:slideUp 0.5s var(--ease) 0.3s both;
}
.scan-step {
display:flex; align-items:center; gap:12px;
padding:12px 16px; border-radius:14px;
background:var(--glass2);
border:1px solid var(--gb);
box-shadow:inset 0 1px 0 var(--rim);
transition:all 0.45s var(--ease);
opacity:0.38;
}
.scan-step.active {
opacity:1; background:var(--glass2);
border-color:rgba(76,201,240,0.22);
box-shadow:0 4px 20px rgba(76,201,240,0.1), inset 0 1px 0 var(--rim);
transform:scale(1.015) translateX(3px);
}
.scan-step.done {
opacity:0.7; background:var(--mints);
border-color:rgba(61,220,151,0.18);
transform:none;
}
/* Step number badge */
.step-num {
width:30px; height:30px; border-radius:9px; flex-shrink:0;
display:grid; place-items:center;
font-size:12px; font-weight:800; color:var(--t3);
background:var(--glass2); border:1px solid var(--gb);
transition:all 0.3s var(--ease);
box-shadow:inset 0 1px 0 var(--rim);
}
.scan-step.active .step-num {
background:var(--grad);
color:transparent; border-color:transparent;
box-shadow:0 4px 12px rgba(76,201,240,0.4);
position:relative;
}
/* Spinner inside active num */
.scan-step.active .step-num::after {
content:''; position:absolute; inset:5px;
border:2px solid var(--gb);
border-top-color:white;
border-radius:50%; animation:spinRing 0.65s linear infinite;
}
.scan-step.done .step-num {
background:var(--mints); color:var(--green);
border-color:rgba(61,220,151,0.2);
font-size:15px;
}
.step-body { flex:1; min-width:0; }
.step-label { font-size:13px; font-weight:700; color:var(--t1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.step-sub { font-size:11px; color:var(--t3); margin-top:1px; font-weight:500; }
.scan-step.active .step-sub { color:var(--teal); }
.step-time { font-size:11px; font-weight:700; color:var(--t4); font-variant-numeric:tabular-nums; flex-shrink:0; }
.scan-step.done .step-time { color:var(--green2); }
/* ── Live counters ── */
.scan-counter {
display:flex; align-items:stretch; gap:0;
border-radius:18px; overflow:hidden;
background:var(--glass2);
border:1px solid var(--gb);
box-shadow:0 4px 20px rgba(0,0,0,0.07), inset 0 1px 0 var(--rim);
animation:slideUp 0.5s var(--ease) 0.4s both;
}
.sc-item { padding:14px 24px; text-align:center; }
.sc-item + .sc-item { border-left:1px solid var(--gb); }
.sc-num { font-size:24px; font-weight:700; letter-spacing:-1px; color:var(--t1); line-height:1; font-variant-numeric:tabular-nums; font-family:var(--font-display); }
.sc-lbl { font-size:10px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; color:var(--t3); margin-top:4px; }
.sc-item.accent .sc-num { color:var(--teal); }
/* ═══════════════════════════════════════════════
EMPTY STATE
═══════════════════════════════════════════════ */
#empty {
display:flex; flex-direction:column; align-items:center;
padding:80px 0; gap:14px;
}
.empty-icon {
width:96px; height:96px;
background:var(--glass); border:1px solid var(--glassbdr);
border-radius:26px; display:grid; place-items:center;
font-size:44px;
box-shadow:0 8px 32px rgba(0,0,0,0.06), inset 0 1px 0 var(--rim);
animation:float 3.5s ease-in-out infinite;
}
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-12px)} }
.empty-h { font-size:22px; font-weight:800; letter-spacing:-0.5px; color:var(--t1); opacity:0.45; }
.empty-p { font-size:14px; color:var(--t3); }
/* ═══════════════════════════════════════════════
CARDS GRID
═══════════════════════════════════════════════ */
#grid {
position:relative;
display:flex;
flex-direction:column;
gap:14px;
}
/* Never-blank auto-refresh — scanline sweep instead of a blank/loading flash */
.grid-scanline {
position:absolute; top:0; left:0; right:0; height:2px;
background:linear-gradient(90deg, transparent, var(--teal), var(--violet), transparent);
opacity:0.9; pointer-events:none; z-index:5; border-radius:2px;
animation: scanSweep 0.9s var(--ease) forwards;
}
@keyframes scanSweep {
0% { transform:translateY(0) scaleX(0.3); opacity:0; }
15% { opacity:1; }
100% { transform:translateY(var(--sweep-h,400px)) scaleX(1); opacity:0; }
}
@media (prefers-reduced-motion: reduce) {
.grid-scanline { display:none; }
}
/* ═══════════════════════════════════════════════
BACKTEST SKELETON + CHOREOGRAPHED REVEAL
═══════════════════════════════════════════════ */
.bt-skeleton { display:flex; flex-direction:column; gap:8px; padding:6px 0; }
.bt-skel-row {
height:14px; border-radius:6px;
background:linear-gradient(90deg, var(--track) 25%, var(--glass2) 50%, var(--track) 75%);
background-size:200% 100%;
animation:skelShimmer 1.4s ease-in-out infinite;
}
.bt-skel-row.w1 { width:60%; }
.bt-skel-row.w2 { width:100%; height:44px; }
.bt-skel-row.w3 { width:80%; }
@keyframes skelShimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
.bt-panel.reveal-in .bt-stat { animation: btStatIn 0.4s var(--ease) both; }
.bt-panel.reveal-in .bt-stat:nth-child(1) { animation-delay:0.02s; }
.bt-panel.reveal-in .bt-stat:nth-child(2) { animation-delay:0.09s; }
.bt-panel.reveal-in .bt-stat:nth-child(3) { animation-delay:0.16s; }
.bt-panel.reveal-in .bt-state-cell { animation: btStatIn 0.4s var(--ease) both; }
.bt-panel.reveal-in .bt-state-cell:nth-child(1) { animation-delay:0.22s; }
.bt-panel.reveal-in .bt-state-cell:nth-child(2) { animation-delay:0.27s; }
.bt-panel.reveal-in .bt-state-cell:nth-child(3) { animation-delay:0.32s; }
.bt-panel.reveal-in .bt-sparkline { animation: btStatIn 0.5s var(--ease) 0.36s both; }
@keyframes btStatIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
@media (prefers-reduced-motion: reduce) {
.bt-skel-row { animation:none; }
.bt-panel.reveal-in .bt-stat, .bt-panel.reveal-in .bt-state-cell, .bt-panel.reveal-in .bt-sparkline { animation:none; }
}
/* ═══════════════════════════════════════════════
TOAST NOTIFICATIONS
═══════════════════════════════════════════════ */
#toast-root {
position:fixed; bottom:calc(50px + env(safe-area-inset-bottom)); left:50%; transform:translateX(-50%);
z-index:900; display:flex; flex-direction:column-reverse; gap:8px; align-items:center;
pointer-events:none;
}
.toast {
display:inline-flex; align-items:center; gap:8px;
background:var(--glass2); border:1px solid var(--gb);
backdrop-filter:blur(14px) saturate(160%); -webkit-backdrop-filter:blur(14px) saturate(160%);
color:var(--t1); font-size:13px; font-weight:600; font-family:var(--font);
padding:10px 18px; border-radius:99px; white-space:nowrap;
box-shadow:0 8px 32px rgba(0,0,0,0.18), inset 0 1px 0 var(--rim);
animation: toastIn 0.35s var(--ease-spring) both, toastOut 0.3s var(--ease) 2.1s forwards;
}
.toast.success { border-color:rgba(61,220,151,0.3); }
.toast.success svg { color:var(--green); flex-shrink:0; }
@keyframes toastIn { from{opacity:0;transform:translateY(16px) scale(0.92)} to{opacity:1;transform:translateY(0) scale(1)} }
@keyframes toastOut { to{opacity:0;transform:translateY(8px) scale(0.95)} }
@media (prefers-reduced-motion: reduce) {
.toast { animation:none; opacity:1; }
}
/* ═══════════════════════════════════════════════
TRADE CARD — LIQUID GLASS PREMIUM
═══════════════════════════════════════════════ */
.card {
position:relative; overflow:hidden;
background:var(--glass);
will-change:transform;
border:1px solid var(--glassbdr);
border-radius:var(--r3);
box-shadow:
inset 0 1px 0 var(--rim),
inset 0 -1px 0 rgba(0,0,0,0.03),
0 8px 32px rgba(0,0,0,0.08),
0 2px 8px rgba(0,0,0,0.04);
padding:22px;
transition:transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.3s;
animation:cardReveal 0.55s var(--ease) both;
cursor:default;
}
@keyframes cardReveal {
from { opacity:0; transform:translateY(24px) scale(0.97); }
to { opacity:1; transform:translateY(0) scale(1); }
}
/* Top specular sheen */
.card::before {
content:''; position:absolute; top:0; left:0; right:0; height:45%;
background:linear-gradient(180deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0) 100%);
border-radius:var(--r3) var(--r3) 0 0; pointer-events:none; z-index:0;
}
/* Hover edge glow */
.card::after {
content:''; position:absolute; top:0; left:8%; right:8%; height:1.5px;
background:linear-gradient(90deg, transparent, var(--teal), transparent);
opacity:0; transition:opacity 0.3s; z-index:0;
}
.card:hover { transform:translateY(-5px) scale(1.008); border-color:var(--rim); box-shadow:inset 0 1px 0 var(--rim), 0 24px 64px rgba(0,0,0,0.13), 0 0 0 1px rgba(76,201,240,0.12); }
.card:hover::after { opacity:1; }
.card > * { position:relative; z-index:1; }
/* Card ambient background tint based on direction */
.card.long-card { background:linear-gradient(160deg, var(--glass) 0%, var(--mints) 130%); }
.card.short-card { background:linear-gradient(160deg, var(--glass) 0%, var(--reds) 130%); }
/* ── Card header ── */
.ch { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:16px; }
.ch-left {}
.ch-sym {
font-size:30px; font-weight:700; letter-spacing:-1px; line-height:1; color:var(--t1);
font-family:var(--font-display);
display:flex; align-items:baseline; gap:5px;
}
/* Quote currency (USDT) in muted style — it's secondary info */
.ch-sym .q { font-size:13px; font-weight:500; color:var(--t4); letter-spacing:0.5px; text-transform:uppercase; font-family:var(--font); }
.ch-badges { display:flex; align-items:center; gap:5px; margin-top:5px; flex-wrap:wrap; }
.badge {
font-size:10px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase;
padding:4px 10px; border-radius:7px;
box-shadow:inset 0 1px 0 var(--rim);
}
.badge-exch { background:rgba(76,201,240,0.1); color:var(--teal); border:1px solid rgba(76,201,240,0.2); }
.badge-cv { background:var(--cyans); color:var(--blue2); border:1px solid rgba(76,201,240,0.2); }
.badge-chg.pos { background:var(--mints); color:var(--green); border:1px solid rgba(61,220,151,0.2); }
.badge-chg.neg { background:var(--red3); color:var(--red); border:1px solid rgba(255,107,122,0.2); }
.badge-fr.fr-hi { background:var(--reds); color:var(--red); border:1px solid rgba(255,107,122,0.15); }
.badge-fr.fr-lo { background:var(--mints); color:var(--green); border:1px solid rgba(61,220,151,0.15); }
.badge-fr.fr-n { background:var(--glass2); color:var(--t3); border:1px solid var(--gb); }
.badge-oi { background:var(--violets); color:var(--purple); border:1px solid rgba(155,123,255,0.15); }
.badge-vol { background:var(--glass2); color:var(--t3); border:1px solid var(--gb); }
.dir-tag {
font-size:10.5px; font-weight:800; letter-spacing:.12em; text-transform:uppercase;
padding:5px 12px; border-radius:8px;
flex-shrink:0;
position:relative; overflow:hidden;
transition:transform 0.2s var(--spring);
}
.dir-tag:hover { transform:scale(1.05); }
.dir-tag.long { background:var(--mints); color:var(--green); border:1px solid rgba(61,220,151,0.25); box-shadow:0 2px 8px rgba(61,220,151,0.2), inset 0 1px 0 var(--rim); }
.dir-tag.short { background:var(--red3); color:var(--red); border:1px solid rgba(255,107,122,0.2); box-shadow:0 2px 8px rgba(255,107,122,0.15), inset 0 1px 0 var(--rim); }
/* ── Confidence ── */
.conf-box {
background:var(--glass2); border:1px solid var(--gb);
border-radius:16px; padding:14px 16px; margin-bottom:14px;
box-shadow:inset 0 1px 0 var(--rim);
transition:transform 0.2s var(--spring);
}
.conf-box:hover { transform:scale(1.01); }
.conf-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
.conf-lbl { font-size:11px; font-weight:700; letter-spacing:1px; text-transform:uppercase; color:var(--t3); }
/* Display font + tabular-nums = Bloomberg-terminal look */
.conf-num { font-size:38px; font-weight:700; letter-spacing:-2px; line-height:1; font-family:var(--font-display); font-variant-numeric:tabular-nums; }
.conf-denom { font-size:16px; font-weight:400; color:var(--t4); letter-spacing:0; }
/* Confidence bar thicker for visibility */
.conf-bar { height:6px; background:var(--track); border-radius:4px; overflow:hidden; margin-top:2px; }
.conf-fill { height:100%; border-radius:4px; transition:width 1.4s var(--ease); }
/* GREEN = safe bet, AMBER = caution, RED = risky — matches trading convention */
.conf-fill.hi { background:linear-gradient(90deg, var(--profit) 0%, var(--profit2) 100%); }
.conf-fill.md { background:linear-gradient(90deg, var(--warn) 0%, var(--warn2) 100%); }
.conf-fill.lo { background:linear-gradient(90deg, var(--loss) 0%, var(--loss2) 100%); }
/* ── Price flash + delta float (live ticker updates) ── */
.lvl.entry { position:relative; overflow:visible; }
.price-flash-up { animation: flashUp 0.9s var(--ease); border-radius:12px; }
.price-flash-down { animation: flashDown 0.9s var(--ease); border-radius:12px; }
@keyframes flashUp { 0%{background:rgba(61,220,151,0.25)} 100%{background:transparent} }
@keyframes flashDown { 0%{background:rgba(255,107,122,0.25)} 100%{background:transparent} }
.price-delta-float {
position:absolute; top:-6px; right:6px;
font-size:10px; font-weight:800; font-family:var(--font-mono);
pointer-events:none; z-index:4;
animation: deltaFloat 1.15s var(--ease) forwards;
}
.price-delta-float.up { color:var(--green); }
.price-delta-float.down { color:var(--red); }
@keyframes deltaFloat {
0% { transform:translateY(2px); opacity:0; }
18% { opacity:1; }
100% { transform:translateY(-20px); opacity:0; }
}
@media (prefers-reduced-motion: reduce) {
.price-flash-up, .price-flash-down, .price-delta-float { animation:none !important; }
}
/* ── Timeframe chips ── */
.tf-row { display:flex; gap:5px; margin-bottom:14px; flex-wrap:wrap; }
.tf { font-size:10px; font-weight:700; letter-spacing:0.3px; padding:4px 10px; border-radius:99px; text-transform:uppercase; transition:transform 0.15s var(--spring); cursor:default; }
.tf:hover { transform:scale(1.05); }
.tf.up { background:var(--mints); color:var(--green); border:1px solid rgba(61,220,151,0.2); }
.tf.down { background:var(--red3); color:var(--red); border:1px solid rgba(255,107,122,0.2); }
.tf.range { background:var(--amber3); color:var(--amber); border:1px solid rgba(245,181,42,0.2); }
.tf.na { background:var(--glass2); color:var(--t3); border:1px solid var(--gb); }
.rule { height:1px; background:var(--track); margin:14px -22px; }
/* ── Levels grid ── */
/* Color-coded: each box tells a story at a glance (trading convention) */
.lvl-grid { display:grid; grid-template-columns:repeat(6,1fr); gap:9px; margin-bottom:10px; }
.lvl-pnl { font-size:10px; font-weight:700; font-variant-numeric:tabular-nums; margin-top:2px; }
.lvl-pnl.pos { color:var(--profit); }
.lvl-pnl.neg { color:var(--loss); }
.lvl-pnl.neutral { color:var(--t3); font-weight:600; }
.lvl {
border-radius:12px; padding:10px 12px;
position:relative; overflow:hidden;
transition:transform 0.2s var(--spring), box-shadow 0.2s;
border:1px solid transparent;
min-width:0;
}
.lvl:hover { transform:translateY(-2px); }
/* Entry = teal tint — neutral action point */
.lvl.entry {
background:var(--cyans);
border-color:rgba(76,201,240,0.15);
box-shadow:inset 0 1px 0 var(--rim);
}
/* SL = red tint — danger, hard stop */
.lvl.sl {
background:var(--reds);
border-color:rgba(255,107,122,0.15);
box-shadow:inset 0 1px 0 var(--rim);
}
/* TP = green tint — profit target */
.lvl.tp {
background:var(--mints);
border-color:rgba(61,220,151,0.15);
box-shadow:inset 0 1px 0 var(--rim);
}
/* R/R — color shifts by quality: green ≥1.5, amber 1.0–1.5, red <1.0 */
.lvl.rr {
background:var(--ambers);
border-color:rgba(245,181,42,0.15);
box-shadow:inset 0 1px 0 var(--rim);
}
.lvl.rr.rr-good { background:var(--mints); border-color:rgba(61,220,151,0.15); }
.lvl.rr.rr-bad { background:var(--reds); border-color:rgba(255,107,122,0.15); }
/* Thick left border as a strong color signal — key for quick scanning */
.lvl::before { content:''; position:absolute; top:0; left:0; bottom:0; width:3px; }
.lvl.entry::before { background:var(--teal); }
.lvl.sl::before { background:var(--loss2); }
.lvl.tp::before { background:var(--profit2); }
.lvl.rr::before { background:var(--warn2); }
.lvl-lbl { font-size:9px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; margin-bottom:5px; }
.lvl.entry .lvl-lbl { color:var(--teal); }
.lvl.sl .lvl-lbl { color:var(--loss); }
.lvl.tp .lvl-lbl { color:var(--profit); }
.lvl.rr .lvl-lbl { color:var(--warn); }
.lvl.rr.rr-good .lvl-lbl { color:var(--profit); }
.lvl.rr.rr-bad .lvl-lbl { color:var(--loss); }
/* Space Grotesk + tabular-nums = precise, Bloomberg-style data */
.lvl-val { font-size:14.5px; font-weight:600; letter-spacing:-0.2px; font-variant-numeric:tabular-nums; font-family:var(--font-display); overflow-wrap:anywhere; }
.lvl.entry .lvl-val { color:var(--t1); }
.lvl.sl .lvl-val { color:var(--loss); }
.lvl.tp .lvl-val { color:var(--profit); }
.lvl.rr .lvl-val { color:var(--warn); }
.lvl.rr.rr-good .lvl-val { color:var(--profit); }
.lvl.rr.rr-bad .lvl-val { color:var(--loss); }
/* ── Sizing strip ── */
.sz-strip {
display:flex; border-radius:12px; overflow:hidden;
border:1px solid var(--gb); margin-bottom:12px;
background:var(--glass2);
box-shadow:inset 0 1px 0 var(--rim);
}
.sz-item { flex:1; padding:10px 12px; text-align:center; }
.sz-item + .sz-item { border-left:1px solid var(--gb); }
.sz-lbl { font-size:11px; font-weight:700; letter-spacing:0.8px; text-transform:uppercase; color:var(--t3); margin-bottom:3px; }
/* Tabular-nums + mono font for all financial figures (font-family also enforced by the shared numerals rule below) */
.sz-val { font-size:16px; font-weight:600; font-variant-numeric:tabular-nums; font-family:var(--font-mono); color:var(--t1); }
/* ── Narrative ── */
.narr {
font-size:12px; color:var(--t2); line-height:1.65;
padding:11px 14px; margin-bottom:12px;
background:var(--cyans);
border-left:2px solid rgba(76,201,240,0.4);
border-radius:0 10px 10px 0;
position:relative;
}
/* ── Score breakdown ── */
.ev-title { font-size:11px; font-weight:800; letter-spacing:0.8px; text-transform:uppercase; color:var(--t3); margin-bottom:10px; }
.ev-row { display:flex; align-items:center; gap:11px; margin-bottom:7px; }
.ev-key { font-size:11px; font-weight:600; color:var(--t2); width:82px; flex-shrink:0; }
.ev-track { flex:1; height:6px; background:var(--track); border-radius:99px; overflow:hidden; }
.ev-bar { height:100%; border-radius:99px; transition:width 1s var(--ease) 0.2s; width:0; }
.ev-bar.hi { background:linear-gradient(90deg, var(--profit), var(--profit2)); }
.ev-bar.med { background:linear-gradient(90deg, var(--warn), var(--warn2)); }
.ev-bar.lo { background:linear-gradient(90deg, var(--loss), var(--loss2)); }
.ev-score { font-size:11px; font-weight:800; width:20px; text-align:right; color:var(--t1); }
/* ── Probability / EV strip ── */
.prob-strip {
display:flex; align-items:stretch; gap:6px;
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:10px 12px; margin-bottom:10px;
}
.prob-block { flex:1; text-align:center; }
.prob-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); margin-bottom:3px; }
.prob-val { font-size:20px; font-weight:700; font-family:var(--font-display); font-variant-numeric:tabular-nums; line-height:1; }
.prob-val.pwin { color:var(--profit); }
.prob-val.ploss { color:var(--loss); }
.prob-val.ev-pos { color:var(--profit); }
.prob-val.ev-neg { color:var(--loss); }
.prob-val.ev-zero{ color:var(--t3); }
.prob-divider { width:1px; background:var(--track); flex-shrink:0; }
/* ── Leverage strip ── */
.lev-strip {
display:flex; align-items:center; gap:8px;
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:9px 12px; margin-bottom:10px;
}
.lev-icon { font-size:16px; flex-shrink:0; }
.lev-main { flex:1; }
.lev-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); margin-bottom:1px; }
.lev-val { font-size:17px; font-weight:700; font-family:var(--font-display); font-variant-numeric:tabular-nums; color:var(--t1); line-height:1.1; }
.lev-max { font-size:10px; color:var(--t3); font-weight:500; }
.lev-warn { font-size:10px; color:var(--warn); font-weight:600; margin-top:2px; }
.lev-cap { font-size:10px; color:var(--loss); font-weight:500; margin-top:2px; }
.lev-profit { font-size:11px; color:var(--profit); font-weight:600; margin-top:2px; font-variant-numeric:tabular-nums; }
/* ── Duration strip ── */
.dur-strip {
display:flex; align-items:center; gap:8px;
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:9px 12px; margin-bottom:10px;
}
.dur-icon { font-size:16px; flex-shrink:0; }
.dur-main { flex:1; }
.dur-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); margin-bottom:1px; }
.dur-val { font-size:15px; font-weight:700; font-family:var(--font-display); color:var(--t1); line-height:1.2; }
/* ── Veto banner ── */
.veto-banner {
background: var(--reds); border: 1.5px solid rgba(255,107,122,0.4);
border-radius: 10px; padding: 10px 14px; margin-bottom: 10px;
display: flex; flex-direction: column; gap: 4px;
}
.veto-banner-title {
font-size: 13px; font-weight: 700; color: var(--red);
display: flex; align-items: center; gap: 6px;
}
.veto-banner-reason {
font-size: 11.5px; color: var(--red); line-height: 1.45;
}
/* ── Price Action section ── */
.pa-section {
margin-bottom:10px;
}
.pa-header {
display:flex; align-items:center; gap:7px; margin-bottom:8px;
}
.pa-title {
font-size:11px; font-weight:800; letter-spacing:0.8px; text-transform:uppercase; color:var(--t3); flex:1;
}
.pa-grade {
font-size:11px; font-weight:800; padding:2px 8px; border-radius:6px; letter-spacing:0.4px; flex-shrink:0;
}
.pa-grade-ap { background:rgba(61,220,151,0.18); color:var(--mint); border:1px solid rgba(61,220,151,0.25); }
.pa-grade-a { background:rgba(61,220,151,0.12); color:var(--mint); border:1px solid rgba(61,220,151,0.2); }
.pa-grade-b { background:rgba(245,181,42,0.13); color:var(--warn); border:1px solid rgba(245,181,42,0.2); }
.pa-grade-c { background:var(--ambers); color:var(--amber); border:1px solid rgba(245,181,42,0.2); }
.pa-grade-d { background:rgba(255,107,122,0.13); color:var(--loss); border:1px solid rgba(255,107,122,0.2); }
.pa-score-num {
font-size:11px; font-weight:700; color:var(--t3); flex-shrink:0;
}
.pa-row {
display:flex; align-items:flex-start; gap:7px;
padding:7px 10px; border-radius:10px;
background:var(--glass2); border:1px solid var(--gb);
margin-bottom:6px; cursor:pointer; position:relative;
}
.pa-row-icon { font-size:13px; flex-shrink:0; margin-top:1px; }
.pa-row-body { flex:1; min-width:0; }
.pa-row-top { display:flex; align-items:center; gap:5px; flex-wrap:wrap; }
.pa-pat-name { font-size:12px; font-weight:700; color:var(--t1); }
.pa-signal-bull { color:var(--profit2); font-size:11px; }
.pa-signal-bear { color:var(--loss2); font-size:11px; }
.pa-dots { font-size:10px; color:var(--t3); letter-spacing:1px; flex-shrink:0; }
.pa-key-badge {
font-size:10px; font-weight:700; padding:1px 5px; border-radius:4px;
background:rgba(245,181,42,0.15); color:var(--warn); border:1px solid rgba(245,181,42,0.2);
}
.pa-stage-confirmed { font-size:10px; font-weight:700; padding:1px 5px; border-radius:4px; background:rgba(61,220,151,0.13); color:var(--profit); border:1px solid rgba(61,220,151,0.2); }
.pa-stage-forming { font-size:10px; font-weight:700; padding:1px 5px; border-radius:4px; background:rgba(245,181,42,0.12); color:var(--warn); border:1px solid rgba(245,181,42,0.18); }
.pa-stage-broken { font-size:10px; font-weight:700; padding:1px 5px; border-radius:4px; background:rgba(155,123,255,0.13); color:var(--violet); border:1px solid rgba(155,123,255,0.22); }
.pa-target { font-size:11px; font-weight:600; color:var(--profit); }
.pa-more { font-size:11px; color:var(--t3); font-weight:600; white-space:nowrap; }
.pa-desc {
display:grid; grid-template-rows:0fr;
transition:grid-template-rows 0.35s var(--ease), margin-top 0.35s var(--ease);
margin-top:0;
}
.pa-desc-inner {
overflow:hidden; min-height:0;
font-size:11px; color:var(--t2); line-height:1.5;
opacity:0; transition:opacity 0.2s var(--ease);
}
.pa-row.expanded .pa-desc { grid-template-rows:1fr; margin-top:4px; }
.pa-row.expanded .pa-desc-inner { opacity:1; transition-delay:0.08s; }
.pa-row-icon { transition:transform 0.3s var(--ease-spring); display:inline-block; }
.pa-row.expanded .pa-row-icon { transform:scale(1.15) rotate(6deg); }
@media (prefers-reduced-motion: reduce) {
.pa-desc, .pa-desc-inner, .pa-row-icon { transition:none; }
}
.pa-ob-line {
font-size:11px; color:var(--t3); padding:4px 2px 0; line-height:1.5;
}
.pa-conflict {
display:flex; align-items:center; gap:5px;
font-size:11px; font-weight:600; color:var(--warn);
background:rgba(245,181,42,0.08); border:1px solid rgba(245,181,42,0.18);
border-radius:8px; padding:5px 10px; margin-top:4px;
}
.pa-conf-grade {
font-size:11px; font-weight:800; padding:1px 7px; border-radius:6px;
letter-spacing:0.3px; margin-left:5px; vertical-align:middle;
}
/* ── Backtest panel ── */
.bt-panel {
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:9px 12px; margin-bottom:10px;
}
/* ── Live Accuracy panel ── */
.live-acc-panel {
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:9px 12px; margin-bottom:10px;
}
.live-acc-header {
display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap;
}
.live-acc-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); flex:1; min-width:0; }
.live-acc-badge {
font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px;
white-space:nowrap; flex-shrink:0;
}
.live-acc-badge.win { background:var(--mints); color:var(--profit); border:1px solid rgba(61,220,151,0.2); }
.live-acc-badge.loss { background:var(--reds); color:var(--loss); border:1px solid rgba(255,107,122,0.2); }
.live-acc-badge.pending { background:var(--ambers); color:var(--warn); border:1px solid rgba(245,181,42,0.2); }
.live-acc-badge.none { background:var(--track); color:var(--t3); border:1px solid var(--gb); }
.live-acc-stats { display:flex; gap:6px; margin-bottom:8px; }
.live-acc-stat {
flex:1; background:var(--track); border-radius:8px; padding:5px 6px; text-align:center;
}
.live-acc-stat-val { font-size:16px; font-weight:800; font-family:var(--font-display); letter-spacing:-0.5px; color:var(--t1); }
.live-acc-stat-val.win { color:var(--profit); }
.live-acc-stat-val.loss { color:var(--loss); }
.live-acc-stat-lbl { font-size:11px; font-weight:600; color:var(--t3); letter-spacing:0.3px; margin-top:1px; }
.live-acc-note { font-size:10px; color:var(--t3); line-height:1.4; margin-top:4px; }
.live-acc-log { margin-top:6px; }
.live-acc-log-row {
display:flex; align-items:center; gap:6px; padding:3px 0;
border-bottom:1px solid var(--gb); font-size:10px;
}
.live-acc-log-row:last-child { border-bottom:none; }
.live-acc-sym { font-weight:700; font-size:10px; color:var(--t1); flex:1; }
.live-acc-dir { font-size:11px; font-weight:700; padding:1px 5px; border-radius:4px; }
.live-acc-dir.long { background:var(--mints); color:var(--profit); }
.live-acc-dir.short { background:var(--reds); color:var(--loss); }
.live-acc-outcome { font-size:11px; font-weight:700; min-width:40px; text-align:center; }
.live-acc-outcome.win { color:var(--profit); }
.live-acc-outcome.loss { color:var(--loss); }
.live-acc-outcome.pending { color:var(--warn); }
.live-acc-outcome.expired { color:var(--t3); }
.live-acc-r { font-size:11px; font-weight:700; color:var(--t2); min-width:28px; text-align:right; }
.live-acc-resolve-btn {
font-size:11px; font-weight:700; padding:3px 8px; border-radius:6px; border:none; cursor:pointer;
background:var(--teal3); color:var(--teal); border:1px solid rgba(76,201,240,0.2);
transition:all 0.2s;
}
.live-acc-resolve-btn:hover { background:var(--teal); color:#fff; }
/* Global accuracy bar at top of page */
.global-acc-bar {
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:10px 14px; margin-bottom:14px;
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
}
.global-acc-title { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); }
.global-acc-wr { font-size:18px; font-weight:800; font-family:var(--font-display); letter-spacing:-0.5px; }
.global-acc-wr.win { color:var(--profit); }
.global-acc-wr.loss { color:var(--loss); }
.global-acc-wr.neutral { color:var(--t2); }
.global-acc-wr.none { color:var(--t3); }
.global-acc-sub { font-size:10px; color:var(--t3); }
.global-acc-spacer { flex:1; }
/* Resolve button at top */
.bt-header {
display:flex; align-items:center; gap:8px; margin-bottom:8px;
}
.bt-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); flex:1; }
.bt-run-btn {
font-size:10px; font-weight:700; padding:3px 10px; border-radius:7px; border:none; cursor:pointer;
background:var(--teal3); color:var(--teal); border:1px solid rgba(76,201,240,0.2);
transition:all 0.2s;
}
.bt-run-btn:hover { background:var(--teal); color:#fff; }
.bt-run-btn:disabled { opacity:0.5; cursor:wait; }
.bt-grounded-badge {
font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px;
background:rgba(76,201,240,0.08); color:var(--blue); border:1px solid rgba(76,201,240,0.15);
}
.bt-stats-row {
display:flex; gap:6px; margin-bottom:8px;
}
.bt-stat {
flex:1; background:var(--track); border-radius:8px; padding:5px 7px; text-align:center;
}
.bt-stat-val { font-size:14px; font-weight:800; font-family:var(--font-display); color:var(--t1); font-variant-numeric:tabular-nums; }
.bt-stat-val.win { color:var(--profit); }
.bt-stat-val.loss { color:var(--loss); }
.bt-stat-val.neutral { color:var(--t2); }
.bt-stat-lbl { font-size:11px; font-weight:600; color:var(--t3); margin-top:1px; }
.bt-state-row { display:flex; gap:5px; margin-bottom:8px; flex-wrap:wrap; }
.bt-state-cell {
flex:1; min-width:70px; background:var(--track); border-radius:7px; padding:4px 6px;
}
.bt-state-name { font-size:11px; font-weight:800; letter-spacing:0.3px; color:var(--t3); }
.bt-state-wr { font-size:13px; font-weight:800; font-family:var(--font-display); font-variant-numeric:tabular-nums; }
.bt-state-wr.bull { color:var(--profit); }
.bt-state-wr.bear { color:var(--loss); }
.bt-state-wr.range { color:var(--warn); }
.bt-state-n { font-size:11px; color:var(--t4); font-weight:600; }
/* Equity curve sparkline */
.bt-sparkline { width:100%; height:40px; display:block; }
/* Loading state */
.bt-loading { font-size:11px; color:var(--t3); font-weight:600; padding:4px 0; text-align:center; }
/* p_win "grounded" indicator */
.prob-val.grounded { border-bottom:2px solid var(--blue2); }
/* ── Market State strip (Markov + HMM) ── */
.state-strip {
background:var(--glass2); border:1px solid var(--gb);
border-radius:12px; padding:9px 12px; margin-bottom:10px;
}
.state-strip-top {
display:flex; align-items:center; gap:8px; margin-bottom:6px;
}
.state-lbl { font-size:11px; font-weight:800; letter-spacing:0.7px; text-transform:uppercase; color:var(--t3); flex:1; }
.state-trust {
font-size:11px; font-weight:700; color:var(--teal); background:var(--teal3);
border:1px solid rgba(76,201,240,0.18); border-radius:5px; padding:2px 6px;
letter-spacing:0.3px;
}
.state-badges { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.state-badge {
display:inline-flex; align-items:center; gap:4px;
font-size:12px; font-weight:800; padding:7px 13px; border-radius:10px;
letter-spacing:-0.2px; font-family:var(--font-display);
}
.state-badge.BULL { color:var(--profit); background:var(--mints); border:1px solid rgba(61,220,151,0.2); }
.state-badge.BEAR { color:var(--loss); background:var(--reds); border:1px solid rgba(255,107,122,0.2); }
.state-badge.RANGING { color:var(--warn); background:var(--ambers); border:1px solid rgba(245,181,42,0.2); }
.state-conf { font-size:10px; font-weight:600; color:var(--t3); }
.state-conflict { font-size:10px; font-weight:600; color:var(--loss); margin-top:3px; }
/* ── Forecast row inside state strip ── */
.forecast-row {
display:flex; gap:8px; margin-top:8px;
}
.fc-cell {
flex:1; background:var(--track); border-radius:8px; padding:5px 6px; text-align:center;
}
.fc-horizon { font-size:11px; font-weight:800; letter-spacing:0.5px; text-transform:uppercase; color:var(--t3); margin-bottom:3px; }
.fc-bars { display:flex; flex-direction:column; gap:2px; }
.fc-bar-row { display:flex; align-items:center; gap:3px; }
.fc-bar-lbl { font-size:11px; font-weight:700; color:var(--t3); width:16px; text-align:right; }
.fc-bar-track { flex:1; height:5px; background:var(--track); border-radius:3px; overflow:hidden; }
.fc-bar-fill { height:100%; border-radius:3px; transition:width 0.8s var(--ease); }
.fc-bar-fill.BULL { background:var(--profit2); }
.fc-bar-fill.BEAR { background:var(--loss2); }
.fc-bar-fill.RANGING { background:var(--warn2); }
.fc-bar-pct { font-size:11px; font-weight:700; color:var(--t2); width:22px; font-variant-numeric:tabular-nums; }
/* ── State filter toggle ── */
.state-filter-bar {
display:flex; align-items:center; gap:8px; margin-bottom:12px;
padding:8px 12px; background:var(--glass2); border:1px solid var(--gb);
border-radius:12px;
}
.state-filter-lbl { font-size:10px; font-weight:800; letter-spacing:0.6px; text-transform:uppercase; color:var(--t3); }
.state-filter-toggle {
display:inline-flex; align-items:center; gap:5px;
background:var(--glass2); border:1px solid var(--gb);
border-radius:8px; padding:4px 10px; min-height:36px; box-sizing:border-box;
font-size:11px; font-weight:700; color:var(--t2); cursor:pointer;
transition:all 0.2s;
}
.state-filter-toggle.active {
background:var(--teal3); border-color:rgba(76,201,240,0.3); color:var(--teal);
}
/* Signal type badge for state_transition */
.sig-type-badge.state_transition { background:rgba(155,123,255,0.12); color:var(--purple2); }
/* ── Actions ── */
.card-acts { display:flex; gap:7px; margin-top:16px; }
/* ═══ Rank-aware card shells — featured hero card + collapsible ranked rows ═══ */
.card.featured-card {
box-shadow:inset 0 1px 0 var(--rim), inset 0 -1px 0 rgba(0,0,0,0.03), 0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04), 0 0 0 1px rgba(76,201,240,0.15);
border-color:rgba(76,201,240,0.3);
}
.featured-ring-row { display:flex; align-items:center; gap:18px; margin-bottom:16px; }
.conf-ring-wrap { position:relative; width:84px; height:84px; flex-shrink:0; }
.conf-ring-wrap svg { transform:rotate(-90deg); width:100%; height:100%; }
.conf-ring-bg { fill:none; stroke:var(--gb); }
.conf-ring-val { fill:none; stroke-linecap:round; transition:stroke-dashoffset 1.4s cubic-bezier(0.22,1,0.36,1); }
.conf-ring-center { position:absolute; inset:0; display:grid; place-items:center; text-align:center; }
.conf-ring-center b { display:block; font-family:var(--font-display); font-size:20px; font-weight:800; line-height:1; }
.conf-ring-center span { font-size:9px; font-weight:700; letter-spacing:0.1em; color:var(--t3); }
.featured-tag { display:inline-flex; align-items:center; gap:6px; font-family:var(--font-display); font-size:11px; font-weight:700; letter-spacing:0.06em; color:var(--teal); text-transform:uppercase; }
/* ── Featured two-column layout: ring/grade/state/score rail + main content ── */
.featured-layout { display:grid; grid-template-columns:300px 1fr; gap:28px; align-items:start; }
.featured-left {
display:flex; flex-direction:column; align-items:center; gap:14px;
padding:20px 16px; border-radius:16px; text-align:center;
background:linear-gradient(160deg, var(--mints, rgba(61,220,151,0.08)), transparent 70%);
border:1px solid var(--gb);
}
.featured-left .conf-ring-wrap { width:158px; height:158px; }
.featured-left .conf-ring-center b { font-size:34px; }
.featured-left .state-strip, .featured-left .panel, .featured-left .featured-state-pills { width:100%; text-align:left; }
.featured-left .featured-state-pills { display:flex; flex-wrap:wrap; gap:7px; justify-content:center; }
.featured-right { display:flex; flex-direction:column; gap:16px; min-width:0; }
.two-col { display:grid; grid-template-columns:1.2fr .8fr; gap:14px; }
@media (max-width:1080px) { .two-col { grid-template-columns:1fr; } }
.tag-grade { display:inline-flex; align-items:center; font-family:var(--font-display); font-weight:700; font-size:12px; letter-spacing:0.06em; padding:4px 12px; border-radius:8px; }
.tag-grade.gA { color:var(--profit, var(--green)); background:var(--mints, rgba(61,220,151,0.11)); border:1px solid rgba(61,220,151,0.3); }
.tag-grade.gB { color:var(--warn, var(--amber)); background:var(--ambers, rgba(245,181,42,0.12)); border:1px solid rgba(245,181,42,0.3); }
.tag-grade.gC { color:var(--loss, var(--red)); background:var(--reds, rgba(255,107,122,0.10)); border:1px solid rgba(255,107,122,0.3); }
/* ── Real Price Action confluence grade chip — sits beside/below .tag-grade
in the featured rail. Independent signal from confidence: derived from
chart patterns + candlesticks + order blocks (confluence.py), not the
AI confidence score. ── */
.pa-grade-chip {
display:inline-flex; align-items:center;
font-family:var(--font-display); font-weight:700; font-size:11px;
letter-spacing:0.06em; padding:5px 12px; border-radius:9px;
}
.pa-grade-chip.pgA { color:var(--mint); background:var(--mints); border:1px solid rgba(61,220,151,0.3); }
.pa-grade-chip.pgB { color:var(--cyan); background:var(--cyans); border:1px solid rgba(76,201,240,0.3); }
.pa-grade-chip.pgC { color:var(--amber); background:var(--ambers); border:1px solid rgba(245,181,42,0.3); }
.pa-grade-chip.pgD { color:var(--red); background:var(--reds); border:1px solid rgba(255,107,122,0.3); }
.pa-grade-chip.pgNA { color:var(--t3); background:var(--glass2); border:1px solid var(--gb); }
@media (max-width:900px) {
.featured-layout { grid-template-columns:1fr; }
.featured-left { flex-direction:row; flex-wrap:wrap; justify-content:center; text-align:left; }
}
.row-summary { display:none; }
.card.row-mode .row-summary {
display:grid; cursor:pointer; min-height:40px;
grid-template-columns:40px 1.5fr 1.1fr .9fr .9fr .8fr .8fr .8fr 70px;
align-items:center; gap:10px;
}
.card.row-mode .card-detail {
display:grid; grid-template-rows:0fr;
transition:grid-template-rows 0.5s var(--ease);
}
.card.row-mode.open .card-detail { grid-template-rows:1fr; }
.card.row-mode .card-detail-inner { overflow:hidden; min-height:0; }
.card.row-mode .card-detail-inner > .ch { margin-top:16px; }
.rs-rank { font-family:var(--font-display); font-weight:800; font-size:14px; color:var(--t3); }
.rs-sym-wrap { display:flex; align-items:center; gap:8px; min-width:0; }
.rs-sym { font-family:var(--font-display); font-weight:700; font-size:14.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.rs-dir { font-size:9px; font-weight:700; letter-spacing:0.08em; padding:2px 7px; border-radius:6px; flex-shrink:0; }
.rs-dir.long { color:var(--green); background:rgba(61,220,151,0.11); }
.rs-dir.short { color:var(--red); background:rgba(255,107,122,0.10); }
.rs-conf { display:flex; align-items:center; gap:7px; }
.rs-conf b { font-family:var(--font-display); font-size:15px; }
.rs-mbar { width:44px; height:4px; border-radius:99px; background:var(--glass2); overflow:hidden; flex-shrink:0; }
.rs-mbar i { display:block; height:100%; border-radius:99px; background:linear-gradient(90deg, var(--profit), var(--profit2)); width:0; transition:width 1s var(--ease) 0.15s; }
.rs-mbar.lo i { background:linear-gradient(90deg, var(--warn), var(--loss2)); }
.rs-stat { text-align:right; }
.rs-stat .k { font-size:9px; font-weight:600; letter-spacing:0.1em; color:var(--t3); }
.rs-stat .v { margin-top:2px; font-size:12.5px; font-weight:700; font-family:var(--font-display); }
.rs-exp { justify-self:end; color:var(--t3); transition:transform 0.35s var(--spring); }
.card.row-mode.open .rs-exp { transform:rotate(180deg); }
@media (max-width:900px) {
.card.row-mode .row-summary { grid-template-columns:22px 1fr 44px 18px; }
.rs-stat { display:none; }
}
.ca {
flex:1; display:inline-flex; align-items:center; justify-content:center; gap:5px;
padding:10px 12px; border-radius:11px;
font-size:12px; font-weight:700; font-family:var(--font);
border:none; cursor:pointer; white-space:nowrap;
transition:transform 0.15s var(--spring), box-shadow 0.15s, background 0.15s;
text-decoration:none;
box-shadow:inset 0 1px 0 var(--rim);
position:relative; overflow:hidden;
}
.ca::after { content:''; position:absolute; inset:0; border-radius:11px; background:rgba(255,255,255,0); transition:background 0.15s; }
.ca:hover::after { background:var(--glass2); }
.ca:hover { transform:translateY(-1px) scale(1.02); }
.ca:active { transform:scale(0.96); }
.ca-chart { background:var(--cyans); color:var(--teal); border:1px solid rgba(76,201,240,0.2); }
.ca-copy { background:var(--glass2); color:var(--t1); border:1px solid var(--gb); }
.ca-ref { flex:0 0 auto; padding:10px 11px; background:var(--glass2); color:var(--t3); border:1px solid var(--gb); }
.ca-ref svg { flex-shrink:0; }
.ca-ref:hover { color:var(--teal); }
/* ── Error card ── */
.err-card {
background:var(--red3); border:1px solid rgba(255,107,122,0.2);
border-radius:var(--r3); padding:18px;
color:var(--red); font-size:13px; font-weight:600;
animation:cardReveal 0.4s var(--ease);
}
/* ── Exchange badge / new badges ── */
.exch-sep { opacity:0.45; font-weight:400; margin:0 2px; }
/* ═══════════════════════════════════════════════
SCROLL TO TOP
═══════════════════════════════════════════════ */
#scroll-top {
position:fixed; bottom:calc(76px + env(safe-area-inset-bottom)); right:28px; z-index:200;
width:44px; height:44px; border-radius:13px; border:none;
background:var(--glass2); color:var(--t2);
backdrop-filter:blur(16px); cursor:pointer;
display:grid; place-items:center;
box-shadow:0 4px 20px rgba(0,0,0,0.1), inset 0 1px 0 var(--rim);
opacity:0; pointer-events:none;
transition:opacity 0.3s, transform 0.2s var(--spring);
border:1px solid var(--gb);
}
#scroll-top.show { opacity:1; pointer-events:auto; }
#scroll-top:hover { transform:translateY(-2px); }
/* ═══════════════════════════════════════════════
UTILITIES
═══════════════════════════════════════════════ */
.spin-icon { animation:spin 0.7s linear infinite; flex-shrink:0; }
/* ── Touch targets — minimum 44px per WCAG 2.5.5 ── */
.chip { min-height:40px; }
.chip input, .chip select { min-height:36px; }
.bt-run-btn { min-height:36px; padding:6px 12px; }
.live-acc-resolve-btn { min-height:36px; padding:6px 10px; }
.signals-refresh-btn { width:36px !important; height:36px !important; }
.scan-btn-wrap { min-height:44px; display:flex; align-items:center; }
#scan-btn { min-height:40px; padding:10px 20px; }
/* ── Disable hover animations on touch devices ── */
@media (hover:none) {
.card:hover { transform:none; box-shadow:0 2px 20px rgba(0,0,0,0.06); }
.stat-glass:hover { transform:none; }
.conf-box:hover { transform:none; }
.lvl:hover { transform:none; background:transparent; }
.sig-card:hover { transform:none; }
}
/* Active states for touch feedback */
.btn-hero:active, .btn-ghost2:active, .sig-cta:active { transform:scale(0.96) !important; opacity:0.88; }
.ca:active { transform:scale(0.96) !important; }
/* ═══════════════════════════════════════════════
TABLET (900px)
═══════════════════════════════════════════════ */
@media (max-width:900px) {
nav { height:auto; padding-top:10px; padding-bottom:10px; flex-wrap:wrap; gap:8px; }
.nav-controls { flex-wrap:wrap; gap:6px; }
.hero { padding:100px 20px 50px; }
.hero-stats { gap:8px; }
.lvl-grid { grid-template-columns:repeat(3,1fr); }
}
/* ═══════════════════════════════════════════════
MOBILE (720px)
═══════════════════════════════════════════════ */
@media (max-width:720px) {
nav { top:8px; width:calc(100% - 16px); padding:0 12px; height:auto; flex-wrap:wrap; padding-top:10px; padding-bottom:10px; gap:8px; border-radius:16px; }
.nav-controls { width:100%; flex-wrap:wrap; }
.chip { flex:1; min-width:0; }
.hero { padding:84px 20px 40px; }
.hero-title { letter-spacing:-1px; line-height:1.05; font-size:clamp(32px,8vw,44px); }
#scan-section { padding:20px 14px 80px; }
.card { padding:18px; border-radius:22px; }
.rule { margin:12px -18px; }
.lvl-grid { grid-template-columns:repeat(3,1fr); }
.hero-stats { gap:7px; }
.stat-glass { padding:12px 16px; min-width:100px; }
}
/* ═══════════════════════════════════════════════
NAV COLLAPSE — gear-toggled controls drawer
(placed after the 720px block so it wins the cascade)
═══════════════════════════════════════════════ */
#nav-gear { display:none; }
@media (max-width:1180px) {
nav { flex-wrap:wrap; }
#nav-gear { display:inline-grid; }
.nav-controls {
display:none; order:10; width:100%; flex-wrap:wrap; gap:6px;
padding-top:8px; margin-top:4px; border-top:1px solid var(--gb);
}
nav.controls-open .nav-controls { display:flex; }
.chip { flex:1 1 30%; min-width:96px; }
}
@media (max-width:480px) {
#fng-pill { display:none !important; }
#status-text { display:none; }
}
/* ═══════════════════════════════════════════════
OBSIDIAN GLASS — RESKIN LAYER
(overrides on top of legacy component CSS;
no selectors removed, JS surface untouched)
═══════════════════════════════════════════════ */
/* sprite icon helper + serif accent */
svg.ic { width:1em; height:1em; stroke:currentColor; fill:none; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; vertical-align:-0.12em; }
.serif { font-family:var(--font-serif); font-style:italic; font-weight:400; letter-spacing:-0.02em; }
/* ── Glass surfaces (backdrop budget: nav 22px; large cards 14px) ── */
nav {
background:var(--glass);
backdrop-filter:blur(22px) saturate(160%);
-webkit-backdrop-filter:blur(22px) saturate(160%);
border:1px solid var(--gb);
box-shadow:var(--shadow), inset 0 1px 0 var(--rim), inset 0 -1px 0 rgba(0,0,0,0.12);
}
.card, .sig-card, #cycle-section, .stat-glass {
background:var(--glass);
backdrop-filter:blur(14px) saturate(160%);
-webkit-backdrop-filter:blur(14px) saturate(160%);
border:1px solid var(--gb);
box-shadow:var(--shadow), inset 0 1px 0 var(--rim);
}
.ticker-wrap {
background:var(--glass);
backdrop-filter:blur(14px) saturate(160%);
-webkit-backdrop-filter:blur(14px) saturate(160%);
}
#cycle-section { position:relative; }
#grid { position:relative; }
/* ── One gradient language: brand gradient only ── */
.brand-mark, #scan-btn, .btn-hero, .sig-cta {
background:var(--grad);
background-size:180% 180%;
}
/* Scan button: animated gradient shimmer (matches mockup .scan-btn) */
#scan-btn { animation:gradShift 6s ease infinite; }
.hero-title .line2 {
background:var(--grad);
-webkit-background-clip:text; background-clip:text;
-webkit-text-fill-color:transparent;
background-size:200% auto;
}
/* ── Typography: numerals → JetBrains Mono, tabular ── */
.stat-num, .tick-price, .tick-chg, .conf-num, .lvl-val, .lvl-pnl, .sz-val,
.prob-val, .lev-val, .bt-stat-val, .bt-state-wr, .bt-state-n,
.live-acc-stat-val, .live-acc-r, .global-acc-wr, .sc-num, .step-time,
.cycle-banner-count, .cycle-stat-val, .cycle-table td, .sig-score-num,
.fc-bar-pct, .ev-score, #refresh-countdown, .chip input,
.badge-chg, .badge-fr, .badge-vol, .badge-oi {
font-family:var(--font-mono);
font-variant-numeric:tabular-nums;
}
/* ── Labels: 10px / 600 / .09em (no 8px bold) ── */
.chip label, .stat-lbl, .sc-lbl, .conf-lbl, .lvl-lbl, .sz-lbl, .prob-lbl,
.lev-lbl, .dur-lbl, .ev-title, .bt-lbl, .bt-stat-lbl, .live-acc-lbl,
.state-lbl, .pa-title, .cycle-stat-lbl, .cycle-banner-label, .sig-score-lbl,
.state-filter-lbl, .global-acc-title, .fc-horizon, .live-acc-stat-lbl {
font-size:10px; font-weight:600; letter-spacing:0.09em; text-transform:uppercase;
}
/* .lvl-lbl (entry/SL/TP/RR box labels) matches mockup's .lvl .k spec exactly */
.lvl-lbl { font-size:9px; font-weight:800; letter-spacing:.12em; }
/* ── Nav theme-toggle / icon buttons (solid, no backdrop) ── */
.icon-btn {
width:36px; height:36px; border-radius:11px; flex-shrink:0;
display:inline-grid; place-items:center; cursor:pointer;
background:var(--glass2); border:1px solid var(--gb); color:var(--t1);
font-size:15px; line-height:1;
transition:transform 0.25s var(--ease-spring), border-color 0.2s;
}
.icon-btn:hover { transform:scale(1.08); border-color:var(--cyan); }
.icon-btn:active { transform:scale(0.9); }
/* native select dropdowns readable on dark */
.chip select option { background:var(--bg2); color:var(--t1); }
/* focus visibility */
:focus-visible { outline:2px solid var(--cyan); outline-offset:2px; }
/* ── Cursor glare on large glass surfaces ── */
.glass-lg::after {
content:''; position:absolute; inset:0; border-radius:inherit;
pointer-events:none; opacity:0; z-index:2;
background:radial-gradient(430px circle at var(--mx,50%) var(--my,50%), var(--glare), transparent 45%);
transition:opacity 0.35s;
}
.glass-lg:hover::after { opacity:1; }
/* ── Scroll reveals ── */
.reveal { opacity:0; transform:translateY(18px); transition:opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out); }
.reveal.in { opacity:1; transform:translateY(0); }
/* ── Theme-swap circular wipe (View Transitions) ── */
::view-transition-old(root), ::view-transition-new(root) { animation:none; mix-blend-mode:normal; }
/* ── Cycle tracker glass polish ── */
.cycle-tab.active { background:var(--glass2); border-color:var(--gb); border-bottom-color:transparent; }
.cycle-tab:hover { background:var(--glass); }
/* hide number spinners everywhere */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
input[type=number] { -moz-appearance:textfield; }
/* ═══════════════════════════════════════════════
ROUND 2 FIXES — mobile, visual polish, tokens
═══════════════════════════════════════════════ */
/* A3: anchored scrolls land below the fixed nav */
#scan-section, #signals-section, #cycle-section, .card { scroll-margin-top:84px; }
/* A2: prevent iOS input-focus zoom (16px minimum) */
@media (max-width:768px) {
.chip input, .chip select { font-size:16px; }
}
/* A4: level boxes on narrow phones */
@media (max-width:480px) {
.lvl-grid { grid-template-columns:repeat(2,1fr); }
.lvl-val { font-size:13px; }
}
/* B5: perf — rendering containment + no backdrop blur on mobile */
.card { contain:layout paint style; }
@media (max-width:768px) {
.card, .card.long-card, .card.short-card, .sig-card, #cycle-section, .stat-glass {
backdrop-filter:none; -webkit-backdrop-filter:none;
background:rgba(12,17,28,0.88);
}
[data-theme="light"] .card, [data-theme="light"] .card.long-card,
[data-theme="light"] .card.short-card, [data-theme="light"] .sig-card,
[data-theme="light"] #cycle-section, [data-theme="light"] .stat-glass {
background:rgba(255,255,255,0.82);
}
.grain { display:none; }
}
/* B6: hero CTAs stack on small phones */
@media (max-width:420px) {
.hero-btns { flex-direction:column; }
.btn-hero, .btn-ghost2 { width:100%; justify-content:center; }
}
@media (max-width:400px) {
.eyebrow { letter-spacing:.12em; }
}
/* C2: news trending chips (decoupled from nav .chip) */
.news-chip {
display:inline-flex; align-items:center; gap:4px;
font-size:11px; font-weight:700; padding:3px 8px;
border:1px solid var(--gb); border-radius:99px;
background:var(--glass2); color:var(--t2); white-space:nowrap;
}
/* C6: taker-flow badge (replaces inline hex styles in JS) */
.badge-taker { font-size:11px; }
.badge-taker.buy { background:var(--mints); color:var(--mint); border:1px solid rgba(61,220,151,0.27); }
.badge-taker.sell { background:var(--reds); color:var(--red); border:1px solid rgba(255,107,122,0.27); }
.badge-taker.flow { background:var(--glass2); color:var(--t3); border:1px solid var(--gb); }
/* E2: on-brand text selection */
::selection { background:rgba(76,201,240,0.28); color:var(--tx); }
[data-theme="light"] ::selection { background:rgba(14,127,168,.22); }
/* E3: missing interactive states */
.live-acc-resolve-btn:disabled { opacity:.5; cursor:wait; }
.state-filter-toggle:hover { border-color:rgba(76,201,240,.3); color:var(--t1); }
.news-ticker-inner a:hover { color:var(--teal); }
/* E7: news marquee — same edge mask as the price ticker */
.news-ticker-inner {
mask-image:linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
-webkit-mask-image:linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
}
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration:0.01ms !important;
animation-iteration-count:1 !important;
transition-duration:0.01ms !important;
scroll-behavior:auto !important;
}
.a-orb { animation:none; }
.reveal { opacity:1; transform:none; }
}
</style>
</head>
<body>
<!-- ── SVG icon sprite ── -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="i-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></symbol>
<symbol id="i-scan" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></symbol>
<symbol id="i-chart" viewBox="0 0 24 24"><path d="M3 3v18h18"/><path d="m7 14 4-4 4 4 5-6"/></symbol>
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></symbol>
<symbol id="i-play" viewBox="0 0 24 24"><polygon points="6 3 20 12 6 21 6 3"/></symbol>
<symbol id="i-heart" viewBox="0 0 24 24"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2C10.5 3.5 9.3 3 7.5 3A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></symbol>
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></symbol>
<symbol id="i-news" viewBox="0 0 24 24"><path d="M4 3h13a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><line x1="7" y1="7" x2="15" y2="7"/><line x1="7" y1="11" x2="15" y2="11"/><line x1="7" y1="15" x2="12" y2="15"/></symbol>
<symbol id="i-trend" viewBox="0 0 24 24"><polyline points="3 17 9 11 13 15 21 5"/><polyline points="14 5 21 5 21 12"/></symbol>
<symbol id="i-trend-down" viewBox="0 0 24 24"><polyline points="3 5 9 11 13 7 21 17"/><polyline points="14 17 21 17 21 10"/></symbol>
<symbol id="i-flame" viewBox="0 0 24 24"><path d="M12 22c4.4 0 8-3.6 8-8 0-3.5-2.5-6.3-4.5-8.5C13.5 3.3 13 2 13 2s-1 4-4 7c-1.9 1.9-3 4-3 6a8 8 0 0 0 6 7.7"/></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></symbol>
<symbol id="i-box" viewBox="0 0 24 24"><path d="M21 8v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8"/><path d="M1 6l11-4 11 4-11 4z"/><path d="M12 6v16"/></symbol>
</defs>
</svg>
<!-- ── Aurora background + grain ── -->
<div class="aurora" aria-hidden="true"><div class="a-orb a1"></div><div class="a-orb a2"></div><div class="a-orb a3"></div></div>
<div class="grain" aria-hidden="true"></div>
<!-- ── Nav ── -->
<nav class="glass-lg">
<a class="brand" href="/">
<div class="brand-mark"><svg class="ic" style="font-size:16px;color:#fff"><use href="#i-zap"/></svg></div>
<span class="brand-name">Trade Copilot</span>
</a>
<span id="status-pill">
<span class="live-dot"></span>
<span id="status-text">Connecting…</span>
</span>
<span id="fng-pill" title="Crypto Fear &amp; Greed Index" style="display:none">
<span id="fng-emoji"><svg class="ic"><use href="#i-flame"/></svg></span>
<span id="fng-label"></span>
<span id="fng-value" style="opacity:0.7;font-size:10px"></span>
</span>
<div class="nav-spacer"></div>
<div class="nav-controls">
<div class="chip"><label for="account">Account $</label><input id="account" type="number" value="10000" min="100"></div>
<div class="chip"><label for="risk">Risk %</label><input id="risk" type="number" value="1" min="0.1" max="5" step="0.1"></div>
<div class="chip"><label for="top">Top</label><input id="top" type="number" value="8" min="3" max="20"></div>
<div class="chip" id="auto-wrap"><label for="auto-interval">Auto</label>
<select id="auto-interval">
<option value="0">Off</option>
<option value="60">1 min</option>
<option value="120">2 min</option>
<option value="300" selected>5 min</option>
</select>
</div>
<div class="chip"><label for="max-hours">Max hold</label>
<select id="max-hours">
<option value="0" selected>Any</option>
<option value="6">6 h</option>
<option value="12">12 h</option>
<option value="24">24 h</option>
<option value="48">48 h</option>
</select>
</div>
<div class="chip"><label for="user-leverage">Lev ×</label>
<select id="user-leverage">
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
<option value="6" selected></option>
<option value="8"></option>
<option value="10">10×</option>
<option value="15">15×</option>
<option value="20">20×</option>
</select>
</div>
</div>
<span id="refresh-countdown"></span>
<button id="nav-gear" class="icon-btn" type="button" title="Scan settings" aria-label="Scan settings" aria-expanded="false"></button>
<button id="theme-toggle" class="icon-btn" type="button" title="Toggle light/dark theme" aria-label="Toggle light/dark theme"></button>
<button id="scan-btn">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
Scan Market
</button>
</nav>
<!-- ── Hero ── -->
<section class="hero">
<div class="eyebrow"><span class="eyebrow-dot"></span>BingX · Real-Time · Zero Hallucination</div>
<h1 class="hero-title">
Your edge in<br><span class="line2">every <span class="serif">market.</span></span>
</h1>
<p class="hero-sub">
Scans 300+ crypto pairs live. Surfaces top setups with entry, stop-loss, take-profit, and position sizing — computed deterministically from BingX + Binance data.
</p>
<div class="hero-btns">
<button class="btn-hero" id="hero-cta">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
Scan Market Now
</button>
<button class="btn-ghost2" onclick="document.getElementById('scan-section').scrollIntoView({behavior:'smooth'})">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
See setups
</button>
</div>
<div class="hero-stats reveal">
<div class="stat-glass"><div class="stat-num" id="stat-pairs"></div><div class="stat-lbl">Top Setups</div></div>
<div class="stat-glass"><div class="stat-num" id="stat-exchange"></div><div class="stat-lbl">Exchange</div></div>
<div class="stat-glass"><div class="stat-num" id="stat-market">USDT</div><div class="stat-lbl">Market</div></div>
<div class="stat-glass"><div class="stat-num" style="font-size:15px;line-height:1.75">35/20/15/15/15</div><div class="stat-lbl">Score Weight</div></div>
</div>
</section>
<!-- ── Ticker tape ── -->
<div class="ticker-wrap">
<div class="ticker-inner" id="ticker">
<span class="tick-item"><span class="tick-sym">BTC</span><span class="tick-price" id="t-btc"></span><span class="tick-chg" id="tc-btc"></span></span>
<span class="tick-item"><span class="tick-sym">ETH</span><span class="tick-price" id="t-eth"></span><span class="tick-chg" id="tc-eth"></span></span>
<span class="tick-item"><span class="tick-sym">SOL</span><span class="tick-price" id="t-sol"></span><span class="tick-chg" id="tc-sol"></span></span>
<span class="tick-item"><span class="tick-sym">BNB</span><span class="tick-price" id="t-bnb"></span><span class="tick-chg" id="tc-bnb"></span></span>
<span class="tick-item"><span class="tick-sym">XRP</span><span class="tick-price" id="t-xrp"></span><span class="tick-chg" id="tc-xrp"></span></span>
<span class="tick-item"><span class="tick-sym">ADA</span><span class="tick-price" id="t-ada"></span><span class="tick-chg" id="tc-ada"></span></span>
<span class="tick-item"><span class="tick-sym">DOGE</span><span class="tick-price" id="t-doge"></span><span class="tick-chg" id="tc-doge"></span></span>
<span class="tick-item"><span class="tick-sym">AVAX</span><span class="tick-price" id="t-avax"></span><span class="tick-chg" id="tc-avax"></span></span>
<!-- duplicate for seamless loop -->
<span class="tick-item"><span class="tick-sym">BTC</span><span class="tick-price" id="t-btc2"></span><span class="tick-chg" id="tc-btc2"></span></span>
<span class="tick-item"><span class="tick-sym">ETH</span><span class="tick-price" id="t-eth2"></span><span class="tick-chg" id="tc-eth2"></span></span>
<span class="tick-item"><span class="tick-sym">SOL</span><span class="tick-price" id="t-sol2"></span><span class="tick-chg" id="tc-sol2"></span></span>
<span class="tick-item"><span class="tick-sym">BNB</span><span class="tick-price" id="t-bnb2"></span><span class="tick-chg" id="tc-bnb2"></span></span>
<span class="tick-item"><span class="tick-sym">XRP</span><span class="tick-price" id="t-xrp2"></span><span class="tick-chg" id="tc-xrp2"></span></span>
<span class="tick-item"><span class="tick-sym">ADA</span><span class="tick-price" id="t-ada2"></span><span class="tick-chg" id="tc-ada2"></span></span>
<span class="tick-item"><span class="tick-sym">DOGE</span><span class="tick-price" id="t-doge2"></span><span class="tick-chg" id="tc-doge2"></span></span>
<span class="tick-item"><span class="tick-sym">AVAX</span><span class="tick-price" id="t-avax2"></span><span class="tick-chg" id="tc-avax2"></span></span>
</div>
</div>
<!-- ── Results ── -->
<!-- ── News Ticker ── -->
<div id="news-ticker" style="display:none;overflow:hidden;background:var(--track);border-bottom:1px solid var(--gb);padding:7px 0;margin-bottom:0">
<div style="display:flex;align-items:center;gap:12px;padding:0 20px">
<span style="font-size:10px;font-weight:800;color:var(--t3);white-space:nowrap;letter-spacing:.06em;flex-shrink:0">TRENDING</span>
<div id="trending-coins" style="display:flex;gap:6px;flex-wrap:nowrap;overflow:hidden"></div>
</div>
<div style="overflow:hidden;margin-top:5px;position:relative">
<div class="news-ticker-inner" style="display:flex;animation:ticker-scroll 60s linear infinite;width:max-content;font-size:11px"></div>
</div>
</div>
<!-- ── Results (Top Setup + Ranked Setups) ── -->
<section id="scan-section" class="reveal">
<div class="section-header">
<div class="section-title"><span class="dot"></span>Live Setups</div>
<div id="meta"></div>
</div>
<div id="error-banner"></div>
<!-- Global Live Accuracy bar — populated by fetchLiveAccuracy() on load -->
<div class="global-acc-bar" id="global-acc-bar">
<span class="global-acc-title"><svg class="ic"><use href="#i-heart"/></svg> Live Accuracy</span>
<span class="global-acc-wr none"></span>
<span class="global-acc-sub">Loading signal history…</span>
<span class="global-acc-spacer"></span>
<button class="live-acc-resolve-btn" onclick="resolveSignals(this)" title="Check outcomes of signals older than 48h">Check Outcomes</button>
</div>
<div id="loader">
<!-- Morphing orb -->
<div class="scan-orb-wrap">
<div class="scan-orb-halo"></div>
<div class="scan-blob"></div>
<div class="scan-orb-core">
<div class="scan-icon-bars">
<div class="sib"></div>
<div class="sib"></div>
<div class="sib"></div>
<div class="sib"></div>
<div class="sib"></div>
</div>
</div>
<div class="scan-orbit"><div class="scan-orbit-dot"></div></div>
</div>
<div class="scan-headline">Scanning the market</div>
<div class="scan-subline">Analysing 300+ USDT pairs · BingX + Binance data fusion</div>
<!-- Step pipeline -->
<div class="scan-pipeline">
<div class="scan-step" id="sd1">
<div class="step-num">1</div>
<div class="step-body">
<div class="step-label">Fetching universe</div>
<div class="step-sub">300+ USDT perpetual pairs from BingX</div>
</div>
<div class="step-time" id="st1"></div>
</div>
<div class="scan-step" id="sd2">
<div class="step-num">2</div>
<div class="step-body">
<div class="step-label">Computing indicators</div>
<div class="step-sub">EMA · RSI · ATR · Support/Resistance</div>
</div>
<div class="step-time" id="st2"></div>
</div>
<div class="scan-step" id="sd3">
<div class="step-num">3</div>
<div class="step-body">
<div class="step-label">Cross-validating sources</div>
<div class="step-sub">BingX funding rate + Binance OI check</div>
</div>
<div class="step-time" id="st3"></div>
</div>
<div class="scan-step" id="sd4">
<div class="step-num">4</div>
<div class="step-body">
<div class="step-label">Ranking setups</div>
<div class="step-sub">35/20/15/15/15 scoring · sorting by confidence</div>
</div>
<div class="step-time" id="st4"></div>
</div>
</div>
<div class="scan-symbol-stream" id="scan-sym-stream"></div>
<!-- Live counters -->
<div class="scan-counter">
<div class="sc-item accent">
<div class="sc-num" id="scan-count">0</div>
<div class="sc-lbl">pairs scanned</div>
</div>
<div class="sc-item">
<div class="sc-num" id="scan-elapsed">0s</div>
<div class="sc-lbl">elapsed</div>
</div>
<div class="sc-item">
<div class="sc-num">BingX</div>
<div class="sc-lbl">source</div>
</div>
</div>
</div>
<div id="empty">
<div class="empty-icon" style="color:var(--t3)"><svg class="ic"><use href="#i-scan"/></svg></div>
<div class="empty-h">Ready to scan</div>
<span class="empty-p">Hit Scan Market to surface top trade setups</span>
</div>
<div id="state-filter-bar" class="state-filter-bar" style="display:none">
<span class="state-filter-lbl"><svg class="ic"><use href="#i-zap"/></svg> State Filter</span>
<button id="state-filter-toggle" class="state-filter-toggle" title="Only show cards where HMM state confidence ≥ 60%">
Show all states
</button>
</div>
<div id="grid"></div>
</section>
<!-- ── Market Signals Panel ── -->
<section id="signals-section" class="reveal">
<div class="section-header">
<div class="section-title"><span class="dot dot-amber"></span>Market Signals</div>
<div id="signals-meta" class="signals-meta-txt"></div>
<button id="signals-refresh" class="signals-refresh-btn" title="Refresh signals">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
</div>
<div id="signals-loader" class="signals-loader">
<div class="signals-spinner"></div>
<span>Scanning mid-cap perps for events…</span>
</div>
<div id="signals-empty" class="signals-empty" style="display:none">
<span style="display:inline-flex"><svg class="ic"><use href="#i-scan"/></svg></span>
<span>No signals right now — market is calm. Will update automatically.</span>
</div>
<div id="signals-grid" class="signals-grid"></div>
</section>
<!-- ── Cycle Tracker ── -->
<section id="cycle-section" class="glass-lg reveal">
<div class="section-header">
<div class="section-title"><span class="dot" style="background:var(--blue2)"></span>Market Cycle Tracker</div>
<span style="font-size:11px;color:var(--t3);font-weight:500">Historical bull &amp; bear pattern analysis</span>
</div>
<!-- Coin tabs — connected to panels below -->
<div class="cycle-tabs-wrap" role="tablist">
<button class="cycle-tab active" data-coin="btc" role="tab" aria-selected="true" onclick="switchCycleTab('btc')">₿ Bitcoin</button>
<button class="cycle-tab" data-coin="eth" role="tab" aria-selected="false" onclick="switchCycleTab('eth')">Ξ Ethereum</button>
<button class="cycle-tab" data-coin="sol" role="tab" aria-selected="false" onclick="switchCycleTab('sol')">◎ Solana ⚠</button>
</div>
<!-- BTC Panel -->
<div id="cycle-panel-btc" class="cycle-panel">
<div class="cycle-banner">
<div>
<div class="cycle-banner-label">Current Phase</div>
<div class="cycle-banner-phase">Bear Market</div>
<div class="cycle-banner-sub">ATH: $125,836 · Oct 6, 2025 · 3 historical cycles</div>
</div>
<div style="text-align:right">
<div class="cycle-banner-label">Est. Days to Avg Bottom</div>
<div id="btc-countdown" class="cycle-banner-count"></div>
<div class="cycle-banner-hint">414 days avg from ATH − days elapsed</div>
</div>
</div>
<div class="cycle-progress-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:12px;font-weight:700;color:var(--t2)">Days elapsed vs. avg bear duration (414 days)</span>
<span id="btc-pct" style="font-size:12px;font-weight:700;color:var(--warn)">—% of avg</span>
</div>
<div style="background:var(--track);border-radius:99px;height:10px;overflow:hidden">
<div id="btc-bar" style="height:100%;border-radius:99px;background:linear-gradient(90deg,var(--red),var(--amber));width:0%;transition:width 1.2s ease"></div>
</div>
<div class="cycle-progress-labels">
<span>● ATH — Oct 6, 2025</span>
<span>● Avg bottom — Dec 2026</span>
<span style="color:var(--profit);font-weight:600">● Bull run →</span>
</div>
<div style="margin-top:8px;font-size:11px;color:var(--t3)">100% does not mean bottom is confirmed — it means the <em>average</em> bear duration has passed. Bottoms can arrive early or late.</div>
</div>
<div class="cycle-stats-row">
<div class="cycle-stat">
<div class="cycle-stat-lbl">Days In Bear</div>
<div class="cycle-stat-val loss" id="btc-days-in"></div>
<div class="cycle-stat-sub">Avg across 3 cycles: ~414 days</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Current Drop</div>
<div class="cycle-stat-val warn" id="btc-drop">—%</div>
<div class="cycle-stat-sub">From $125,836 ATH</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Historical Drop Range</div>
<div class="cycle-stat-val loss">−77–86%</div>
<div class="cycle-stat-sub">2022: −77% · 2018: −84% · 2015: −86%</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Est. Bottom Price</div>
<div class="cycle-stat-val" style="color:var(--blue2)" id="btc-bottom-est">$18–29K</div>
<div class="cycle-stat-sub">If −77% to −86% repeats from ATH</div>
</div>
</div>
<div class="cycle-table-wrap">
<div class="cycle-table-hdr"><svg class="ic"><use href="#i-chart"/></svg> All Historical BTC Bear + Bull Cycles</div>
<table class="cycle-table">
<thead><tr><th>Cycle</th><th>ATH Price</th><th>Bear Days</th><th>Drop</th><th>Bull Days</th><th>Bull Gain</th></tr></thead>
<tbody>
<tr><td>2013–2015</td><td>$1,127</td><td><span class="badge-bear">477 days</span></td><td class="loss-txt">−86%</td><td><span class="badge-bull">~746 days</span></td><td class="profit-txt">+5,800%</td></tr>
<tr class="alt"><td>2017–2018</td><td>$19,665</td><td><span class="badge-bear">389 days</span></td><td class="loss-txt">−84%</td><td><span class="badge-bull">~1,068 days</span></td><td class="profit-txt">+11,000%</td></tr>
<tr><td>2021–2022</td><td>$69,044</td><td><span class="badge-bear">376 days</span></td><td class="loss-txt">−77%</td><td><span class="badge-bull">~1,061 days</span></td><td class="profit-txt">+2,200%</td></tr>
<tr class="now-row"><td>2025–? · IN PROGRESS</td><td>$125,836</td><td><span class="badge-now" id="btc-now-days">— days in</span></td><td class="warn-txt" id="btc-now-drop">—% so far</td><td style="color:var(--t3);font-style:italic">~800–1,000 days?</td><td style="color:var(--t3);font-style:italic">TBD</td></tr>
</tbody>
</table>
</div>
<div class="cycle-note"><svg class="ic"><use href="#i-alert"/></svg> <strong>Pattern, not prediction.</strong> Each BTC cycle has been less extreme — drops get shallower, bull gains shrink. With 3 data points, confidence is moderate. ETF inflows &amp; macro conditions can alter timing significantly. Never trade based on cycle timing alone.</div>
</div>
<!-- ETH Panel -->
<div id="cycle-panel-eth" class="cycle-panel" style="display:none">
<div class="cycle-banner">
<div>
<div class="cycle-banner-label">Current Phase</div>
<div class="cycle-banner-phase">Bear Market</div>
<div class="cycle-banner-sub">ATH: $4,953 · Aug 25, 2025 · Only 2 historical cycles</div>
</div>
<div style="text-align:right">
<div class="cycle-banner-label">Est. Days to Avg Bottom</div>
<div id="eth-countdown" class="cycle-banner-count"></div>
<div class="cycle-banner-hint">365 days avg from ATH − days elapsed</div>
</div>
</div>
<div class="cycle-progress-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:12px;font-weight:700;color:var(--t2)">Days elapsed vs. avg bear duration (365 days)</span>
<span id="eth-pct" style="font-size:12px;font-weight:700;color:var(--warn)">—% of avg</span>
</div>
<div style="background:var(--track);border-radius:99px;height:10px;overflow:hidden">
<div id="eth-bar" style="height:100%;border-radius:99px;background:linear-gradient(90deg,var(--red),var(--amber));width:0%;transition:width 1.2s ease"></div>
</div>
<div class="cycle-progress-labels">
<span>● ATH — Aug 25, 2025</span>
<span>● Avg bottom — Aug 2026</span>
<span style="color:var(--profit);font-weight:600">● Bull run →</span>
</div>
<div style="margin-top:8px;font-size:11px;color:var(--t3)">Based on only 2 cycles — treat this as a rough signal, not a forecast.</div>
</div>
<div class="cycle-stats-row">
<div class="cycle-stat">
<div class="cycle-stat-lbl">Days In Bear</div>
<div class="cycle-stat-val loss" id="eth-days-in"></div>
<div class="cycle-stat-sub">Avg across 2 cycles: ~365 days</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Current Drop</div>
<div class="cycle-stat-val warn" id="eth-drop">—%</div>
<div class="cycle-stat-sub">From $4,953 ATH</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Historical Drop Range</div>
<div class="cycle-stat-val loss">−80–94%</div>
<div class="cycle-stat-sub">2022: −80% · 2018: −94%</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Est. Bottom Price</div>
<div class="cycle-stat-val" style="color:var(--blue2)">$300–990</div>
<div class="cycle-stat-sub">$990 = 2022 pattern · $300 = 2018 pattern</div>
</div>
</div>
<div class="cycle-table-wrap">
<div class="cycle-table-hdr"><svg class="ic"><use href="#i-chart"/></svg> All Historical ETH Bear + Bull Cycles</div>
<table class="cycle-table">
<thead><tr><th>Cycle</th><th>ATH Price</th><th>Bear Days</th><th>Drop</th><th>Bull Days</th><th>Bull Gain</th></tr></thead>
<tbody>
<tr><td>2017–2018</td><td>$1,400</td><td><span class="badge-bear">~365 days</span></td><td class="loss-txt">−94%</td><td><span class="badge-bull">~365 days</span></td><td class="profit-txt">+17,400%</td></tr>
<tr class="alt"><td>2021–2022</td><td>$4,379</td><td><span class="badge-bear">~365 days</span></td><td class="loss-txt">−80%</td><td><span class="badge-bull">~882 days</span></td><td class="profit-txt">+6,000%</td></tr>
<tr class="now-row"><td>2025–? · IN PROGRESS</td><td>$4,953</td><td><span class="badge-now" id="eth-now-days">— days in</span></td><td class="warn-txt" id="eth-now-drop">—% so far</td><td style="color:var(--t3);font-style:italic">~600–900 days?</td><td style="color:var(--t3);font-style:italic">TBD</td></tr>
</tbody>
</table>
</div>
<div class="cycle-note"><svg class="ic"><use href="#i-alert"/></svg> <strong>ETH has only 2 complete cycles — low confidence.</strong> Both bottomed at ~365 days from ATH. ETH's relationship to BTC is shifting with ETF dynamics and changing tokenomics. The $300–$990 range is wide because the 2018 and 2022 cycles were very different market regimes. Less data = less certainty.</div>
</div>
<!-- SOL Panel -->
<div id="cycle-panel-sol" class="cycle-panel" style="display:none">
<div class="cycle-sol-warning">
<span style="font-size:18px;flex-shrink:0"><svg class="ic" style="width:1em;height:1em"><use href="#i-alert"/></svg></span>
<div><strong>Insufficient cycle data.</strong> SOL has only 1 historical bear cycle and it was distorted by the FTX bankruptcy in Nov 2022, causing an unnatural −97% crash. Any projections below are illustrative only — there is no clean historical basis for a SOL bottom estimate.</div>
</div>
<div class="cycle-banner sol">
<div>
<div class="cycle-banner-label">Current Phase</div>
<div class="cycle-banner-phase">Bear Market</div>
<div class="cycle-banner-sub">ATH: $294.85 · Jan 19, 2025 · 1 distorted cycle</div>
</div>
<div style="text-align:right">
<div class="cycle-banner-label">Days In Bear</div>
<div id="sol-countdown" class="cycle-banner-count"></div>
<div class="cycle-banner-hint">No reliable avg — 1 data point only</div>
</div>
</div>
<div class="cycle-progress-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:12px;font-weight:700;color:var(--t2)">Days elapsed vs. prior cycle (418 days — FTX distorted)</span>
<span id="sol-pct" style="font-size:12px;font-weight:700;color:var(--warn)">—% of prior</span>
</div>
<div style="background:var(--track);border-radius:99px;height:10px;overflow:hidden">
<div id="sol-bar" style="height:100%;border-radius:99px;background:linear-gradient(90deg,var(--red),var(--amber));width:0%;transition:width 1.2s ease"></div>
</div>
<div class="cycle-progress-labels">
<span>● ATH — Jan 19, 2025</span>
<span>⚠ Reference only — not a forecast</span>
<span style="color:var(--profit);font-weight:600">● Recovery?</span>
</div>
</div>
<div class="cycle-stats-row">
<div class="cycle-stat">
<div class="cycle-stat-lbl">Days In Bear</div>
<div class="cycle-stat-val loss" id="sol-days-in"></div>
<div class="cycle-stat-sub">Prior cycle: ~418 days (distorted)</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Current Drop</div>
<div class="cycle-stat-val warn">−77%</div>
<div class="cycle-stat-sub">From $294.85 ATH</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Prior Bear Drop</div>
<div class="cycle-stat-val loss">−97%</div>
<div class="cycle-stat-sub">⚠ FTX collapse — not a market cycle</div>
</div>
<div class="cycle-stat">
<div class="cycle-stat-lbl">Est. Bottom Price</div>
<div class="cycle-stat-val" style="color:var(--t3)">Unreliable</div>
<div class="cycle-stat-sub">1 distorted data point — no valid range</div>
</div>
</div>
<div class="cycle-table-wrap">
<div class="cycle-table-hdr"><svg class="ic"><use href="#i-chart"/></svg> SOL Cycle History — ⚠ Extremely Limited Data</div>
<table class="cycle-table">
<thead><tr><th>Cycle</th><th>ATH Price</th><th>Bear Days</th><th>Drop</th><th>Bull Days</th><th>Bull Gain</th></tr></thead>
<tbody>
<tr><td>2021–2022 ⚠</td><td>$259</td><td><span class="badge-bear">~418 days</span></td><td class="loss-txt">−97% (FTX)</td><td><span class="badge-bull">~600 days</span></td><td class="profit-txt">+11,000%</td></tr>
<tr class="alt"><td colspan="6" style="color:var(--loss);font-size:11px;padding:10px 14px">⚠ The −97% drop was caused by FTX bankruptcy (Nov 2022), not natural market forces. This cycle cannot be used as a reliable baseline for future bear depth or duration.</td></tr>
<tr class="now-row"><td>2025–? · IN PROGRESS</td><td>$294.85</td><td><span class="badge-now" id="sol-now-days">— days in</span></td><td class="warn-txt">−77% so far</td><td style="color:var(--t3);font-style:italic">Unknown</td><td style="color:var(--t3);font-style:italic">TBD</td></tr>
</tbody>
</table>
</div>
<div class="cycle-note"><svg class="ic"><use href="#i-alert"/></svg> <strong>SOL projections are speculative.</strong> The current −77% drop already matches a normal crypto bear market without any exchange collapse. SOL may be closer to a natural bottom than the distorted 2022 cycle suggests. Watch BTC/ETH cycle positioning as a leading indicator instead.</div>
</div>
</section>
<div id="toast-root"></div>
<!-- Scroll to top -->
<button id="scroll-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 15l-6-6-6 6"/></svg>
</button>
<script>
/* ═══ Helpers ═══ */
const $ = id => document.getElementById(id);
const sf = n => {
if (n == null) return '—';
const v = +n;
// Magnitude-aware formatting — no trailing zeros, readable at a glance
if (v >= 10000) return v.toLocaleString('en-US',{maximumFractionDigits:0});
if (v >= 1000) return v.toLocaleString('en-US',{maximumFractionDigits:1});
if (v >= 100) return v.toFixed(2);
if (v >= 10) return v.toFixed(3);
if (v >= 1) return v.toFixed(4);
if (v >= 0.001) return v.toFixed(6);
return v.toFixed(8);
};
const fu = n => {
if (n == null) return '—';
const v = +n;
if (v >= 1e9) return '$'+(v/1e9).toFixed(2)+'B';
if (v >= 1e6) return '$'+(v/1e6).toFixed(1)+'M';
if (v >= 1e3) return '$'+(v/1e3).toFixed(1)+'K';
return '$'+v.toFixed(2);
};
const pc = n => n==null?null:(n>=0?'+':'')+( +n).toFixed(2)+'%';
/* ═══ Toast notifications ═══ */
function showToast(msg, type='success') {
const root = $('toast-root'); if (!root) return;
const t = document.createElement('div');
t.className = 'toast ' + type;
const icon = type==='success' ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>' : '';
t.innerHTML = icon + '<span>' + msg + '</span>';
root.appendChild(t);
setTimeout(()=>t.remove(), 2450);
}
/* ═══ Scroll to top ═══ */
window.addEventListener('scroll', () => {
$('scroll-top').classList.toggle('show', window.scrollY > 400);
}, { passive: true });
/* ═══ Status ═══ */
async function checkStatus() {
try {
const d = await fetch('/api/status').then(r=>r.json());
const pill=$('status-pill'), tx=$('status-text');
if (d.ok) {
tx.textContent = d.source.toUpperCase()+' · Live';
pill.className='ok';
const se=$('stat-exchange'); if(se&&se.textContent==='—') se.textContent=d.source.toUpperCase();
} else { tx.textContent='Offline'; pill.className='err'; }
} catch { $('status-text').textContent='Offline'; $('status-pill').className='err'; }
}
/* ═══ Fear & Greed ═══ */
async function checkFearGreed() {
try {
const d = await fetch('/api/catalyst').then(r=>r.json());
const fg = d.fear_greed; if (!fg||fg.value==null) return;
const pill=$('fng-pill');
if (!pill) return;
const val=+fg.value, label=fg.label||'';
// Sentiment class map (icon stays a single flame; color/label carry the sentiment, matching mockup's pill convention)
let cls='fng-neutral';
if(val<=24) { cls='fng-xfear'; }
else if(val<=44) { cls='fng-fear'; }
else if(val<=55) { cls='fng-neutral'; }
else if(val<=74) { cls='fng-greed'; }
else { cls='fng-xgreed'; }
const short=label.replace('Extreme ','X. ');
pill.className=cls; pill.style.display='inline-flex';
$('fng-emoji').innerHTML='<svg class="ic"><use href="#i-flame"/></svg>';
$('fng-label').textContent=short;
$('fng-value').textContent=val+'/100';
pill.title='Fear & Greed Index: '+label+' ('+val+'/100)';
} catch(e) { /* silent — F&G is a bonus */ }
}
/* ═══ Ticker tape ═══ */
const TICKER_SYMS = {
'BTC-USDT':['t-btc','tc-btc','t-btc2','tc-btc2'],
'ETH-USDT':['t-eth','tc-eth','t-eth2','tc-eth2'],
'SOL-USDT':['t-sol','tc-sol','t-sol2','tc-sol2'],
'BNB-USDT':['t-bnb','tc-bnb','t-bnb2','tc-bnb2'],
'XRP-USDT':['t-xrp','tc-xrp','t-xrp2','tc-xrp2'],
'ADA-USDT':['t-ada','tc-ada','t-ada2','tc-ada2'],
'DOGE-USDT':['t-doge','tc-doge','t-doge2','tc-doge2'],
'AVAX-USDT':['t-avax','tc-avax','t-avax2','tc-avax2'],
};
async function updateTicker() {
try {
const syms = Object.keys(TICKER_SYMS).join(',');
const d = await fetch(`/api/prices?symbols=${encodeURIComponent(syms)}`).then(r=>r.json());
for (const [sym,[pid,cid,pid2,cid2]] of Object.entries(TICKER_SYMS)) {
const p = d.prices[sym]; if(!p) continue;
const prEl=$( pid), chEl=$(cid), prEl2=$(pid2), chEl2=$(cid2);
if(prEl) prEl.textContent = sf(p.price);
if(prEl2) prEl2.textContent = sf(p.price);
const chg = p.change_24h_pct;
if(chEl && chg!=null) {
chEl.textContent = (chg>=0?'▲ ':' ▼ ')+Math.abs(chg).toFixed(2)+'%';
chEl.className = 'tick-chg '+(chg>=0?'up':'dn');
if(chEl2) { chEl2.textContent=chEl.textContent; chEl2.className=chEl.className; }
}
}
} catch {}
}
/* ═══ Scanning symbol stream — illustrative rotation, not literal per-pair progress ═══ */
const SCAN_SYM_POOL = ['BTC-USDT','ETH-USDT','SOL-USDT','BNB-USDT','XRP-USDT','ADA-USDT','DOGE-USDT','AVAX-USDT','LINK-USDT','DOT-USDT','MATIC-USDT','LTC-USDT','ATOM-USDT','NEAR-USDT','APT-USDT','ARB-USDT','OP-USDT','SUI-USDT','INJ-USDT','TIA-USDT','FIL-USDT','ETC-USDT','TRX-USDT','ICP-USDT'];
let symStreamTimer=null, symStreamIdx=0;
function startSymbolStream() {
const el=$('scan-sym-stream'); if(!el) return;
symStreamIdx=0; clearInterval(symStreamTimer);
const tick=()=>{
el.style.opacity='0';
setTimeout(()=>{
el.innerHTML='Scanning <span class="sym-cur">'+SCAN_SYM_POOL[symStreamIdx % SCAN_SYM_POOL.length]+'</span>…';
el.style.opacity='1';
symStreamIdx++;
},90);
};
tick();
symStreamTimer=setInterval(tick,260);
}
function stopSymbolStream() { clearInterval(symStreamTimer); }
/* ═══ Loader step animator ═══ */
let stepTimer=null, elapsedTimer=null, countTimer=null, currentStep=0;
let scanStartTime=0;
let step4WaitTimer=null, step4MsgIdx=0;
// Step 4 ("Ranking setups") is the last step and has no further server signal to advance on —
// on a slow cold-start scan (40-90s) it would otherwise sit frozen on "running…" for a long
// time. Rotate its sub-label through reassurance copy every ~5s instead of appearing stuck,
// until stopLoaderSteps() is called when the real fetch resolves.
const STEP4_WAIT_MESSAGES = [
'35/20/15/15/15 scoring · sorting by confidence',
'Still ranking — large markets can take up to 90s',
'Cross-checking confidence bands…',
'Almost there — finalizing setup order…'
];
function startLoaderSteps() {
currentStep=0; scanStartTime=Date.now();
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer); clearInterval(step4WaitTimer);
step4MsgIdx=0;
// Reset all steps
for(let i=1;i<=4;i++){
const el=$('sd'+i); if(!el) continue;
el.className='scan-step';
const num=el.querySelector('.step-num'); if(num) num.textContent=i;
const t=$('st'+i); if(t) t.textContent='';
}
const sd4sub=document.querySelector('#sd4 .step-sub');
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[0];
const sc=$('scan-count'); if(sc) sc.textContent='0';
const se=$('scan-elapsed'); if(se) se.textContent='0s';
// Activate first step immediately
activateStep(1);
startSymbolStream();
// Progress through steps: 1→2 at 3s, 2→3 at 7s, 3→4 at 11s. These are illustrative pacing
// for the first 3 steps only — step 4 has its own indefinite-wait handling below since the
// real scan duration varies 40-90s and isn't streamed to the client.
const timings=[3000,4000,4000];
let cumulative=0;
timings.forEach((delay,i)=>{
cumulative+=delay;
setTimeout(()=>{
markDone(i+1);
activateStep(i+2);
if(i+2===4) startStep4Wait();
}, cumulative);
});
// Elapsed counter
elapsedTimer=setInterval(()=>{
const s=Math.floor((Date.now()-scanStartTime)/1000);
const se=$('scan-elapsed'); if(se) se.textContent=s+'s';
},500);
// Fake count-up: 0→280 over ~12s (pace slows toward end), then holds at 280 gracefully
// (rather than looking frozen mid-count) for the remainder of a slow scan.
let count=0;
countTimer=setInterval(()=>{
const target=280, elapsed=(Date.now()-scanStartTime)/1000;
const progress=Math.min(elapsed/12,1);
count=Math.floor(target*(1-Math.pow(1-progress,2.5)));
const sc=$('scan-count'); if(sc) sc.textContent=count;
if(progress>=1) clearInterval(countTimer);
},120);
}
function startStep4Wait() {
clearInterval(step4WaitTimer);
step4MsgIdx=0;
step4WaitTimer=setInterval(()=>{
step4MsgIdx=(step4MsgIdx+1)%STEP4_WAIT_MESSAGES.length;
const sd4sub=document.querySelector('#sd4 .step-sub');
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[step4MsgIdx];
const t=$('st4'); if(t) t.textContent=Math.floor((Date.now()-scanStartTime)/1000)+'s';
},5000);
}
function activateStep(n) {
if(n>4) return;
const el=$('sd'+n); if(!el) return;
el.className='scan-step active';
const t=$('st'+n); if(t) t.textContent='running…';
}
function markDone(n) {
const el=$('sd'+n); if(!el) return;
el.className='scan-step done';
const num=el.querySelector('.step-num'); if(num) num.textContent='✓';
const elapsed=((Date.now()-scanStartTime)/1000).toFixed(1);
const t=$('st'+n); if(t) t.textContent=elapsed+'s';
}
function stopLoaderSteps() {
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer); clearInterval(step4WaitTimer);
const sd4sub=document.querySelector('#sd4 .step-sub');
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[0];
for(let i=1;i<=4;i++) markDone(i);
const sc=$('scan-count'); if(sc) sc.textContent='280+';
stopSymbolStream();
}
/* ═══ Auto-refresh ═══ */
let autoCountdown=0, countdownTimer=null, liveTickerTimer=null;
const liveCards = new Map();
function getAutoInterval() { return parseInt($('auto-interval').value)||0; }
// Arms the 1s countdown tick without resetting autoCountdown — used by startCountdown()
// and by the visibilitychange resume path. Always clears first, so it can never double.
function armCountdownInterval() {
clearInterval(countdownTimer);
countdownTimer=setInterval(()=>{
autoCountdown--; updateCD();
if(autoCountdown<=0) { clearInterval(countdownTimer); refreshScanQuietly().then(()=>startCountdown()); }
},1000);
}
function startCountdown() {
clearInterval(countdownTimer);
const secs=getAutoInterval(), cd=$('refresh-countdown'), wrap=$('auto-wrap');
if(!secs) {
cd.style.display='none';
if(wrap) wrap.style.borderColor='';
return;
}
cd.style.display='inline-flex'; autoCountdown=secs;
updateCD();
armCountdownInterval();
}
function updateCD() {
const cd=$('refresh-countdown');
const m=Math.floor(autoCountdown/60), s=autoCountdown%60;
cd.textContent=`↻ ${m}:${String(s).padStart(2,'0')}`;
cd.className=autoCountdown<=15?'urgent':'';
const total=getAutoInterval()||1;
const pct=Math.max(0,Math.min(100,(autoCountdown/total)*100));
cd.style.setProperty('--pct', pct.toFixed(1));
}
$('auto-interval').addEventListener('change', startCountdown);
/* ═══ Settings persistence (localStorage) ═══ */
const SETTINGS_KEY = 'tc_settings_v1';
const SETTINGS_FIELDS = [
{ id: 'account', parse: Number, def: 10000 },
{ id: 'risk', parse: Number, def: 1 },
{ id: 'top', parse: Number, def: 8 },
{ id: 'auto-interval', parse: String, def: '300' },
{ id: 'user-leverage', parse: Number, def: 6 },
{ id: 'max-hours', parse: String, def: '0' },
];
function saveSettings() {
const out = {};
SETTINGS_FIELDS.forEach(f => { out[f.id] = $(f.id).value; });
try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(out)); } catch(e) {}
syncAlertConfig();
}
// Push account/risk/top/leverage to the server so the background Telegram
// alert loop uses the same settings as the dashboard, instead of its own
// fixed defaults. Only fires from real user edits (saveSettings only runs on
// 'change'/'blur'), never automatically on page load — so nothing changes
// for the alert loop until you actually touch a setting here. Fire-and-forget:
// failures never block or affect the on-screen scan.
function syncAlertConfig() {
const account = +$('account').value || 10000;
const risk = +$('risk').value || 1;
const top = +$('top').value || 8;
const userLev = +$('user-leverage').value || 6;
const qs = `account=${account}&risk_pct=${risk}&top=${top}&user_leverage=${userLev}`;
fetch(`/api/alert-config?${qs}`, { method: 'POST' }).catch(() => {});
}
function loadSettings() {
let saved = {};
try { saved = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}'); } catch(e) {}
SETTINGS_FIELDS.forEach(f => {
const el = $(f.id);
if (!el) return;
const val = saved[f.id] !== undefined ? saved[f.id] : null;
if (val !== null) el.value = val;
});
}
// Save on every change
SETTINGS_FIELDS.forEach(f => {
const el = $(f.id);
if (el) el.addEventListener('change', saveSettings);
});
// Also save on input (for number fields, catch mid-type changes on blur)
['account','risk','top'].forEach(id => {
const el = $(id);
if (el) el.addEventListener('blur', saveSettings);
});
// Restore on load, then auto-scan immediately
loadSettings();
runScan();
/* ═══ Live price ticker (5s) ═══ */
async function tickPrices() {
if(!liveCards.size) return;
const syms=[...liveCards.keys()].join(',');
try {
const d=await fetch(`/api/prices?symbols=${encodeURIComponent(syms)}`).then(r=>r.json());
const now=new Date(d.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
for(const [sym,{el}] of liveCards) {
const p=d.prices[sym]; if(!p) continue;
// Row-mode collapsed cards: also mirror the live price into the summary bar's
// ENTRY cell (no flash there — keep it simple and cheap).
if (el.classList.contains('row-mode')) {
const rsEntryVal = el.querySelector('.row-summary .rs-stat[data-rs="entry"] .v');
if (rsEntryVal) rsEntryVal.textContent = sf(p.price);
}
const priceEl=el.querySelector('.lvl.entry .lvl-val');
if(priceEl) {
const old=parseFloat(priceEl.dataset.raw||0), nw=p.price;
priceEl.textContent=sf(nw); priceEl.dataset.raw=nw;
if(old&&nw!==old) {
const up = nw>old;
priceEl.style.transition='color 0.35s';
priceEl.style.color=up?'var(--green)':'var(--red)';
setTimeout(()=>{ priceEl.style.color=''; },1400);
const wrap = priceEl.closest('.lvl.entry');
if (wrap) {
wrap.classList.remove('price-flash-up','price-flash-down');
void wrap.offsetWidth; // restart animation if still in-flight
wrap.classList.add(up?'price-flash-up':'price-flash-down');
const deltaPct = ((nw-old)/old*100);
const d = document.createElement('span');
d.className = 'price-delta-float ' + (up?'up':'down');
d.textContent = (up?'+':'') + deltaPct.toFixed(3) + '%';
wrap.appendChild(d);
setTimeout(()=>d.remove(), 1200);
}
}
}
const chgEl=el.querySelector('.badge-chg');
if(chgEl&&p.change_24h_pct!=null) {
const chg=+p.change_24h_pct;
chgEl.textContent=(chg>=0?'+':'')+chg.toFixed(2)+'%';
chgEl.className='badge badge-chg '+(chg>=0?'pos':'neg');
}
const meta=$('meta');
if(meta&&meta.textContent) meta.textContent=meta.textContent.replace(/· \d\d:\d\d.*$/,'· '+now+' · live');
}
} catch {}
}
function startLiveTicker() { clearInterval(liveTickerTimer); liveTickerTimer=setInterval(tickPrices,5000); }
function stopLiveTicker() { clearInterval(liveTickerTimer); liveCards.clear(); }
/* ═══ Backtest runner ═══ */
async function runBacktest(sym, btn) {
const panel = document.getElementById('bt-' + sym);
const resultEl = document.getElementById('bt-result-' + sym);
if (!panel) return;
if (btn) { btn.disabled = true; btn.textContent = '⏳'; }
if (resultEl) resultEl.innerHTML = '<div class="bt-skeleton"><div class="bt-skel-row w1"></div><div class="bt-skel-row w2"></div><div class="bt-skel-row w3"></div></div>';
try {
const bt = await fetch(`/api/backtest/${sym}`).then(r => r.json());
if (bt.error) {
if (resultEl) resultEl.textContent = '⚠ ' + bt.error;
} else {
// Re-render the panel in place, then choreograph the reveal
panel.outerHTML = renderBtResultGlobal(bt, sym);
const fresh = document.getElementById('bt-' + sym);
if (fresh) requestAnimationFrame(() => fresh.classList.add('reveal-in'));
}
} catch(e) {
if (resultEl) resultEl.textContent = '⚠ ' + e.message;
} finally {
if (btn) { btn.disabled = false; btn.textContent = btn.textContent === '⏳' ? 'Run Backtest' : '↺'; }
}
}
// Global version of renderBtResult (same logic, defined outside buildCard scope)
function renderBtResultGlobal(bt, sym) {
const wrTp1 = bt.win_rate_tp1 != null ? Math.round(bt.win_rate_tp1*100) : (bt.win_rate != null ? Math.round(bt.win_rate*100) : null);
const wrTp2 = bt.win_rate_tp2 != null ? Math.round(bt.win_rate_tp2*100) : null;
const wr = wrTp1;
const wrCls = wr==null?'neutral':wr>=55?'win':wr>=45?'neutral':'loss';
const avgR = bt.avg_r_tp1 != null ? (bt.avg_r_tp1 > 0 ? '+' : '') + bt.avg_r_tp1.toFixed(2) + 'R'
: bt.avg_r != null ? (bt.avg_r > 0 ? '+' : '') + bt.avg_r.toFixed(2) + 'R' : '—';
const dd = bt.max_drawdown_r != null ? bt.max_drawdown_r.toFixed(1)+'R' : '—';
const mo = bt.lookback_months || '?';
const n = bt.total_trades || 0;
const tf = bt.candle_interval || '15m';
const blendActive = bt.blend_active === true;
const groundedBadge = blendActive
? `<span class="bt-grounded-badge">✓ Data grounded</span>`
: (n >= 10 ? `<span class="bt-grounded-badge" style="opacity:0.6"><svg class="ic"><use href="#i-chart"/></svg> ${n} trades</span>` : `<span style="font-size:10px;color:var(--muted);padding:2px 6px">⚠ ${n} trades (need 10+)</span>`);
const stateRows = Object.entries(bt.by_state||{}).map(([state, s]) => {
const swr1 = s.win_rate_tp1 != null ? Math.round(s.win_rate_tp1*100) : Math.round((s.win_rate||0)*100);
const swr2 = s.win_rate_tp2 != null ? Math.round(s.win_rate_tp2*100) : null;
const cls = state==='BULL'?'bull':state==='BEAR'?'bear':'range';
const tp2txt = swr2 != null ? `<span style="font-size:11px;color:var(--muted);margin-left:3px">TP2:${swr2}%</span>` : '';
return `<div class="bt-state-cell">
<div class="bt-state-name">${state}</div>
<div class="bt-state-wr ${cls}">${swr1}%${tp2txt}</div>
<div class="bt-state-n">${s.trades} trades</div>
</div>`;
}).join('');
const tp2Row = wrTp2 != null ? `<div style="font-size:10.5px;color:var(--muted);text-align:center;margin-top:2px;margin-bottom:4px">
TP1 exit: <b style="color:${wrTp1>=55?'var(--profit2)':wrTp1>=45?'var(--amber)':'var(--loss2)'}">${wrTp1}%</b> &nbsp;|&nbsp;
TP2 hold: <b style="color:${wrTp2>=55?'var(--profit2)':wrTp2>=45?'var(--amber)':'var(--loss2)'}">${wrTp2}%</b>
</div>` : '';
const curve = bt.equity_curve || [];
let sparkSVG = '';
if (curve.length >= 2) {
const mn = Math.min(...curve), mx = Math.max(...curve);
const range = mx - mn || 1;
const W = 260, H = 52;
const pts = curve.map((v,i) => {
const x = (i/(curve.length-1))*W;
const y = (H-12) - ((v-mn)/range)*(H-12);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const finalColor = curve[curve.length-1] >= 0 ? 'var(--profit2)' : 'var(--loss2)';
const termVal = curve[curve.length-1];
const termLabel = (termVal>=0?'+':'')+termVal.toFixed(1)+'R';
const zeroY = (H-12) - ((0-mn)/range)*(H-12);
sparkSVG = `<svg class="bt-sparkline" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
<polyline fill="none" stroke="${finalColor}" stroke-width="1.5" points="${pts}"/>
<line x1="0" y1="${zeroY.toFixed(1)}" x2="${W}" y2="${zeroY.toFixed(1)}"
stroke="rgba(148,158,178,0.4)" stroke-width="0.5" stroke-dasharray="3,3"/>
<text x="3" y="${(zeroY-2).toFixed(1)}" font-size="8" fill="rgba(148,158,178,0.75)" font-family="monospace">0R</text>
<text x="${W-3}" y="${H}" font-size="9" fill="${finalColor}" font-family="monospace" text-anchor="end" font-weight="700">${termLabel}</text>
</svg>`;
}
return `<div class="bt-panel" id="bt-${sym}">
<div class="bt-header">
<span class="bt-lbl" title="Momentum signal history — structure + volume signals only. Funding rates and news catalyst (40% of live score) are excluded."><svg class="ic"><use href="#i-chart"/></svg> Signal History · ${mo}mo · ${n} trades · ${tf}</span>
${groundedBadge}
<button class="bt-run-btn" onclick="runBacktest('${sym}',this)" title="Refresh backtest">↺</button>
</div>
<div class="bt-stats-row">
<div class="bt-stat"><div class="bt-stat-val ${wrCls}">${wr!=null?wr+'%':'—'}</div><div class="bt-stat-lbl">TP1 Win Rate</div></div>
<div class="bt-stat"><div class="bt-stat-val ${parseFloat(bt.avg_r_tp1||bt.avg_r)>=0?'win':'loss'}">${avgR}</div><div class="bt-stat-lbl">Avg R</div></div>
<div class="bt-stat"><div class="bt-stat-val neutral">-${dd}</div><div class="bt-stat-lbl">Max DD</div></div>
</div>
${tp2Row}
${stateRows?`<div class="bt-state-row">${stateRows}</div>`:''}
${sparkSVG}
</div>`;
}
// ═══ Live Accuracy panel ═══
// Fetches signal accuracy from /api/signal-accuracy and renders a panel
let _liveAccData = null;
async function fetchLiveAccuracy() {
try {
const r = await fetch('/api/signal-accuracy');
_liveAccData = await r.json();
renderGlobalAccuracy(_liveAccData);
} catch(e) {
// Silently fail — accuracy panel is non-critical
}
}
function renderGlobalAccuracy(acc) {
const el = $('global-acc-bar');
if (!el) return;
const n = acc.total_resolved || 0;
const wr = acc.win_rate != null ? Math.round(acc.win_rate * 100) : null;
const pending = acc.pending || 0;
const wins = acc.wins || 0;
const losses = acc.losses || 0;
let wrHTML, wrCls;
if (wr != null) {
wrCls = wr >= 55 ? 'win' : wr >= 45 ? 'neutral' : 'loss';
wrHTML = `<span class="global-acc-wr ${wrCls}">${wr}%</span>`;
} else {
wrCls = 'none';
const needed = acc.min_signals_needed || 0;
wrHTML = `<span class="global-acc-wr none">—</span>`;
}
const avgR = acc.avg_r != null ? (acc.avg_r > 0 ? '+' : '') + acc.avg_r.toFixed(2) + 'R' : '';
const subText = wr != null
? `${wins}W / ${losses}L · ${n} resolved${avgR ? ' · avg ' + avgR : ''}`
: (n < (acc.min_signals_needed || 5)
? `${pending} pending · need ${acc.min_signals_needed || 5} resolved signals to show rate`
: `${pending} pending · resolving in 48h`);
el.innerHTML = `
<span class="global-acc-title"><svg class="ic"><use href="#i-heart"/></svg> Live Accuracy</span>
${wrHTML}
<span class="global-acc-sub">${subText}</span>
<span class="global-acc-spacer"></span>
<button class="live-acc-resolve-btn" onclick="resolveSignals(this)" title="Check outcomes of signals older than 48h">Check Outcomes</button>
`;
}
async function resolveSignals(btn) {
if (btn) { btn.disabled = true; btn.textContent = '⏳'; }
try {
const r = await fetch('/api/resolve-signals', {method:'POST'});
const result = await r.json();
await fetchLiveAccuracy();
if (btn) btn.textContent = `✓ ${result.resolved} resolved`;
setTimeout(() => { if(btn) { btn.disabled=false; btn.textContent='Check Outcomes'; }}, 2500);
} catch(e) {
if (btn) { btn.disabled=false; btn.textContent='Check Outcomes'; }
}
}
function buildLiveAccPanel(sym) {
if (!_liveAccData) return '';
const acc = _liveAccData;
const symData = acc.symbol_breakdown;
const n = acc.total_resolved || 0;
const wr = acc.win_rate != null ? Math.round(acc.win_rate * 100) : null;
const wins = acc.wins || 0;
const losses = acc.losses || 0;
const pending = acc.pending || 0;
let badge, wrVal;
if (wr != null) {
const cls = wr >= 55 ? 'win' : wr >= 45 ? 'pending' : 'loss';
badge = `<span class="live-acc-badge ${cls}">${wr}% live win rate</span>`;
wrVal = `<div class="live-acc-stat-val ${wr>=55?'win':wr<45?'loss':''}">${wr}%</div>`;
} else {
badge = `<span class="live-acc-badge none">Collecting data</span>`;
wrVal = `<div class="live-acc-stat-val" style="color:var(--t3)">—</div>`;
}
const avgR = acc.avg_r != null ? (acc.avg_r > 0 ? '+' : '') + acc.avg_r.toFixed(2) + 'R' : '—';
const note = wr != null
? `Based on ${n} resolved signals · checks 48h price range for TP1/SL hit`
: `${acc.min_signals_needed || 5} more signals needed · checking prices 48h after each signal`;
// Recent log rows (last 5)
const recent = (acc.recent || []).slice(0, 5);
const logRows = recent.map(s => {
const outcomeIcon = s.outcome==='win'?'✓':s.outcome==='loss'?'✗':s.outcome==='expired'?'–':'…';
const r = s.r_achieved != null ? (s.r_achieved>0?'+':'')+s.r_achieved+'R' : '';
const age = s.logged_at ? Math.round((Date.now()/1000 - s.logged_at)/3600) + 'h ago' : '';
return `<div class="live-acc-log-row">
<span class="live-acc-sym">${s.symbol}</span>
<span class="live-acc-dir ${s.direction}">${s.direction}</span>
<span class="live-acc-outcome ${s.outcome}">${outcomeIcon} ${s.outcome}</span>
<span class="live-acc-r">${r}</span>
<span style="font-size:11px;color:var(--t3)">${age}</span>
</div>`;
}).join('');
return `<div class="live-acc-panel">
<div class="live-acc-header">
<span class="live-acc-lbl"><svg class="ic"><use href="#i-heart"/></svg> Live Accuracy (real outcomes)</span>
${badge}
</div>
<div class="live-acc-stats">
<div class="live-acc-stat">${wrVal}<div class="live-acc-stat-lbl">Win Rate</div></div>
<div class="live-acc-stat"><div class="live-acc-stat-val ${acc.avg_r==null?'':acc.avg_r>=0?'win':'loss'}"${acc.avg_r==null?' style="color:var(--t3)"':''}>${avgR}</div><div class="live-acc-stat-lbl">Avg R</div></div>
<div class="live-acc-stat"><div class="live-acc-stat-val">${wins}/${n}</div><div class="live-acc-stat-lbl">W / Resolved</div></div>
<div class="live-acc-stat"><div class="live-acc-stat-val">${pending}</div><div class="live-acc-stat-lbl">Pending</div></div>
</div>
${logRows ? `<div class="live-acc-note">${note}</div><div class="live-acc-log">${logRows}</div>` : `<div class="live-acc-note">${note}</div>`}
</div>`;
}
/* ═══ State filter toggle ═══ */
const _stateToggle = $('state-filter-toggle');
function applyStateFilter() {
const isActive = _stateToggle?.classList.contains('active') || false;
const cards = document.querySelectorAll('#grid .card');
let visible = 0, hidden = 0;
cards.forEach(card => {
const conf = parseFloat(card.dataset.conf || '1');
if (isActive && conf < 0.60) {
card.style.display = 'none';
hidden++;
} else {
card.style.display = '';
visible++;
}
});
// Update toggle label with count
if (_stateToggle) {
if (isActive) {
_stateToggle.textContent = `✓ HMM ≥60% · ${visible} card${visible!==1?'s':''}`;
_stateToggle.title = `Showing ${visible} high-confidence cards · ${hidden} hidden`;
} else {
_stateToggle.textContent = 'Show all states';
_stateToggle.title = 'Only show cards where HMM state confidence ≥ 60%';
}
}
// Show empty message if all cards hidden
const empty = $('empty');
if (empty) {
if (visible === 0 && cards.length > 0 && isActive) {
empty.style.display = 'flex';
const lbl = empty.querySelector('span:last-child');
if (lbl) lbl.textContent = `All ${cards.length} cards filtered out — HMM confidence < 60%. Try widening the filter or running a new scan.`;
} else if (cards.length > 0) {
empty.style.display = 'none';
}
}
}
if (_stateToggle) {
_stateToggle.addEventListener('click', () => {
_stateToggle.classList.toggle('active');
applyStateFilter();
});
}
/* ═══ Nav gear — toggles the collapsed controls drawer on narrow viewports ═══ */
const _navGear = $('nav-gear');
if (_navGear) _navGear.addEventListener('click', () => {
const nav = document.querySelector('nav');
if (!nav) return;
const open = nav.classList.toggle('controls-open');
_navGear.setAttribute('aria-expanded', open ? 'true' : 'false');
});
/* ═══ Scan buttons ═══ */
$('scan-btn').addEventListener('click', runScan);
$('hero-cta').addEventListener('click', ()=>{
document.getElementById('scan-section').scrollIntoView({behavior:'smooth'});
setTimeout(runScan,350);
});
function setLoading(on) {
const btn=$('scan-btn'), hcta=$('hero-cta');
btn.disabled=hcta.disabled=on;
const spinSVG=`<svg class="spin-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-2.12-9.36L21 3v6h-6"/></svg>`;
btn.innerHTML = on ? spinSVG+'Scanning…' : `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>Scan Market`;
hcta.innerHTML = on ? spinSVG+'Scanning…' : `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>Scan Market Now`;
}
/* ═══ FLIP re-rank + never-blank auto-refresh ═══ */
function flipReorder(container, mutation) {
// Keyed by data-sym (stable identity) rather than DOM element reference: refreshScanQuietly
// replaces unchanged cards with brand-new nodes via replaceWith(), so an element-reference
// keyed map would never match post-mutation and the FLIP transform would always be skipped.
const before = new Map([...container.children]
.filter(el => el.dataset && el.dataset.sym)
.map(el => [el.dataset.sym, el.getBoundingClientRect()]));
mutation();
const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced) return;
[...container.children].forEach(el => {
const key = el.dataset && el.dataset.sym;
const f = key ? before.get(key) : null;
if (!f) return; // brand-new/unkeyed element — handled by its own reveal animation
const l = el.getBoundingClientRect();
const dx = f.left - l.left, dy = f.top - l.top;
if (dx || dy) {
el.style.transition = 'none';
el.style.transform = `translate(${dx}px,${dy}px)`;
requestAnimationFrame(() => {
el.style.transition = 'transform 0.5s var(--ease)';
el.style.transform = '';
});
}
});
}
function triggerScanlineSweep() {
const grid = $('grid'); if (!grid) return;
const sweep = document.createElement('div');
sweep.className = 'grid-scanline';
sweep.style.setProperty('--sweep-h', grid.offsetHeight + 'px');
grid.appendChild(sweep);
sweep.addEventListener('animationend', () => sweep.remove());
setTimeout(() => sweep.remove(), 1200); // fallback if animationend doesn't fire
}
// Quiet refresh for the auto-refresh countdown — diffs by symbol, re-ranks in place via
// FLIP, never blanks the grid or shows the big scan loader. Falls back to a full runScan()
// if there's nothing on screen yet to diff against.
async function refreshScanQuietly() {
const grid = $('grid');
if (!grid || !grid.children.length) return runScan();
const account=+$('account').value||10000, risk=+$('risk').value||1;
const top=+$('top').value||8, userLev=+$('user-leverage').value||6;
try {
const data = await fetch(`/api/scan?account=${account}&risk_pct=${risk}&top=${top}&user_leverage=${userLev}`)
.then(r => { if(!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });
const ts = new Date(data.fetched_at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
const meta=$('meta'); if (meta) meta.textContent = `${data.cards.length} setups · ${data.source.toUpperCase()} · ${ts} · auto`;
const maxHours=+($('max-hours')?.value||0);
const allCards = data.cards.filter(c => {
if (maxHours>0) { const durMax=c.duration?.max_hours; if (durMax!=null && durMax>maxHours) return false; }
return true;
});
if (!allCards.length) return; // don't blank an existing view on an empty auto-tick
const seen = new Set();
const existingBySym = new Map([...grid.children].filter(el => el.dataset && el.dataset.sym).map(el => [el.dataset.sym, el]));
flipReorder(grid, () => {
allCards.forEach((c,i) => {
seen.add(c.symbol);
const old = existingBySym.get(c.symbol);
const fresh = buildCard(c, account, risk, {rank:i+1, featured:i===0});
if (old) {
fresh.style.animation = 'none'; // quiet update — no re-entry pop
if (old.classList.contains('open')) fresh.classList.add('open');
old.replaceWith(fresh);
} else {
fresh.style.animationDelay = (i*40) + 'ms';
}
grid.appendChild(fresh); // moves into rank position (appendChild relocates existing nodes)
if (c.symbol) liveCards.set(c.symbol, {el:fresh, account, risk});
});
existingBySym.forEach((el, symKey) => {
if (seen.has(symKey)) return;
el.style.transition = 'opacity 0.3s var(--ease)';
el.style.opacity = '0';
setTimeout(() => el.remove(), 320);
liveCards.delete(symKey);
});
});
requestAnimationFrame(() => { grid.querySelectorAll('.ev-bar, .rs-mbar i').forEach(b => { if(b.dataset.w) b.style.width = b.dataset.w + '%'; }); });
applyStateFilter();
triggerScanlineSweep();
} catch(e) {
// Auto-refresh failures stay silent — don't disrupt the current view, next tick retries
}
}
/* ═══ Main scan ═══ */
async function runScan() {
const grid=$('grid'), meta=$('meta'), err=$('error-banner');
const loader=$('loader'), empty=$('empty');
setLoading(true); grid.innerHTML='';
err.style.display='none'; empty.style.display='none';
loader.style.display='flex'; startLoaderSteps();
stopLiveTicker();
const account=+$('account').value||10000;
const risk=+$('risk').value||1;
const top=+$('top').value||8;
const userLev=+$('user-leverage').value||6;
try {
const data=await fetch(`/api/scan?account=${account}&risk_pct=${risk}&top=${top}&user_leverage=${userLev}`)
.then(r=>{ if(!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });
stopLoaderSteps();
loader.classList.add('exiting');
setTimeout(()=>{ loader.style.display='none'; loader.classList.remove('exiting'); }, 320);
const ts=new Date(data.fetched_at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
meta.textContent=`${data.cards.length} setups · ${data.source.toUpperCase()} · ${ts}`;
const sp=$('stat-pairs'); if(sp) sp.textContent=data.cards.length+'+';
const se=$('stat-exchange'); if(se) se.textContent=data.source.toUpperCase();
if(!data.cards.length) { empty.style.display='flex'; return; }
const maxHours=+($('max-hours')?.value||0);
const stateFilterOn = $('state-filter-toggle')?.classList.contains('active') || false;
// Show state filter bar now that we have data
const sfBar = $('state-filter-bar');
if (sfBar) sfBar.style.display = 'flex';
// Render all cards (no server-side state filter — filter applied client-side via applyStateFilter)
const allCards = data.cards.filter(c => {
if(maxHours>0){ const durMax=c.duration?.max_hours; if(durMax!=null && durMax>maxHours) return false; }
return true;
});
allCards.forEach((c,i)=>{
const el=buildCard(c,account,risk,{rank:i+1,featured:i===0});
el.style.animationDelay=(i*60)+'ms';
grid.appendChild(el);
// Batch bar animations — single rAF after all cards appended
if(c.symbol) liveCards.set(c.symbol,{el,account,risk});
});
// Animate all evidence bars in one batch after DOM is settled
requestAnimationFrame(()=>{
grid.querySelectorAll('.ev-bar, .rs-mbar i').forEach(b=>{ if(b.dataset.w) b.style.width=b.dataset.w+'%'; });
});
// Apply state filter client-side immediately
applyStateFilter();
// If no cards at all (API returned 0)
if(allCards.length===0){
empty.style.display='flex';
const lbl=empty.querySelector('span:last-child');
if(lbl) lbl.textContent='No setups found — market may be consolidating. Try again in a few minutes.';
}
startLiveTicker();
if(getAutoInterval()) startCountdown();
} catch(e) {
loader.style.display='none'; stopLoaderSteps();
// Show human-readable error with retry button
const errMsg = e.message.includes('503') ? 'BingX may be rate-limiting. Try again in 30 seconds.'
: e.message.includes('network') || e.message.includes('fetch') ? 'Network error — check your connection.'
: e.message.includes('timeout') ? 'Scan timed out — server is busy. Try again.'
: 'Scan failed: ' + e.message.slice(0,80);
err.innerHTML = `<span>⚠ ${errMsg}</span>
<button onclick="this.parentElement.style.display='none'; runScan();" style="margin-left:10px;padding:4px 10px;border-radius:6px;border:1px solid rgba(255,107,122,0.3);background:var(--reds);color:var(--red);font-size:11px;font-weight:700;cursor:pointer;">Try Again</button>
<button onclick="this.parentElement.style.display='none';" style="margin-left:6px;padding:4px 8px;border-radius:6px;border:1px solid var(--gb);background:transparent;color:var(--t3);font-size:11px;cursor:pointer;">✕</button>`;
err.style.display='block';
} finally { setLoading(false); }
}
/* ═══ Build card ═══ */
function buildCard(c, account, risk, meta) {
meta = meta || {};
const rank = meta.rank != null ? meta.rank : null;
const featured = !!meta.featured;
const isRowMode = !featured && rank != null;
if(c.error) {
const d=document.createElement('div'); d.className='err-card';
d.textContent=`${c.symbol}: ${c.error}`; return d;
}
const conf=+(c.confidence||0);
const confPct=Math.min(100,conf*10);
const confCls=conf>=7?'hi':conf>=5?'md':'lo';
const confColor=conf>=7?'var(--green)':conf>=5?'var(--amber)':'var(--red)';
const dir=(c.direction||'long').toLowerCase();
const lv=c.levels||{}, sz=c.sizing||{};
const ev=c.evidence||{}, tf=c.timeframes||{};
const ri=c.rank_info||{};
const prob=c.probability||{}, lev=c.leverage||{}, dur=c.duration||{};
const ms=c.market_state||{};
const bts=c.backtest_summary||null;
const sym=c.symbol||'';
const hasDash=sym.includes('-');
const quote=sym.endsWith('USDT')||sym.endsWith('-USDT')?'USDT':'USD';
const base=hasDash?sym.replace(/-USDT$|-USD$/,''):sym.replace(/USDT$|USD$/,'');
const exchName=(c.source||'').toUpperCase();
const secName=(c.secondary_source||'').toUpperCase();
const exchBadge=exchName?`<span class="badge badge-exch">${exchName}${secName?`<span class="exch-sep">+</span>${secName}`:''}</span>`:'';
const cvBadge=c.cross_validated?`<span class="badge badge-cv">✓ Verified</span>`:'';
// News sentiment badge from catalyst evidence
const catEv=(c.evidence||{}).catalyst||{};
const catNotes=catEv.notes||[];
const newsNote=catNotes.find(n=>n.startsWith('News:'))||'';
let newsBadge='';
if(newsNote) {
if(newsNote.includes('no CryptoPanic key')||newsNote.includes('no_key')) {
newsBadge=`<span class="badge badge-news news-nokey" title="Set CRYPTOPANIC_TOKEN in HF Secrets"><svg class="ic"><use href="#i-news"/></svg> No key</span>`;
} else if(newsNote.toLowerCase().includes('bullish')) {
newsBadge=`<span class="badge badge-news news-bullish" title="${newsNote}"><svg class="ic"><use href="#i-news"/></svg> Bullish</span>`;
} else if(newsNote.toLowerCase().includes('bearish')) {
newsBadge=`<span class="badge badge-news news-bearish" title="${newsNote}"><svg class="ic"><use href="#i-news"/></svg> Bearish</span>`;
} else if(newsNote.includes('unavailable')||newsNote.includes('error')) {
newsBadge='';
} else {
newsBadge=`<span class="badge badge-news news-neutral" title="${newsNote}"><svg class="ic"><use href="#i-news"/></svg> Neutral</span>`;
}
}
const chg=ri.change_24h_pct;
const chgBadge=chg!=null?`<span class="badge badge-chg ${chg>=0?'pos':'neg'}">${pc(chg)}</span>`:'';
const volBadge=ri.quote_volume_usd?`<span class="badge badge-vol">${fu(ri.quote_volume_usd)}</span>`:'';
const fr=c.funding_rate;
const frBadge=fr!=null?(()=>{
const frPct=(fr*100).toFixed(4);
const cls=fr>0.0005?'fr-hi':fr<-0.0005?'fr-lo':'fr-n';
return `<span class="badge badge-fr ${cls}">FR ${fr>=0?'+':''}${frPct}%</span>`;
})():'';
const _tr=(c.evidence&&c.evidence.positioning)?c.evidence.positioning.taker_ratio:null;
const takerBadge=_tr!=null?(()=>{
const pct=Math.round(_tr*100);
if(_tr>0.55) return `<span class="badge badge-taker buy">● ${pct}% buy</span>`;
if(_tr<0.45) return `<span class="badge badge-taker sell">● ${pct}% sell</span>`;
return `<span class="badge badge-taker flow">● ${pct}% flow</span>`;
})():'';
const oi=c.open_interest;
const oiBadge=oi!=null&&oi>0&&c.close?`<span class="badge badge-oi">OI ${fu(oi*c.close)}</span>`:'';
const tfHTML=['1h','15m','5m'].map(t=>{
const s=(tf[t]?.structure||'').toLowerCase();
const cls=s==='uptrend'?'up':s==='downtrend'?'down':s?'range':'na';
return `<span class="tf ${cls}">${t} · ${s||'n/a'}</span>`;
}).join('');
const evKeys = c.pa_available
? ['structure','volume','positioning','catalyst','price_action']
: ['structure','volume','positioning','catalyst'];
const evLabels = {structure:'Structure',volume:'Volume',positioning:'Positioning',catalyst:'Catalyst',price_action:'Price Action'};
const evHTML = evKeys.map(k=>{
const e=ev[k]||{}, sc=+(e.score||0);
const barCls = sc>=7?'hi':sc>=4?'med':'lo';
const lbl = evLabels[k] || (k[0].toUpperCase()+k.slice(1));
return `<div class="ev-row">
<span class="ev-key">${lbl}</span>
<div class="ev-track"><div class="ev-bar ${barCls}" data-w="${sc*10}" style="width:0%"></div></div>
<span class="ev-score" style="color:${sc>=7?'var(--profit)':sc>=4?'var(--warn)':'var(--loss)'}">${sc}</span>
</div>`;
}).join('');
// ── Probability / EV block ──
const pwin=prob.p_win!=null?Math.round(prob.p_win*100):null;
const ploss=pwin!=null?100-pwin:null;
const evVal=prob.ev!=null?prob.ev:null;
const evCls=evVal==null?'ev-zero':evVal>0?'ev-pos':'ev-neg';
const evStr=evVal!=null?(evVal>0?'+':'')+evVal.toFixed(2):'—';
const probHTML=pwin!=null?`
<div class="prob-strip">
<div class="prob-block">
<div class="prob-lbl">Win %</div>
<div class="prob-val pwin${prob.bt_grounded ? ' grounded' : ''}">${pwin}%</div>
</div>
<div class="prob-divider"></div>
<div class="prob-block">
<div class="prob-lbl">Loss %</div>
<div class="prob-val ploss">${ploss}%</div>
</div>
<div class="prob-divider"></div>
<div class="prob-block">
<div class="prob-lbl">Exp. Value</div>
<div class="prob-val ${evCls}">${evStr}</div>
</div>
</div>`:'';
// ── Leverage block (user-configured) ──
const userLevVal = +($('user-leverage')?.value || 6);
const marginToPost = lev.margin_to_post != null ? lev.margin_to_post : (sz.notional ? +(sz.notional / userLevVal).toFixed(2) : null);
const lossAtStop = lev.loss_at_stop != null ? lev.loss_at_stop : null;
const liqDistPct = lev.liq_dist_pct != null ? lev.liq_dist_pct : +(( 1/userLevVal - 0.005)*100).toFixed(2);
const stopDistPct = lv.sl && c.close ? +(Math.abs(lv.sl - c.close)/c.close*100).toFixed(2) : null;
const levProfitUsd = lev.actual_profit_usd != null ? lev.actual_profit_usd : null;
const levProfitPct = lev.actual_profit_pct != null ? lev.actual_profit_pct : null;
const levNeeded = lev.needed_for_target != null ? lev.needed_for_target : null;
const levHTML = `
<div class="lev-strip">
<div class="lev-icon" style="color:var(--cyan)"><svg class="ic"><use href="#i-zap"/></svg></div>
<div class="lev-main">
<div class="lev-lbl">At ${userLevVal}× leverage (your setting)</div>
${marginToPost != null ? `<div class="lev-val">Margin to post: <strong>$${Number(marginToPost).toFixed(2)} USDT</strong> <span class="lev-max">← enter on BingX "By Cost"</span></div>` : ''}
${lossAtStop != null ? `<div class="lev-warn">If stop hit: lose $${Number(lossAtStop).toFixed(2)}${stopDistPct!=null?' (stop '+stopDistPct+'% away)':''}</div>` : ''}
${liqDistPct != null ? `<div class="lev-cap">Liq. ~${liqDistPct}% from entry${stopDistPct!=null&&liqDistPct>stopDistPct?' · stop fires first ✓':' · ⚠ check stop placement'}</div>` : ''}
${levProfitUsd != null ? `<div class="lev-profit">TP1 profit: +$${Number(levProfitUsd).toFixed(2)} (+${levProfitPct}% on account)</div>` : ''}
${levNeeded != null ? `<div class="lev-cap" style="margin-top:2px">Need ${levNeeded}× for +5% account at TP1</div>` : ''}
<div class="lev-warn" style="margin-top:6px;font-size:10px">⚠ Set <strong>isolated margin</strong> on BingX before placing</div>
${lv.sl ? `<div class="lev-cap" style="font-size:10px;color:var(--loss)">⚠ Place stop order at ${sf(lv.sl)} before position opens</div>` : ''}
</div>
</div>`;
// ── Duration block ──
const durLabel=dur.label||null;
const durHTML=durLabel?`
<div class="dur-strip">
<div class="dur-icon">⏱</div>
<div class="dur-main">
<div class="dur-lbl">Time to TP1 (est.)</div>
<div class="dur-val">${durLabel}</div>
</div>
</div>`:'';
// ── Market State block (Markov + HMM) ──
let stateHTML = '';
let stateBadgesSimpleHTML = ''; // lightweight HMM/MK pills only — used in the featured card's left rail
let cardStateConf = null; // exposed on card root so applyStateFilter() works (bug fix)
const mkData = ms.markov, hmmData = ms.hmm;
if (mkData || hmmData) {
const bestState = hmmData?.state || mkData?.state || 'RANGING';
const bestConf = hmmData?.confidence || mkData?.confidence || 0.5;
cardStateConf = bestConf;
const confPctState = Math.round(bestConf * 100);
const aligned = ms.aligned_with_trade !== false;
const conflictNote = !aligned
? `<div class="state-conflict">⚠ State conflicts with trade direction — lower conviction</div>` : '';
// Markov + HMM badges
let badges = '';
if (hmmData) badges += `<span class="state-badge ${hmmData.state}" title="HMM: probabilistic inference">HMM · ${hmmData.state} <span class="state-conf">${Math.round(hmmData.confidence*100)}%</span></span>`;
if (mkData) badges += `<span class="state-badge ${mkData.state}" title="Markov: state + transition matrix">MK · ${mkData.state} <span class="state-conf">${Math.round(mkData.confidence*100)}%</span></span>`;
stateBadgesSimpleHTML = `<div class="state-badges featured-state-pills">${badges}</div>`;
// Forecast bars for 1d, 3d, 7d
let forecastHTML = '';
const forecast = mkData?.forecast || {};
if (Object.keys(forecast).length > 0) {
const horizons = [['1d','1 Day'], ['3d','3 Day'], ['7d','7 Day']];
forecastHTML = `<div class="forecast-row">` + horizons.map(([key, label]) => {
const f = forecast[key] || {};
return `<div class="fc-cell">
<div class="fc-horizon">${label}</div>
<div class="fc-bars">
${['BULL','BEAR','RANGING'].map(s => {
const pct = Math.round((f[s]||0)*100);
return `<div class="fc-bar-row">
<span class="fc-bar-lbl">${s[0]}</span>
<div class="fc-bar-track"><div class="fc-bar-fill ${s}" style="width:${pct}%"></div></div>
<span class="fc-bar-pct">${pct}%</span>
</div>`;
}).join('')}
</div>
</div>`;
}).join('') + `</div>`;
}
stateHTML = `
<div class="state-strip" data-state="${bestState}" data-conf="${bestConf}">
<div class="state-strip-top">
<span class="state-lbl"><svg class="ic"><use href="#i-scan"/></svg> Market State (Markov + HMM)</span>
<span class="state-trust">✓ No lookahead bias</span>
</div>
<div class="state-badges">${badges}</div>
${conflictNote}
${forecastHTML}
</div>`;
}
// ── Backtest panel ──
function buildBtPanel(bt, sym) {
if (!bt || !bt.total_trades) {
return `<div class="bt-panel" id="bt-${sym}">
<div class="bt-header">
<span class="bt-lbl" title="Momentum signal history — structure + volume signals only. Funding rates and news catalyst (40% of live score) are excluded from backtest."><svg class="ic"><use href="#i-chart"/></svg> Signal History · structure+vol</span>
<button class="bt-run-btn" onclick="runBacktest('${sym}',this)">Run Backtest</button>
</div>
<div class="bt-loading" id="bt-result-${sym}">Not yet run — click to backtest ~6 months of 15m history</div>
</div>`;
}
return renderBtResult(bt, sym);
}
function renderBtResult(bt, sym) {
// Primary = TP1 win rate (matches how most traders exit)
// Secondary = TP2 win rate (ambitious hold target)
const wrTp1 = bt.win_rate_tp1 != null ? Math.round(bt.win_rate_tp1*100) : (bt.win_rate != null ? Math.round(bt.win_rate*100) : null);
const wrTp2 = bt.win_rate_tp2 != null ? Math.round(bt.win_rate_tp2*100) : null;
const wrCls = wrTp1==null?'neutral':wrTp1>=55?'win':wrTp1>=45?'neutral':'loss';
const avgR = bt.avg_r_tp1 != null ? (bt.avg_r_tp1 > 0 ? '+' : '') + bt.avg_r_tp1.toFixed(2) + 'R'
: bt.avg_r != null ? (bt.avg_r > 0 ? '+' : '') + bt.avg_r.toFixed(2) + 'R' : '—';
const dd = bt.max_drawdown_r != null ? bt.max_drawdown_r.toFixed(1)+'R' : '—';
const mo = bt.lookback_months || '?';
const n = bt.total_trades || 0;
const tf = bt.candle_interval || '15m';
// Data Grounded badge — only show when blend actually fired (N >= 10)
const blendActive = bt.blend_active === true;
const groundedBadge = blendActive
? `<span class="bt-grounded-badge">✓ Data grounded</span>`
: (n >= 10 ? `<span class="bt-grounded-badge" style="opacity:0.6"><svg class="ic"><use href="#i-chart"/></svg> ${n} trades</span>` : `<span style="font-size:10px;color:var(--muted);padding:2px 6px">⚠ ${n} trades (need 10+)</span>`);
// State breakdown — show both TP1 and TP2 win rates per state
const stateRows = Object.entries(bt.by_state||{}).map(([state, s]) => {
const swr1 = s.win_rate_tp1 != null ? Math.round(s.win_rate_tp1*100) : Math.round((s.win_rate||0)*100);
const swr2 = s.win_rate_tp2 != null ? Math.round(s.win_rate_tp2*100) : null;
const cls = state==='BULL'?'bull':state==='BEAR'?'bear':'range';
const tp2txt = swr2 != null ? `<span style="font-size:11px;color:var(--muted);margin-left:3px">TP2:${swr2}%</span>` : '';
return `<div class="bt-state-cell">
<div class="bt-state-name">${state}</div>
<div class="bt-state-wr ${cls}">${swr1}%${tp2txt}</div>
<div class="bt-state-n">${s.trades} trades</div>
</div>`;
}).join('');
// Equity sparkline (based on TP1 — matches real trading)
const curve = bt.equity_curve || [];
let sparkSVG = '';
if (curve.length >= 2) {
const mn = Math.min(...curve), mx = Math.max(...curve);
const range = mx - mn || 1;
const W = 260, H = 52;
const pts = curve.map((v,i) => {
const x = (i/(curve.length-1))*W;
const y = (H-12) - ((v-mn)/range)*(H-12);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const finalColor = curve[curve.length-1] >= 0 ? 'var(--profit2)' : 'var(--loss2)';
const termVal = curve[curve.length-1];
const termLabel = (termVal>=0?'+':'')+termVal.toFixed(1)+'R';
const zeroY = (H-12) - ((0-mn)/range)*(H-12);
sparkSVG = `<svg class="bt-sparkline" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
<polyline fill="none" stroke="${finalColor}" stroke-width="1.5" points="${pts}"/>
<line x1="0" y1="${zeroY.toFixed(1)}" x2="${W}" y2="${zeroY.toFixed(1)}"
stroke="rgba(148,158,178,0.4)" stroke-width="0.5" stroke-dasharray="3,3"/>
<text x="3" y="${(zeroY-2).toFixed(1)}" font-size="8" fill="rgba(148,158,178,0.75)" font-family="monospace">0R</text>
<text x="${W-3}" y="${H}" font-size="9" fill="${finalColor}" font-family="monospace" text-anchor="end" font-weight="700">${termLabel}</text>
</svg>`;
}
// TP2 comparison row
const tp2Row = wrTp2 != null ? `<div style="font-size:10.5px;color:var(--muted);text-align:center;margin-top:2px;margin-bottom:4px">
TP1 exit: <b style="color:${wrTp1>=55?'var(--profit2)':wrTp1>=45?'var(--amber)':'var(--loss2)'}">${wrTp1}%</b> &nbsp;|&nbsp;
TP2 hold: <b style="color:${wrTp2>=55?'var(--profit2)':wrTp2>=45?'var(--amber)':'var(--loss2)'}">${wrTp2}%</b>
</div>` : '';
return `<div class="bt-panel" id="bt-${sym}">
<div class="bt-header">
<span class="bt-lbl" title="Momentum signal history — structure + volume signals only. Funding rates and news catalyst (40% of live score) are excluded."><svg class="ic"><use href="#i-chart"/></svg> Signal History · ${mo}mo · ${n} trades · ${tf}</span>
${groundedBadge}
<button class="bt-run-btn" onclick="runBacktest('${sym}',this)" title="Refresh backtest">↺</button>
</div>
<div class="bt-stats-row">
<div class="bt-stat"><div class="bt-stat-val ${wrCls}">${wrTp1!=null?wrTp1+'%':'—'}</div><div class="bt-stat-lbl">TP1 Win Rate</div></div>
<div class="bt-stat"><div class="bt-stat-val ${parseFloat(bt.avg_r_tp1||bt.avg_r)>=0?'win':'loss'}">${avgR}</div><div class="bt-stat-lbl">Avg R</div></div>
<div class="bt-stat"><div class="bt-stat-val neutral">-${dd}</div><div class="bt-stat-lbl">Max DD</div></div>
</div>
${tp2Row}
${stateRows?`<div class="bt-state-row">${stateRows}</div>`:''}
${sparkSVG}
</div>`;
}
// ── Price Action section ──
function buildPriceActionSection(card) {
const pa = card.price_action;
if (!pa || !pa.available) return '';
// Confluence grade badge
const grade = (pa.confluence_grade || '').toUpperCase();
const gradeCls = grade === 'A+' ? 'pa-grade-ap'
: grade === 'A' ? 'pa-grade-a'
: grade === 'B' ? 'pa-grade-b'
: grade === 'C' ? 'pa-grade-c'
: grade === 'D' ? 'pa-grade-d'
: 'pa-grade-b';
const gradeHtml = grade
? `<span class="pa-grade ${gradeCls}">${grade}</span>` : '';
const paScore = pa.pa_score != null
? `<span class="pa-score-num">PA ${Math.round(pa.pa_score * 100)}%</span>` : '';
// Strength dots helper: filled ● up to strength, empty ○ to max 3
function dots(n, max) {
const filled = Math.min(n, max);
return '●'.repeat(filled) + '○'.repeat(Math.max(0, max - filled));
}
// ── Candlestick patterns row
let csHTML = '';
const csPats = (pa.candlestick && pa.candlestick.patterns) || [];
if (csPats.length > 0) {
const shown = csPats.slice(0, 2);
const extra = csPats.length - shown.length;
const items = shown.map((p, i) => {
const sigCls = p.signal === 'bullish' ? 'pa-signal-bull' : 'pa-signal-bear';
const sigArrow = p.signal === 'bullish' ? '<svg class="ic"><use href="#i-trend"/></svg>' : '<svg class="ic"><use href="#i-trend-down"/></svg>';
const keyBadge = p.at_key_level ? `<span class="pa-key-badge">KEY</span>` : '';
const descHtml = p.description
? `<div class="pa-desc"><div class="pa-desc-inner">${p.description}</div></div>` : '';
return `<div class="pa-row" onclick="this.classList.toggle('expanded')">
<span class="pa-row-icon ${sigCls}">${sigArrow}</span>
<div class="pa-row-body">
<div class="pa-row-top">
<span class="pa-pat-name">${p.name}</span>
<span class="${sigCls}">${p.signal}</span>
<span class="pa-dots">${dots(p.strength || 1, 3)}</span>
${keyBadge}
${i === shown.length - 1 && extra > 0 ? `<span class="pa-more">+${extra} more</span>` : ''}
</div>
${descHtml}
</div>
</div>`;
}).join('');
csHTML = items;
}
// ── Chart pattern row
let cpHTML = '';
const cpPats = (pa.chart_patterns && pa.chart_patterns.patterns) || [];
if (cpPats.length > 0) {
const p = cpPats[0]; // show first/strongest
const stage = (p.stage || '').toLowerCase();
const stageCls = stage === 'confirmed' ? 'pa-stage-confirmed'
: stage === 'broken' ? 'pa-stage-broken'
: 'pa-stage-forming';
const stageLabel = stage.toUpperCase() || 'FORMING';
const targetHtml = p.target_pct != null
? `<span class="pa-target">Target: +${p.target_pct}%</span>` : '';
const sigArrow = p.signal === 'bullish' ? '<svg class="ic"><use href="#i-trend"/></svg>' : '<svg class="ic"><use href="#i-trend-down"/></svg>';
const sigCls = p.signal === 'bullish' ? 'pa-signal-bull' : 'pa-signal-bear';
const descHtml = p.description
? `<div class="pa-desc"><div class="pa-desc-inner">${p.description}</div></div>` : '';
cpHTML = `<div class="pa-row" onclick="this.classList.toggle('expanded')">
<span class="pa-row-icon ${sigCls}">${sigArrow}</span>
<div class="pa-row-body">
<div class="pa-row-top">
<span class="pa-pat-name">${p.name}</span>
<span class="${stageCls}">${stageLabel}</span>
${targetHtml}
</div>
${descHtml}
</div>
</div>`;
}
// ── Order block summary line
let obHTML = '';
const ob = pa.order_blocks && pa.order_blocks.result;
if (ob) {
if (ob.summary) {
obHTML = `<div class="pa-ob-line"><svg class="ic"><use href="#i-box"/></svg> ${ob.summary}</div>`;
} else {
const lines = [];
if (ob.nearest_ob_below) {
const z = ob.nearest_ob_below;
lines.push(`OB Support: ${z.zone_low.toLocaleString()}${z.zone_high.toLocaleString()}${z.status === 'fresh' ? ' (fresh)' : ''}`);
}
if (ob.nearest_ob_above) {
const z = ob.nearest_ob_above;
lines.push(`OB Resistance: ${z.zone_low.toLocaleString()}${z.zone_high.toLocaleString()}${z.status === 'fresh' ? ' (fresh)' : ''}`);
}
if (lines.length) obHTML = `<div class="pa-ob-line"><svg class="ic"><use href="#i-box"/></svg> ${lines.join(' · ')}</div>`;
}
}
// ── Conflict warning
const conflictHTML = card.pattern_direction_conflict
? `<div class="pa-conflict">⚠ Patterns suggest opposite direction</div>` : '';
// If we have nothing to show, return empty
if (!csHTML && !cpHTML && !obHTML && !conflictHTML && !grade) return '';
return `<div class="pa-section">
<div class="pa-header">
<span class="pa-title">Price Action</span>
${gradeHtml}
${paScore}
</div>
${csHTML}
${cpHTML}
${obHTML}
${conflictHTML}
</div>`;
}
const btHTML = buildBtPanel(bts, sym);
// ── Veto banner: shown when R/R < 1.0 (hard no-trade) ──
const isVetoed = c.vetoed === true;
const vetoBanner = isVetoed && c.veto_reason ? `
<div class="veto-banner">
<div class="veto-banner-title"><svg class="ic" style="width:1em;height:1em;vertical-align:-2px"><use href="#i-alert"/></svg> NO TRADE — Risk Exceeds Reward</div>
<div class="veto-banner-reason">${c.veto_reason}</div>
</div>` : '';
let narr=(c.narrative||'');
if(narr.length>210) narr=narr.slice(0,207)+'…';
const plan=[
`${sym} ${dir.toUpperCase()} | Confidence ${conf}/10`,
`Entry ${sf(c.close)}`,
`SL ${sf(lv.sl)}`,
`TP1 ${sf(lv.tp1)} TP2 ${sf(lv.tp2)} TP3 ${sf(lv.tp3)}`,
`R/R ${lv.rr1??'—'}×`,
`Risk ${fu(sz.risk_usd)} | Units ${sz.units??'—'} | Notional ${fu(sz.notional)}`,
`Exchange ${exchName}${secName?'+'+secName:''}`,
`Time ${new Date().toLocaleString()}`
].join('\n');
const tvUrl=`https://www.tradingview.com/chart/?symbol=${encodeURIComponent(c.tv_symbol||sym)}`;
const el=document.createElement('div');
el.className=`card ${dir}-card${featured?' featured-card':''}${isRowMode?' row-mode':''}`;
el.dataset.sym = sym; // used by never-blank auto-refresh to diff/re-rank in place
if (rank != null) el.dataset.rank = String(rank);
if (featured) el.dataset.featured = '1';
if (cardStateConf != null) el.dataset.conf = String(cardStateConf); // data-conf on card root (state filter fix)
// ── Row-summary bar — collapsed header for non-featured ranked rows ──
const rsArrow = dir==='long' ? '▲' : '▼';
const rr1forRow = lv.rr1;
const rowSummaryHTML = isRowMode ? `
<div class="row-summary">
<span class="rs-rank">${String(rank).padStart(2,'0')}</span>
<div class="rs-sym-wrap"><span class="rs-sym">${sym}</span><span class="rs-dir ${dir}">${rsArrow} ${dir.toUpperCase()}</span></div>
<div class="rs-conf"><b style="color:${confColor}">${conf}</b><span class="rs-mbar ${conf<5?'lo':''}"><i data-w="${confPct}"></i></span></div>
<div class="rs-stat" data-rs="entry"><div class="k">ENTRY</div><div class="v">${sf(c.close)}</div></div>
<div class="rs-stat"><div class="k">R/R</div><div class="v" style="color:${rr1forRow!=null&&rr1forRow<1?'var(--loss)':'var(--t1)'}">${rr1forRow??'—'}×</div></div>
<div class="rs-stat"><div class="k">WIN %</div><div class="v" style="color:${pwin!=null?(pwin>=50?'var(--green)':'var(--red)'):'var(--t1)'}">${pwin!=null?pwin+'%':'—'}</div></div>
<div class="rs-stat"><div class="k">EV</div><div class="v" style="color:${evVal!=null?(evVal>0?'var(--green)':'var(--red)'):'var(--t1)'}">${evVal!=null?(evVal>0?'+':'')+evVal.toFixed(2):'—'}</div></div>
<div class="rs-stat"><div class="k">EST. TP1</div><div class="v">${durLabel||'—'}</div></div>
<span class="rs-exp">▾</span>
</div>` : '';
// ── Featured ring + tier tag — shown only on the #1 ranked "Top Setup" card.
// "TIER" here is derived from the AI confidence score (0–10) — it is NOT
// a pattern-based grade. The real price-action confluence grade (from
// chart patterns + candlesticks + order blocks, computed in confluence.py)
// is shown separately via the "PA GRADE" chip below, sourced directly
// from card.price_action.confluence_grade. The two can disagree. ──
const ringCirc = 226.2; // 2*pi*36
const gradeLetter = conf>=7 ? 'A' : conf>=5 ? 'B' : 'C';
const gradeCssCls = isVetoed ? 'gC' : gradeLetter==='A' ? 'gA' : gradeLetter==='B' ? 'gB' : 'gC';
const gradeTagLabel = isVetoed ? 'VETOED · NO TRADE' : `TIER ${gradeLetter} · TRADEABLE`;
// ── Real Price Action confluence grade (pattern-based, independent of confidence) ──
const paGradeRaw = (c.pa_available && c.price_action?.confluence_grade) ? c.price_action.confluence_grade : null;
const paScoreRaw = c.price_action?.confluence_score;
const paChipCls = paGradeRaw === 'A+' || paGradeRaw === 'A' ? 'pgA'
: paGradeRaw === 'B' ? 'pgB'
: paGradeRaw === 'C' ? 'pgC'
: paGradeRaw === 'D' ? 'pgD'
: 'pgNA';
const paChipLabel = paGradeRaw
? `PA GRADE ${paGradeRaw}${paScoreRaw!=null?` · ${Math.round(paScoreRaw)}`:''}`
: 'PA · N/A';
const paGradeChipHTML = `<span class="pa-grade-chip ${paChipCls}">${paChipLabel}</span>`;
const featuredRingWrapHTML = featured ? `
<div class="conf-ring-wrap">
<svg viewBox="0 0 84 84">
<circle class="conf-ring-bg" cx="42" cy="42" r="36" stroke-width="7"/>
<circle class="conf-ring-val" cx="42" cy="42" r="36" stroke-width="7" stroke="${confColor}" stroke-dasharray="${ringCirc}" stroke-dashoffset="${(ringCirc-(ringCirc*confPct/100)).toFixed(1)}"/>
</svg>
<div class="conf-ring-center"><b>${conf}</b><span>/ 10</span></div>
</div>` : '';
// ── Named content blocks — shared between the normal single-column layout
// and the featured two-column layout, so nothing is duplicated or lost ──
const chHTML = `
<div class="ch">
<div class="ch-left">
<div class="ch-sym">${base}<span class="q">${quote}</span></div>
<div class="ch-badges">${exchBadge}${cvBadge}${newsBadge}${chgBadge}${volBadge}${frBadge}${takerBadge}${oiBadge}</div>
</div>
<span class="dir-tag ${dir}">${dir.toUpperCase()}</span>
</div>`;
const confBoxHTML = `
<div class="conf-box">
<div class="conf-row">
<span class="conf-lbl">Confidence Score</span>
<span class="conf-num" style="color:${confColor}">${conf}<span class="conf-denom">/10</span></span>${(()=>{const g=(c.price_action||{}).confluence_grade;if(!g)return '';const gc=g==='A+'?'pa-grade-ap':g==='A'?'pa-grade-a':g==='B'?'pa-grade-b':g==='C'?'pa-grade-c':'pa-grade-d';return `<span class="pa-conf-grade ${gc}">${g}</span>`;})()}
</div>
<div class="conf-bar"><div class="conf-fill ${confCls}" style="width:${confPct}%"></div></div>
</div>`;
const tfBlockHTML = `
${vetoBanner}
<div class="tf-row">${tfHTML}</div>
<div class="rule"></div>`;
const lvlGridHTML = `
<div class="lvl-grid">
${(()=>{
// Dollar P&L at each level using recommended leverage
const entry=c.close||0;
const recLev=+($('user-leverage')?.value||lev.user_leverage||lev.recommended||6);
const pos=sz.notional*recLev; // leveraged position size
const sign=dir==='long'?1:-1;
function pnl(price){ return price&&entry?round2(pos*(price-entry)/entry*sign):null; }
function fmt(v){ if(v==null) return ''; const s=v>=0?'+':''; const abs=Math.abs(v); const dec=abs<1?2:abs<10?1:0; return `<div class="lvl-pnl ${v>=0?'pos':'neg'}">${s}$${abs.toLocaleString('en',{minimumFractionDigits:dec,maximumFractionDigits:dec})}</div>`; }
function round2(v){ return Math.round(v*100)/100; }
const pSL=pnl(lv.sl), pTP1=pnl(lv.tp1), pTP2=pnl(lv.tp2), pTP3=pnl(lv.tp3);
return `
<div class="lvl entry"><div class="lvl-lbl">Entry</div><div class="lvl-val">${sf(entry)}</div><div class="lvl-pnl neutral">${recLev}× lev</div></div>
<div class="lvl sl"> <div class="lvl-lbl">Stop Loss</div><div class="lvl-val">${sf(lv.sl)}</div>${fmt(pSL)}</div>
${(()=>{ const rr=lv.rr1; const rrCls=rr==null?'':rr>=1.5?'rr-good':rr<1.0?'rr-bad':''; const rrWarn=rr!=null&&rr<1.0?`<div class="lvl-pnl loss" style="font-size:11px;font-weight:700;">⚠ Risk > Reward</div>`:''; return `<div class="lvl rr ${rrCls}"><div class="lvl-lbl">R / R</div><div class="lvl-val">${rr??'—'}×</div>${rrWarn}</div>`; })()}
<div class="lvl tp"> <div class="lvl-lbl">TP 1</div><div class="lvl-val">${sf(lv.tp1)}</div>${fmt(pTP1)}</div>
<div class="lvl tp"> <div class="lvl-lbl">TP 2</div><div class="lvl-val">${sf(lv.tp2)}</div>${fmt(pTP2)}</div>
<div class="lvl tp"> <div class="lvl-lbl">TP 3</div><div class="lvl-val">${sf(lv.tp3)}</div>${fmt(pTP3)}</div>`;
})()}
</div>`;
const szStripHTML = `
<div class="sz-strip">
<div class="sz-item"><div class="sz-lbl">Risk $</div><div class="sz-val">${fu(sz.risk_usd)}</div></div>
<div class="sz-item"><div class="sz-lbl">BingX qty</div><div class="sz-val">${sz.units??'—'}<span style="display:block;font-size:9px;color:var(--warn);font-weight:700;margin-top:1px">← enter on BingX</span></div></div>
<div class="sz-item"><div class="sz-lbl">Notional</div><div class="sz-val">${fu(sz.notional)}</div></div>
</div>`;
const narrHTML = `${narr?`<div class="narr">${narr}</div>`:''}<div class="rule"></div>`;
const evTitleHTML = `<div class="ev-title">Score Breakdown — ${c.pa_available ? '35/20/15/15/15' : '40/20/20/20'}</div>${evHTML}`;
const priceActionHTML = buildPriceActionSection(c);
const cardActsHTML = `
<div class="card-acts">
<a class="ca ca-chart" href="${tvUrl}" target="_blank" rel="noopener">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
Chart
</a>
<button class="ca ca-copy" data-plan="${plan.replace(/"/g,'&quot;').replace(/\n/g,'\\n')}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Copy Plan
</button>
<button class="ca ca-ref" data-sym="${sym}" data-acc="${account}" data-risk="${risk}" data-rank="${rank??''}" data-featured="${featured?'1':'0'}" title="Refresh this card">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
</div>`;
// ── Featured cards: two-column layout (ring/grade/state/score rail + main content).
// Non-featured cards: original single-column stacked layout, unchanged. ──
const bodyHTML = featured ? `
<div class="featured-layout">
<div class="featured-left">
${featuredRingWrapHTML}
<span class="tag-grade ${gradeCssCls}">${gradeTagLabel}</span>
${paGradeChipHTML}
${stateBadgesSimpleHTML}
<div class="panel">${evTitleHTML}</div>
</div>
<div class="featured-right">
${chHTML}
${tfBlockHTML}
${lvlGridHTML}
${szStripHTML}
${probHTML}
<div class="two-col">
<div>${levHTML}</div>
<div style="display:flex;flex-direction:column;gap:10px">${durHTML}${buildLiveAccPanel(sym)}</div>
</div>
${stateHTML}${btHTML}
${narrHTML}
${priceActionHTML}
${cardActsHTML}
</div>
</div>` : `
${chHTML}
${confBoxHTML}
${tfBlockHTML}
${lvlGridHTML}
${szStripHTML}
${probHTML}${levHTML}${durHTML}${stateHTML}${btHTML}${buildLiveAccPanel(sym)}
${narrHTML}
${evTitleHTML}
${priceActionHTML}
${cardActsHTML}`;
el.innerHTML=`${rowSummaryHTML}
<div class="card-detail"><div class="card-detail-inner">
${bodyHTML}
</div></div>`;
if (isRowMode) {
const rowHeader = el.querySelector('.row-summary');
if (rowHeader) rowHeader.addEventListener('click', function(){
el.classList.toggle('open');
if (el.classList.contains('open')) {
el.querySelectorAll('.ev-bar, .rs-mbar i').forEach(b => { if(b.dataset.w) b.style.width = b.dataset.w + '%'; });
}
});
}
el.querySelector('.ca-copy').addEventListener('click', function(){
const text=this.dataset.plan.replace(/\\n/g,'\n');
navigator.clipboard.writeText(text).then(()=>{
const orig=this.innerHTML;
this.innerHTML=`<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg> Copied!`;
this.style.color='var(--green)'; this.style.borderColor='rgba(61,220,151,0.3)';
this.style.transform='scale(1.04)';
setTimeout(()=>{ this.style.transform=''; }, 220);
showToast(base + ' trade plan copied to clipboard');
setTimeout(()=>{ this.innerHTML=orig; this.style.color=''; this.style.borderColor=''; },2200);
}).catch(()=>prompt('Copy:',text));
});
el.querySelector('.ca-ref').addEventListener('click', async function(){
const {sym:s,acc,risk:r,rank:rk,featured:ft}=this.dataset, orig=this.innerHTML;
this.disabled=true;
this.innerHTML=`<svg class="spin-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 12a9 9 0 1 1-2.12-9.36L21 3v6h-6"/></svg>`;
try {
const data=await fetch(`/api/card/${s}?account=${acc}&risk_pct=${r}`).then(x=>x.json());
const newEl=buildCard(data,+acc,+r,{rank: rk?+rk:null, featured: ft==='1'});
if (el.classList.contains('open')) newEl.classList.add('open');
el.replaceWith(newEl);
requestAnimationFrame(()=>{
setTimeout(()=>{ newEl.querySelectorAll('.ev-bar, .rs-mbar i').forEach(b=>{ if(b.dataset.w) b.style.width=b.dataset.w+'%'; }); },100);
});
if(data.symbol) liveCards.set(data.symbol,{el:newEl,account:+acc,risk:+r});
} catch { this.disabled=false; this.innerHTML=orig; }
});
return el;
}
/* ═══ Signal cards ═══ */
const MOVE_LABELS = {
breakout:'Breakout', acceleration:'Trend Accel.', reversal:'Reversal',
news:'News Spike', state_transition:'Regime Change'
};
// ── Signal type metadata ─────────────────────────────────────────────────────
const SIG_TYPE_META = {
rsi_cross: { label:'RSI Cross', icon:'<svg class="ic"><use href="#i-chart"/></svg>', color:'var(--teal)' },
volume_surge: { label:'Volume Surge', icon:'<svg class="ic"><use href="#i-trend"/></svg>', color:'var(--profit)' },
bb_squeeze: { label:'Squeeze Breakout', icon:'<svg class="ic"><use href="#i-scan"/></svg>', color:'var(--warn)' },
funding_extreme: { label:'Funding Extreme', icon:'<svg class="ic"><use href="#i-zap"/></svg>', color:'var(--violet)' },
price_surge: { label:'Price Surge', icon:'<svg class="ic"><use href="#i-rocket"/></svg>', color:'var(--profit)' },
};
function buildSignalCard(a) {
const sym = a.symbol || '';
const base = sym.replace(/-USDT$|-USD$|USDT$|USD$/,'');
const quote = (sym.endsWith('USDT') || sym.endsWith('-USDT')) ? 'USDT' : 'USD';
const dir = (a.direction || 'neutral').toLowerCase();
const urgency = a.urgency || 'medium';
const tvUrl = `https://www.tradingview.com/chart/?symbol=${encodeURIComponent(a.tv_symbol || sym)}`;
const meta = SIG_TYPE_META[a.signal_type] || { label: a.signal_type, icon:'<svg class="ic"><use href="#i-alert"/></svg>', color:'var(--t3)' };
const dirLabel = dir === 'long' ? '▲ LONG'
: dir === 'short' ? '▼ SHORT'
: '— WATCH';
const change24 = a.change_24h_pct != null
? `<span style="color:${a.change_24h_pct >= 0 ? 'var(--profit)' : 'var(--loss)'};font-size:11px;font-weight:600">${a.change_24h_pct >= 0 ? '+' : ''}${a.change_24h_pct.toFixed(2)}% 24h</span>`
: '';
const priceRow = a.close != null
? `<div style="font-size:11px;color:var(--t3);margin-top:4px">${sf(a.close)} ${change24}</div>`
: '';
const urgencyDot = urgency === 'critical' ? '<span style="color:var(--red)">●</span>'
: urgency === 'high' ? '<span style="color:var(--warn)">●</span>'
: '<span style="color:var(--amber)">●</span>';
const el = document.createElement('div');
el.className = `sig-card urgency-${urgency}`;
el.innerHTML = `
<div class="sig-header">
<div>
<div class="sig-sym">${base}<span class="q">${quote}</span></div>
${priceRow}
</div>
<span class="sig-dir ${dir}">${dirLabel}</span>
</div>
<div class="sig-types">
<span class="sig-type-badge" style="background:color-mix(in srgb, ${meta.color} 12%, transparent);color:${meta.color};border:1px solid color-mix(in srgb, ${meta.color} 30%, transparent)">${meta.icon} ${meta.label}</span>
<span style="font-size:10px;color:var(--t3);margin-left:6px">${urgencyDot} ${urgency === 'critical' ? 'Critical' : urgency === 'high' ? 'High priority' : 'Watch'}</span>
</div>
<div class="sig-reasons">
<div class="sig-reason ${dir === 'long' ? 'bull' : dir === 'short' ? 'bear' : ''}" style="font-size:11.5px;line-height:1.5">${a.description || ''}</div>
</div>
<a class="sig-cta" href="${tvUrl}" target="_blank" rel="noopener">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
Open on TradingView
</a>`;
return el;
}
// ── News ticker ──────────────────────────────────────────────────────────────
function renderNewsTicker(data) {
const ticker = $('news-ticker');
const trendRow = $('trending-coins');
if (!ticker || !data) return;
// Trending coins chips
if (trendRow && data.trending_coins && data.trending_coins.length) {
trendRow.innerHTML = data.trending_coins.map((c,i) =>
`<span class="news-chip" style="cursor:default" title="Trending #${i+1} on CoinGecko"><svg class="ic" style="color:var(--warn)"><use href="#i-flame"/></svg> ${c.symbol}</span>`
).join('');
}
// Headlines marquee
const headlines = (data.headlines || []).slice(0, 10);
if (!headlines.length) { ticker.style.display = 'none'; return; }
const items = headlines.map(h => {
const coins = h.coins_mentioned && h.coins_mentioned.length
? ` <span style="color:var(--teal);font-weight:700">[${h.coins_mentioned.slice(0,3).join(', ')}]</span>`
: '';
return `<a href="${h.link}" target="_blank" rel="noopener" style="color:var(--t2);text-decoration:none;white-space:nowrap;margin-right:48px">${h.title}${coins}</a>`;
}).join('<span style="color:var(--t3);margin-right:48px">·</span>');
ticker.querySelector('.news-ticker-inner').innerHTML = items + items; // duplicate for seamless loop
ticker.style.display = 'block';
}
async function loadNews() {
try {
const data = await fetch('/api/news').then(r => r.json());
renderNewsTicker(data);
} catch(e) {
// News is non-critical — fail silently
}
}
async function loadSignals() {
const loader = $('signals-loader');
const empty = $('signals-empty');
const grid = $('signals-grid');
const meta = $('signals-meta');
const btn = $('signals-refresh');
if (!grid) return;
grid.innerHTML = '';
if (empty) empty.style.display = 'none';
if (loader) loader.style.display = 'flex';
if (btn) btn.classList.add('spinning');
try {
const data = await fetch('/api/signals').then(r => r.json());
if (loader) loader.style.display = 'none';
if (btn) btn.classList.remove('spinning');
const alerts = data.alerts || [];
const ts = data.fetched_at ? new Date(data.fetched_at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) : '';
if (meta) meta.textContent = `${alerts.length} signal${alerts.length!==1?'s':''} · ${data.scanned||0} scanned · ${ts}`;
if (!alerts.length) {
if (empty) {
empty.style.display = 'flex';
const lbl = empty.querySelector('span:last-child');
if (lbl) lbl.textContent = 'No signals right now — market is calm. Will update automatically.';
}
return;
}
alerts.forEach((a, i) => {
const el = buildSignalCard(a);
el.style.animationDelay = (i * 60) + 'ms';
grid.appendChild(el);
});
} catch(e) {
if (loader) loader.style.display = 'none';
if (btn) btn.classList.remove('spinning');
if (meta) meta.textContent = 'Signals unavailable';
if (empty) {
empty.style.display = 'flex';
const lbl = empty.querySelector('span:last-child');
if (lbl) lbl.textContent = 'Signals failed to load — ' + e.message;
}
}
}
$('signals-refresh')?.addEventListener('click', () => { loadSignals(); loadNews(); });
/* ═══ Market Cycle Tracker (BTC / ETH / SOL) ═══ */
function switchCycleTab(coin) {
document.querySelectorAll('.cycle-tab').forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected','false');
});
document.querySelectorAll('.cycle-panel').forEach(p => p.style.display = 'none');
const activeTab = document.querySelector(`.cycle-tab[data-coin="${coin}"]`);
if (activeTab) { activeTab.classList.add('active'); activeTab.setAttribute('aria-selected','true'); }
const panel = document.getElementById(`cycle-panel-${coin}`);
if (panel) panel.style.display = 'block';
}
function getLivePrice(tickerId, fallback) {
const el = document.getElementById(tickerId);
if (el && el.textContent && el.textContent !== '—') {
const p = parseFloat(el.textContent.replace(/[$,]/g,''));
if (!isNaN(p) && p > 0) return p;
}
return fallback;
}
function setBarStyle(bar, pct) {
if (!bar) return;
if (pct >= 85) bar.style.background = 'linear-gradient(90deg,var(--red),var(--mint))';
else if (pct >= 60) bar.style.background = 'linear-gradient(90deg,var(--red),var(--amber))';
else bar.style.background = 'linear-gradient(90deg,var(--red),var(--amber))';
bar.style.width = pct + '%';
}
function initCycleTracker() {
const today = new Date();
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
/* ── BTC ── */
const BTC_ATH_DATE = new Date('2025-10-06');
const BTC_ATH_PRICE = 125836;
const BTC_AVG_BEAR = 414;
const btcEstBottom = new Date(BTC_ATH_DATE.getTime() + BTC_AVG_BEAR * 86400000);
const btcDaysIn = Math.floor((today - BTC_ATH_DATE) / 86400000);
const btcDaysLeft = Math.max(0, Math.floor((btcEstBottom - today) / 86400000));
const btcPct = Math.min(100, Math.round((btcDaysIn / BTC_AVG_BEAR) * 100));
const btcPrice = getLivePrice('t-btc', 59000);
const btcDrop = Math.round((1 - btcPrice / BTC_ATH_PRICE) * 100);
// If price is already above estimated bottom range, flag it
const btcBottomLow = Math.round(BTC_ATH_PRICE * 0.14); // −86%
const btcBottomHigh = Math.round(BTC_ATH_PRICE * 0.23); // −77%
const btcBottomEl = document.getElementById('btc-bottom-est');
if (btcBottomEl) {
if (btcPrice < btcBottomHigh) {
btcBottomEl.textContent = `$${Math.round(btcBottomLow/1000)}–${Math.round(btcBottomHigh/1000)}K`;
} else {
btcBottomEl.textContent = 'Above range';
btcBottomEl.style.color = 'var(--profit)';
btcBottomEl.closest('.cycle-stat').querySelector('.cycle-stat-sub').textContent = 'Price already above historical bottom estimate';
}
}
set('btc-days-in', btcDaysIn);
set('btc-drop', `−${btcDrop}%`);
set('btc-pct', `${btcPct}% of avg`);
set('btc-now-days', `${btcDaysIn} days in`);
set('btc-now-drop', `−${btcDrop}% so far`);
const btcCd = document.getElementById('btc-countdown');
if (btcCd) btcCd.textContent = btcDaysLeft <= 0 ? 'Past avg' : `~${btcDaysLeft} days`;
setBarStyle(document.getElementById('btc-bar'), btcPct);
/* ── ETH ── */
const ETH_ATH_DATE = new Date('2025-08-25');
const ETH_ATH_PRICE = 4953;
const ETH_AVG_BEAR = 365;
const ethEstBottom = new Date(ETH_ATH_DATE.getTime() + ETH_AVG_BEAR * 86400000);
// Use floor of ms diff — consistent with BTC calculation
const ethDaysIn = Math.floor((today - ETH_ATH_DATE) / 86400000);
const ethDaysLeft = Math.max(0, Math.floor((ethEstBottom - today) / 86400000));
const ethPct = Math.min(100, Math.round((ethDaysIn / ETH_AVG_BEAR) * 100));
const ethPrice = getLivePrice('t-eth', 1600);
const ethDrop = Math.round((1 - ethPrice / ETH_ATH_PRICE) * 100);
set('eth-days-in', ethDaysIn);
set('eth-drop', `−${ethDrop}%`);
set('eth-pct', `${ethPct}% of avg`);
set('eth-now-days', `${ethDaysIn} days in`);
set('eth-now-drop', `−${ethDrop}% so far`);
const ethCd = document.getElementById('eth-countdown');
if (ethCd) ethCd.textContent = ethDaysLeft <= 0 ? 'Past avg' : `~${ethDaysLeft} days`;
setBarStyle(document.getElementById('eth-bar'), ethPct);
/* ── SOL ── */
const SOL_ATH_DATE = new Date('2025-01-19');
const SOL_AVG_BEAR = 418; // only 1 cycle, FTX-distorted — used as reference only
const solDaysIn = Math.floor((today - SOL_ATH_DATE) / 86400000);
const solPct = Math.min(100, Math.round((solDaysIn / SOL_AVG_BEAR) * 100));
set('sol-days-in', solDaysIn);
set('sol-pct', `${solPct}% of prior`);
set('sol-now-days', `${solDaysIn} days in`);
const solCd = document.getElementById('sol-countdown');
if (solCd) solCd.textContent = `${solDaysIn} days`;
setBarStyle(document.getElementById('sol-bar'), solPct);
}
/* ═══ Init ═══ */
checkStatus();
checkFearGreed();
loadSignals();
loadNews();
updateTicker();
fetchLiveAccuracy(); // load live accuracy on startup
setTimeout(initCycleTracker, 2000); // init after ticker has had time to load BTC price
// Store interval IDs so we can pause/resume on tab visibility
const _intervals = {
status: setInterval(checkStatus, 30000),
fearGreed: setInterval(checkFearGreed, 3600000),
ticker: setInterval(updateTicker, 8000),
accuracy: setInterval(fetchLiveAccuracy, 300000),
news: setInterval(loadNews, 600000), // refresh news every 10 min
};
// Pause all background intervals when tab is hidden — saves CPU/battery
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
Object.values(_intervals).forEach(id => clearInterval(id));
stopLiveTicker();
clearInterval(countdownTimer); // pause auto-refresh countdown too
} else {
// Resume when tab becomes visible again
_intervals.status = setInterval(checkStatus, 30000);
_intervals.fearGreed = setInterval(checkFearGreed, 3600000);
_intervals.ticker = setInterval(updateTicker, 8000);
_intervals.accuracy = setInterval(fetchLiveAccuracy, 300000);
_intervals.news = setInterval(loadNews, 600000);
// Resume the auto-refresh countdown where it left off (armCountdownInterval
// clears before setting, so this can never create a doubled interval)
if (getAutoInterval() && autoCountdown > 0) armCountdownInterval();
// Immediately refresh key data
checkStatus();
updateTicker();
startLiveTicker();
}
});
if(getAutoInterval()) startCountdown();
/* ═══ Obsidian Glass additions — purely additive (theme, glare, reveals) ═══ */
(function(){
/* ── Theme toggle: localStorage 'tc_theme', default dark ── */
const root = document.documentElement, THEME_KEY = 'tc_theme';
// Theme already restored synchronously in <head> — prevents flash-of-wrong-theme on load
function applyTheme(next){
root.setAttribute('data-theme', next);
try { localStorage.setItem(THEME_KEY, next); } catch(e) {}
}
const tbtn = document.getElementById('theme-toggle');
if (tbtn) tbtn.addEventListener('click', function(e){
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
if (document.startViewTransition) {
const x = e.clientX || window.innerWidth - 60, y = e.clientY || 30;
const r = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
const vt = document.startViewTransition(() => applyTheme(next));
vt.ready.then(() => {
document.documentElement.animate(
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`] },
{ duration: 480, easing: 'cubic-bezier(.22,1,.36,1)', pseudoElement: '::view-transition-new(root)' }
);
}).catch(function(){});
} else {
applyTheme(next);
}
});
/* ── Cursor glare on large glass surfaces (.glass-lg), rAF-throttled ── */
let glareRaf = 0;
document.addEventListener('pointermove', function(e){
if (glareRaf) return;
const t = e.target, cx = e.clientX, cy = e.clientY;
glareRaf = requestAnimationFrame(function(){
glareRaf = 0;
if (!t || !t.closest) return;
const g = t.closest('.glass-lg');
if (!g || g.offsetHeight < 180) return; // skip short surfaces
const rc = g.getBoundingClientRect();
g.style.setProperty('--mx', ((cx - rc.left) / rc.width * 100) + '%');
g.style.setProperty('--my', ((cy - rc.top) / rc.height * 100) + '%');
});
}, { passive: true });
/* ── Scroll reveals: IntersectionObserver adds .in to .reveal ── */
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver(function(entries){
entries.forEach(function(en){
if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); }
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach(function(el){ io.observe(el); });
} else {
document.querySelectorAll('.reveal').forEach(function(el){ el.classList.add('in'); });
}
})();
/* ═══ Idle-life scheduler (ADHD-friendly engagement layer) ═══ */
(function(){
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let idleTimer=null;
const IDLE_MS = 45000;
function pulseIdleTargets() {
const targets = [
document.querySelector('.eyebrow-dot'),
document.getElementById('scan-btn'),
document.querySelector('.section-title .dot')
].filter(Boolean);
targets.forEach(function(t){
t.classList.add('idle-pulse');
setTimeout(function(){ t.classList.remove('idle-pulse'); }, 2600);
});
}
function resetIdle() {
clearTimeout(idleTimer);
idleTimer = setTimeout(pulseIdleTargets, IDLE_MS);
}
['mousemove','keydown','click','scroll','touchstart'].forEach(function(evt){
document.addEventListener(evt, resetIdle, { passive:true });
});
resetIdle();
})();
</script>
</body>
</html>