Utkarsh Pathak commited on
Commit
a95ccf0
·
1 Parent(s): 67c0def

Loader: morphing gradient blob, dancing bars, clean step pipeline, no emoji

Browse files
Files changed (1) hide show
  1. static/index.html +192 -156
static/index.html CHANGED
@@ -372,156 +372,186 @@ nav {
372
  ═══════════════════════════════════════════════ */
373
  #loader {
374
  display:none; flex-direction:column; align-items:center;
375
- padding:60px 24px 80px; gap:0;
376
  animation:slideUp 0.4s var(--ease);
377
  }
378
 
379
- /* Central radar / orb */
380
- .scan-radar {
381
- position:relative; width:180px; height:180px;
382
- margin-bottom:40px; flex-shrink:0;
 
383
  }
384
- .scan-radar-core {
385
- position:absolute; inset:30px;
386
- background:var(--glass);
387
- border:1px solid var(--glassbdr);
388
- border-radius:50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  display:grid; place-items:center;
390
- font-size:46px; line-height:1;
391
- backdrop-filter:blur(28px) saturate(180%);
392
- box-shadow:0 12px 40px rgba(0,0,0,0.1), inset 0 1px 0 white, 0 0 0 1px rgba(13,148,136,0.1);
393
- animation:radarBreathe 2.4s ease-in-out infinite;
394
- z-index:2;
395
- }
396
- @keyframes radarBreathe {
397
- 0%,100% { transform:scale(1); box-shadow:0 12px 40px rgba(0,0,0,0.1), inset 0 1px 0 white, 0 0 0 0 rgba(13,148,136,0.25); }
398
- 50% { transform:scale(1.06); box-shadow:0 20px 60px rgba(0,0,0,0.14), inset 0 1px 0 white, 0 0 0 16px rgba(13,148,136,0); }
399
- }
400
-
401
- /* Rings */
402
- .scan-ring {
403
- position:absolute; border-radius:50%;
404
- border:2px solid transparent; inset:0;
405
- }
406
- .scan-ring-1 {
407
- border-top-color:var(--teal); border-right-color:rgba(13,148,136,0.3);
408
- animation:spinRing 1.4s linear infinite;
409
- }
410
- .scan-ring-2 {
411
- inset:-14px;
412
- border-bottom-color:var(--blue2); border-left-color:rgba(59,130,246,0.25);
413
- animation:spinRing 2.2s linear infinite reverse;
414
- }
415
- .scan-ring-3 {
416
- inset:-30px;
417
- border:1.5px solid transparent;
418
- border-top-color:rgba(124,58,237,0.35); border-right-color:rgba(167,139,250,0.2);
419
- animation:spinRing 3.5s linear infinite;
420
- }
421
-
422
- /* Radar sweep */
423
- .scan-sweep {
424
- position:absolute; inset:0; border-radius:50%; overflow:hidden; z-index:1;
425
- }
426
- .scan-sweep::after {
427
- content:''; position:absolute; inset:0; border-radius:50%;
428
- background:conic-gradient(from 0deg, transparent 70%, rgba(13,148,136,0.18) 85%, rgba(13,148,136,0.08) 100%);
429
- animation:spinRing 2s linear infinite;
430
- }
431
-
432
- /* Ping dot */
433
- .scan-ping {
434
- position:absolute; top:50%; left:50%;
435
- width:8px; height:8px; border-radius:50%;
436
- background:var(--teal);
437
- transform:translate(-50%,-50%) translateX(52px);
438
- transform-origin:center;
439
- animation:pingOrbit 1.4s linear infinite;
440
- z-index:3;
441
- box-shadow:0 0 0 0 rgba(13,148,136,0.6);
442
- }
443
- .scan-ping::after {
444
- content:''; position:absolute; inset:-4px; border-radius:50%;
445
- background:rgba(13,148,136,0.25);
446
- animation:pingFlare 1.4s ease-out infinite;
447
- }
448
- @keyframes pingOrbit {
449
- from { transform:translate(-50%,-50%) rotate(0deg) translateX(52px); }
450
- to { transform:translate(-50%,-50%) rotate(360deg) translateX(52px); }
451
- }
452
- @keyframes pingFlare { 0%{transform:scale(1);opacity:1} 100%{transform:scale(3);opacity:0} }
453
- @keyframes spinRing { to{transform:rotate(360deg)} }
454
 
455
  /* ── Step pipeline ── */
456
  .scan-pipeline {
457
- width:100%; max-width:540px;
458
- display:flex; flex-direction:column; gap:10px;
459
- margin-bottom:32px;
 
460
  }
461
  .scan-step {
462
- display:flex; align-items:center; gap:14px;
463
- padding:14px 18px; border-radius:16px;
464
- background:rgba(255,255,255,0.55);
465
- border:1px solid rgba(255,255,255,0.85);
466
- backdrop-filter:blur(16px);
467
- box-shadow:0 2px 12px rgba(0,0,0,0.04), inset 0 1px 0 rgba(255,255,255,0.9);
468
- transition:all 0.4s var(--ease);
469
- opacity:0.4;
470
  }
471
  .scan-step.active {
472
- opacity:1; background:rgba(255,255,255,0.85);
473
- box-shadow:0 4px 24px rgba(13,148,136,0.12), 0 0 0 1px rgba(13,148,136,0.15), inset 0 1px 0 white;
474
- border-color:rgba(13,148,136,0.2);
475
- transform:translateX(4px);
476
  }
477
  .scan-step.done {
478
- opacity:0.75; background:rgba(209,250,229,0.4);
479
- border-color:rgba(5,150,105,0.2);
 
480
  }
481
- .step-icon {
482
- width:36px; height:36px; border-radius:10px; flex-shrink:0;
483
- display:grid; place-items:center; font-size:17px;
484
- background:rgba(255,255,255,0.8); border:1px solid rgba(0,0,0,0.06);
 
 
 
485
  transition:all 0.3s var(--ease);
486
  box-shadow:inset 0 1px 0 white;
487
  }
488
- .scan-step.active .step-icon {
489
- background:linear-gradient(135deg,var(--teal),#0f766e);
490
- border-color:transparent; font-size:0;
491
- box-shadow:0 4px 14px rgba(13,148,136,0.35), inset 0 1px 0 rgba(255,255,255,0.2);
 
492
  }
493
- .scan-step.active .step-icon::after {
494
- content:''; width:16px; height:16px;
495
- border:2px solid rgba(255,255,255,0.4);
 
496
  border-top-color:white;
497
- border-radius:50%; animation:spinRing 0.7s linear infinite;
498
  }
499
- .scan-step.done .step-icon {
500
- background:rgba(209,250,229,0.8); border-color:rgba(5,150,105,0.2);
501
- }
502
- .scan-step.done .step-icon::before { content:'✓'; font-size:16px; color:var(--green); }
503
- .step-body { flex:1; }
504
- .step-label { font-size:14px; font-weight:700; color:var(--t1); letter-spacing:-0.2px; }
505
- .step-sub { font-size:12px; color:var(--t3); margin-top:2px; font-weight:500; }
506
- .scan-step.active .step-sub { color:var(--teal); font-weight:600; }
507
- .step-time {
508
- font-size:11px; font-weight:700; color:var(--t4);
509
- font-variant-numeric:tabular-nums;
510
  }
 
 
 
 
 
511
  .scan-step.done .step-time { color:var(--green2); }
512
 
513
- /* ── Count-up ── */
514
  .scan-counter {
515
- display:flex; align-items:center; gap:10px;
516
- padding:12px 22px; border-radius:99px;
517
- background:rgba(255,255,255,0.75);
518
  border:1px solid rgba(255,255,255,0.95);
519
- backdrop-filter:blur(16px);
520
- box-shadow:0 2px 12px rgba(0,0,0,0.06), inset 0 1px 0 white;
 
521
  }
522
- .counter-num { font-size:22px; font-weight:900; letter-spacing:-1px; color:var(--t1); font-variant-numeric:tabular-nums; }
523
- .counter-lbl { font-size:12px; font-weight:600; color:var(--t3); }
524
- .counter-sep { width:1px; height:24px; background:rgba(0,0,0,0.08); }
 
 
525
 
526
  /* ═══════════════════════════════════════════════
527
  EMPTY STATE
@@ -899,67 +929,74 @@ nav {
899
  <div id="error-banner"></div>
900
 
901
  <div id="loader">
902
- <!-- Radar orb -->
903
- <div class="scan-radar">
904
- <div class="scan-sweep"></div>
905
- <div class="scan-ring scan-ring-3"></div>
906
- <div class="scan-ring scan-ring-2"></div>
907
- <div class="scan-ring scan-ring-1"></div>
908
- <div class="scan-radar-core">📡</div>
909
- <div class="scan-ping"></div>
 
 
 
 
 
 
910
  </div>
911
 
 
 
 
912
  <!-- Step pipeline -->
913
  <div class="scan-pipeline">
914
  <div class="scan-step" id="sd1">
915
- <div class="step-icon">🔍</div>
916
  <div class="step-body">
917
  <div class="step-label">Fetching universe</div>
918
- <div class="step-sub">Loading 300+ USDT pairs from BingX</div>
919
  </div>
920
  <div class="step-time" id="st1"></div>
921
  </div>
922
  <div class="scan-step" id="sd2">
923
- <div class="step-icon">📊</div>
924
  <div class="step-body">
925
  <div class="step-label">Computing indicators</div>
926
- <div class="step-sub">EMA · RSI · ATR · S/R levels</div>
927
  </div>
928
  <div class="step-time" id="st2"></div>
929
  </div>
930
  <div class="scan-step" id="sd3">
931
- <div class="step-icon">🔀</div>
932
  <div class="step-body">
933
- <div class="step-label">Cross-validating</div>
934
- <div class="step-sub">BingX funding + Binance OI check</div>
935
  </div>
936
  <div class="step-time" id="st3"></div>
937
  </div>
938
  <div class="scan-step" id="sd4">
939
- <div class="step-icon">🏆</div>
940
  <div class="step-body">
941
  <div class="step-label">Ranking setups</div>
942
- <div class="step-sub">40/20/20/20 scoring framework</div>
943
  </div>
944
  <div class="step-time" id="st4"></div>
945
  </div>
946
  </div>
947
 
948
- <!-- Live counter -->
949
  <div class="scan-counter">
950
- <div>
951
- <div class="counter-num" id="scan-count">0</div>
952
- <div class="counter-lbl">pairs analysed</div>
953
  </div>
954
- <div class="counter-sep"></div>
955
- <div>
956
- <div class="counter-num" id="scan-elapsed">0s</div>
957
- <div class="counter-lbl">elapsed</div>
958
  </div>
959
- <div class="counter-sep"></div>
960
- <div>
961
- <div class="counter-num">BingX</div>
962
- <div class="counter-lbl">data source</div>
963
  </div>
964
  </div>
965
  </div>
@@ -1057,10 +1094,10 @@ function startLoaderSteps() {
1057
  clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer);
1058
 
1059
  // Reset all steps
1060
- const icons=['🔍','📊','🔀','🏆'];
1061
  for(let i=1;i<=4;i++){
1062
  const el=$('sd'+i); if(!el) continue;
1063
- el.className='scan-step'; el.querySelector('.step-icon').textContent=icons[i-1];
 
1064
  const t=$('st'+i); if(t) t.textContent='';
1065
  }
1066
  const sc=$('scan-count'); if(sc) sc.textContent='0';
@@ -1098,13 +1135,12 @@ function activateStep(n) {
1098
  if(n>4) return;
1099
  const el=$('sd'+n); if(!el) return;
1100
  el.className='scan-step active';
1101
- el.querySelector('.step-icon').textContent='';
1102
  const t=$('st'+n); if(t) t.textContent='running…';
1103
  }
1104
  function markDone(n) {
1105
  const el=$('sd'+n); if(!el) return;
1106
  el.className='scan-step done';
1107
- const icon=el.querySelector('.step-icon'); if(icon) { icon.textContent=''; }
1108
  const elapsed=((Date.now()-scanStartTime)/1000).toFixed(1);
1109
  const t=$('st'+n); if(t) t.textContent=elapsed+'s';
1110
  }
 
372
  ═══════════════════════════════════════════════ */
373
  #loader {
374
  display:none; flex-direction:column; align-items:center;
375
+ padding:56px 24px 72px; gap:0;
376
  animation:slideUp 0.4s var(--ease);
377
  }
378
 
379
+ /* ── Morphing orb ── */
380
+ .scan-orb-wrap {
381
+ position:relative; width:160px; height:160px;
382
+ margin-bottom:36px;
383
+ display:grid; place-items:center;
384
  }
385
+
386
+ /* Outer glow halo */
387
+ .scan-orb-halo {
388
+ position:absolute; inset:-24px; border-radius:50%;
389
+ background:radial-gradient(circle, rgba(13,148,136,0.14) 0%, transparent 70%);
390
+ animation:haloPulse 2.5s ease-in-out infinite;
391
+ }
392
+ @keyframes haloPulse {
393
+ 0%,100% { transform:scale(1); opacity:0.7; }
394
+ 50% { transform:scale(1.18); opacity:1; }
395
+ }
396
+
397
+ /* The morphing blob */
398
+ .scan-blob {
399
+ position:absolute; inset:0;
400
+ background:linear-gradient(135deg, var(--teal) 0%, var(--blue2) 50%, var(--purple) 100%);
401
+ background-size:200% 200%;
402
+ border-radius:60% 40% 55% 45% / 50% 60% 40% 50%;
403
+ animation:blobMorph 4s ease-in-out infinite, gradShift 3s ease infinite;
404
+ filter:blur(1px);
405
+ opacity:0.9;
406
+ }
407
+ @keyframes blobMorph {
408
+ 0% { border-radius:60% 40% 55% 45% / 50% 60% 40% 50%; }
409
+ 25% { border-radius:50% 50% 40% 60% / 60% 40% 60% 40%; }
410
+ 50% { border-radius:40% 60% 60% 40% / 45% 55% 45% 55%; }
411
+ 75% { border-radius:55% 45% 45% 55% / 55% 45% 55% 45%; }
412
+ 100%{ border-radius:60% 40% 55% 45% / 50% 60% 40% 50%; }
413
+ }
414
+
415
+ /* Inner frosted disc */
416
+ .scan-orb-core {
417
+ position:relative; z-index:2;
418
+ width:80px; height:80px; border-radius:50%;
419
+ background:rgba(255,255,255,0.88);
420
+ backdrop-filter:blur(20px);
421
+ border:1px solid rgba(255,255,255,1);
422
+ box-shadow:0 8px 32px rgba(0,0,0,0.12), inset 0 1px 0 white;
423
  display:grid; place-items:center;
424
+ }
425
+
426
+ /* Custom SVG-style scan icon pure CSS bars */
427
+ .scan-icon-bars {
428
+ display:flex; align-items:flex-end; gap:3px; height:28px;
429
+ }
430
+ .sib {
431
+ width:4px; border-radius:3px;
432
+ background:linear-gradient(180deg, var(--teal), var(--blue2));
433
+ }
434
+ .sib:nth-child(1) { height:12px; animation:barDance 1.2s ease-in-out 0.0s infinite alternate; }
435
+ .sib:nth-child(2) { height:22px; animation:barDance 1.2s ease-in-out 0.15s infinite alternate; }
436
+ .sib:nth-child(3) { height:28px; animation:barDance 1.2s ease-in-out 0.3s infinite alternate; }
437
+ .sib:nth-child(4) { height:18px; animation:barDance 1.2s ease-in-out 0.45s infinite alternate; }
438
+ .sib:nth-child(5) { height:10px; animation:barDance 1.2s ease-in-out 0.6s infinite alternate; }
439
+ @keyframes barDance {
440
+ from { transform:scaleY(0.35); opacity:0.5; }
441
+ to { transform:scaleY(1); opacity:1; }
442
+ }
443
+
444
+ /* Orbiting dot */
445
+ .scan-orbit {
446
+ position:absolute; inset:0; animation:spinRing 2.2s linear infinite; z-index:3;
447
+ }
448
+ .scan-orbit-dot {
449
+ position:absolute; top:4px; left:50%; transform:translateX(-50%);
450
+ width:10px; height:10px; border-radius:50%;
451
+ background:white;
452
+ box-shadow:0 0 0 2px rgba(13,148,136,0.4), 0 2px 8px rgba(13,148,136,0.6);
453
+ }
454
+ .scan-orbit-dot::after {
455
+ content:''; position:absolute; inset:-3px; border-radius:50%;
456
+ background:rgba(255,255,255,0.4);
457
+ animation:orbitPing 2.2s ease-out infinite;
458
+ }
459
+ @keyframes orbitPing {
460
+ 0% { transform:scale(1); opacity:0.8; }
461
+ 100% { transform:scale(2.5); opacity:0; }
462
+ }
463
+ @keyframes spinRing { to{transform:rotate(360deg)} }
464
+
465
+ /* ── Headline ── */
466
+ .scan-headline {
467
+ font-size:26px; font-weight:900; letter-spacing:-1px; color:var(--t1);
468
+ text-align:center; margin-bottom:6px;
469
+ animation:slideUp 0.5s var(--ease) 0.1s both;
470
+ }
471
+ .scan-subline {
472
+ font-size:13px; color:var(--t3); font-weight:500; text-align:center;
473
+ margin-bottom:32px;
474
+ animation:slideUp 0.5s var(--ease) 0.2s both;
475
+ }
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  /* ── Step pipeline ── */
478
  .scan-pipeline {
479
+ width:100%; max-width:520px;
480
+ display:flex; flex-direction:column; gap:8px;
481
+ margin-bottom:28px;
482
+ animation:slideUp 0.5s var(--ease) 0.3s both;
483
  }
484
  .scan-step {
485
+ display:flex; align-items:center; gap:12px;
486
+ padding:12px 16px; border-radius:14px;
487
+ background:rgba(255,255,255,0.5);
488
+ border:1px solid rgba(255,255,255,0.8);
489
+ backdrop-filter:blur(12px);
490
+ box-shadow:inset 0 1px 0 rgba(255,255,255,0.8);
491
+ transition:all 0.45s var(--ease);
492
+ opacity:0.38;
493
  }
494
  .scan-step.active {
495
+ opacity:1; background:rgba(255,255,255,0.9);
496
+ border-color:rgba(13,148,136,0.22);
497
+ box-shadow:0 4px 20px rgba(13,148,136,0.1), inset 0 1px 0 white;
498
+ transform:scale(1.015) translateX(3px);
499
  }
500
  .scan-step.done {
501
+ opacity:0.7; background:rgba(220,252,231,0.45);
502
+ border-color:rgba(5,150,105,0.18);
503
+ transform:none;
504
  }
505
+
506
+ /* Step number badge */
507
+ .step-num {
508
+ width:30px; height:30px; border-radius:9px; flex-shrink:0;
509
+ display:grid; place-items:center;
510
+ font-size:12px; font-weight:800; color:var(--t3);
511
+ background:rgba(255,255,255,0.7); border:1px solid rgba(0,0,0,0.06);
512
  transition:all 0.3s var(--ease);
513
  box-shadow:inset 0 1px 0 white;
514
  }
515
+ .scan-step.active .step-num {
516
+ background:linear-gradient(135deg, var(--teal), #0891b2);
517
+ color:transparent; border-color:transparent;
518
+ box-shadow:0 4px 12px rgba(13,148,136,0.4);
519
+ position:relative;
520
  }
521
+ /* Spinner inside active num */
522
+ .scan-step.active .step-num::after {
523
+ content:''; position:absolute; inset:5px;
524
+ border:2px solid rgba(255,255,255,0.35);
525
  border-top-color:white;
526
+ border-radius:50%; animation:spinRing 0.65s linear infinite;
527
  }
528
+ .scan-step.done .step-num {
529
+ background:rgba(209,250,229,0.9); color:var(--green);
530
+ border-color:rgba(5,150,105,0.2);
531
+ font-size:15px;
 
 
 
 
 
 
 
532
  }
533
+ .step-body { flex:1; min-width:0; }
534
+ .step-label { font-size:13px; font-weight:700; color:var(--t1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
535
+ .step-sub { font-size:11px; color:var(--t3); margin-top:1px; font-weight:500; }
536
+ .scan-step.active .step-sub { color:var(--teal); }
537
+ .step-time { font-size:11px; font-weight:700; color:var(--t4); font-variant-numeric:tabular-nums; flex-shrink:0; }
538
  .scan-step.done .step-time { color:var(--green2); }
539
 
540
+ /* ── Live counters ── */
541
  .scan-counter {
542
+ display:flex; align-items:stretch; gap:0;
543
+ border-radius:18px; overflow:hidden;
544
+ background:rgba(255,255,255,0.78);
545
  border:1px solid rgba(255,255,255,0.95);
546
+ backdrop-filter:blur(20px);
547
+ box-shadow:0 4px 20px rgba(0,0,0,0.07), inset 0 1px 0 white;
548
+ animation:slideUp 0.5s var(--ease) 0.4s both;
549
  }
550
+ .sc-item { padding:14px 24px; text-align:center; }
551
+ .sc-item + .sc-item { border-left:1px solid rgba(0,0,0,0.06); }
552
+ .sc-num { font-size:24px; font-weight:900; letter-spacing:-1px; color:var(--t1); line-height:1; font-variant-numeric:tabular-nums; }
553
+ .sc-lbl { font-size:10px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; color:var(--t3); margin-top:4px; }
554
+ .sc-item.accent .sc-num { color:var(--teal); }
555
 
556
  /* ═══════════════════════════════════════════════
557
  EMPTY STATE
 
929
  <div id="error-banner"></div>
930
 
931
  <div id="loader">
932
+ <!-- Morphing orb -->
933
+ <div class="scan-orb-wrap">
934
+ <div class="scan-orb-halo"></div>
935
+ <div class="scan-blob"></div>
936
+ <div class="scan-orb-core">
937
+ <div class="scan-icon-bars">
938
+ <div class="sib"></div>
939
+ <div class="sib"></div>
940
+ <div class="sib"></div>
941
+ <div class="sib"></div>
942
+ <div class="sib"></div>
943
+ </div>
944
+ </div>
945
+ <div class="scan-orbit"><div class="scan-orbit-dot"></div></div>
946
  </div>
947
 
948
+ <div class="scan-headline">Scanning the market</div>
949
+ <div class="scan-subline">Analysing 300+ USDT pairs · BingX + Binance data fusion</div>
950
+
951
  <!-- Step pipeline -->
952
  <div class="scan-pipeline">
953
  <div class="scan-step" id="sd1">
954
+ <div class="step-num">1</div>
955
  <div class="step-body">
956
  <div class="step-label">Fetching universe</div>
957
+ <div class="step-sub">300+ USDT perpetual pairs from BingX</div>
958
  </div>
959
  <div class="step-time" id="st1"></div>
960
  </div>
961
  <div class="scan-step" id="sd2">
962
+ <div class="step-num">2</div>
963
  <div class="step-body">
964
  <div class="step-label">Computing indicators</div>
965
+ <div class="step-sub">EMA · RSI · ATR · Support/Resistance</div>
966
  </div>
967
  <div class="step-time" id="st2"></div>
968
  </div>
969
  <div class="scan-step" id="sd3">
970
+ <div class="step-num">3</div>
971
  <div class="step-body">
972
+ <div class="step-label">Cross-validating sources</div>
973
+ <div class="step-sub">BingX funding rate + Binance OI check</div>
974
  </div>
975
  <div class="step-time" id="st3"></div>
976
  </div>
977
  <div class="scan-step" id="sd4">
978
+ <div class="step-num">4</div>
979
  <div class="step-body">
980
  <div class="step-label">Ranking setups</div>
981
+ <div class="step-sub">40/20/20/20 scoring · sorting by confidence</div>
982
  </div>
983
  <div class="step-time" id="st4"></div>
984
  </div>
985
  </div>
986
 
987
+ <!-- Live counters -->
988
  <div class="scan-counter">
989
+ <div class="sc-item accent">
990
+ <div class="sc-num" id="scan-count">0</div>
991
+ <div class="sc-lbl">pairs scanned</div>
992
  </div>
993
+ <div class="sc-item">
994
+ <div class="sc-num" id="scan-elapsed">0s</div>
995
+ <div class="sc-lbl">elapsed</div>
 
996
  </div>
997
+ <div class="sc-item">
998
+ <div class="sc-num">BingX</div>
999
+ <div class="sc-lbl">source</div>
 
1000
  </div>
1001
  </div>
1002
  </div>
 
1094
  clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer);
1095
 
1096
  // Reset all steps
 
1097
  for(let i=1;i<=4;i++){
1098
  const el=$('sd'+i); if(!el) continue;
1099
+ el.className='scan-step';
1100
+ const num=el.querySelector('.step-num'); if(num) num.textContent=i;
1101
  const t=$('st'+i); if(t) t.textContent='';
1102
  }
1103
  const sc=$('scan-count'); if(sc) sc.textContent='0';
 
1135
  if(n>4) return;
1136
  const el=$('sd'+n); if(!el) return;
1137
  el.className='scan-step active';
 
1138
  const t=$('st'+n); if(t) t.textContent='running…';
1139
  }
1140
  function markDone(n) {
1141
  const el=$('sd'+n); if(!el) return;
1142
  el.className='scan-step done';
1143
+ const num=el.querySelector('.step-num'); if(num) num.textContent='';
1144
  const elapsed=((Date.now()-scanStartTime)/1000).toFixed(1);
1145
  const t=$('st'+n); if(t) t.textContent=elapsed+'s';
1146
  }