Deepfake Authenticator commited on
Commit
459d5a1
·
1 Parent(s): 91da8ab

feat: React + Three.js UI with 3D reactive orb

Browse files

- Vite + React + TypeScript frontend
- Three.js / React Three Fiber: animated 3D orb that changes color/behavior
based on analysis state (idle=blue, loading=fast spin, FAKE=red, REAL=green)
- Framer Motion: spring animations, AnimatePresence transitions, shimmer button
- Zustand state management
- Animated confidence arc (SVG semicircle gauge)
- Frame timeline with per-bar Framer Motion animations
- Agent pipeline cards with sequential activation
- Audio analysis card with Wav2Vec2 + heuristic scores
- Sticky 3D orb panel on desktop, stacks on mobile
- Vite proxy for dev, FastAPI serves built dist in production

backend/main.py CHANGED
@@ -135,23 +135,43 @@ async def analyze_video(file: UploadFile = File(...)):
135
 
136
 
137
  # ── Serve frontend ────────────────────────────
138
- frontend_path = Path(__file__).parent.parent / "frontend-vanilla"
 
 
139
 
140
- if frontend_path.exists():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  @app.get("/script.js")
142
  async def serve_script():
143
- from fastapi.responses import FileResponse
144
  return FileResponse(
145
- str(frontend_path / "script.js"),
146
  media_type="application/javascript",
147
  headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
148
  )
149
 
150
  @app.get("/")
151
  async def serve_index():
152
- from fastapi.responses import FileResponse
153
  return FileResponse(
154
- str(frontend_path / "index.html"),
155
  headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
156
  )
157
 
 
135
 
136
 
137
  # ── Serve frontend ────────────────────────────
138
+ # Prefer built React dist, fall back to vanilla HTML
139
+ _react_dist = Path(__file__).parent.parent / "frontend-dist"
140
+ _vanilla_dir = Path(__file__).parent.parent / "frontend-vanilla"
141
 
142
+ if _react_dist.exists():
143
+ # Serve React SPA
144
+ app.mount("/assets", StaticFiles(directory=str(_react_dist / "assets")), name="assets")
145
+
146
+ @app.get("/")
147
+ async def serve_index():
148
+ return FileResponse(
149
+ str(_react_dist / "index.html"),
150
+ headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
151
+ )
152
+
153
+ # Catch-all for React Router (SPA fallback)
154
+ @app.get("/{full_path:path}")
155
+ async def spa_fallback(full_path: str):
156
+ index = _react_dist / "index.html"
157
+ if index.exists():
158
+ return FileResponse(str(index))
159
+ return {"detail": "Not found"}
160
+
161
+ elif _vanilla_dir.exists():
162
+ # Fallback: vanilla HTML
163
  @app.get("/script.js")
164
  async def serve_script():
 
165
  return FileResponse(
166
+ str(_vanilla_dir / "script.js"),
167
  media_type="application/javascript",
168
  headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
169
  )
170
 
171
  @app.get("/")
172
  async def serve_index():
 
173
  return FileResponse(
174
+ str(_vanilla_dir / "index.html"),
175
  headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
176
  )
177
 
frontend/index.html CHANGED
@@ -1,413 +1,14 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
6
- <title>Deepfake Authenticator</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"/>
9
- <style>
10
- :root{--g:#00ff88;--r:#ff3355;--w:#ffaa00;--bg:#050508;--card:rgba(255,255,255,0.04);--border:rgba(0,255,136,0.18);--muted:#5a6a7a;}
11
- *{box-sizing:border-box;margin:0;padding:0;}
12
- body{background:var(--bg);font-family:'Space Grotesk',sans-serif;color:#e2e8f0;min-height:100vh;overflow-x:hidden;}
13
- code,pre,.mono{font-family:'JetBrains Mono',monospace;}
14
- ::-webkit-scrollbar{width:5px;} ::-webkit-scrollbar-track{background:var(--bg);} ::-webkit-scrollbar-thumb{background:rgba(0,255,136,0.25);border-radius:4px;}
15
- .hidden{display:none!important;}
16
- </style>
17
- </head>
18
- <body>
19
-
20
- <style>
21
- /* ── Particle Canvas ── */
22
- #particles{position:fixed;inset:0;z-index:0;pointer-events:none;}
23
- /* ── Grid overlay ── */
24
- .grid-bg{position:fixed;inset:0;z-index:0;pointer-events:none;
25
- background-image:linear-gradient(rgba(0,255,136,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,255,136,0.03) 1px,transparent 1px);
26
- background-size:60px 60px;}
27
- /* ── Glass ── */
28
- .glass{background:var(--card);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.5);}
29
- .glass-sm{background:rgba(255,255,255,0.03);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.07);border-radius:12px;}
30
- /* ── Glow borders ── */
31
- .glow-green{box-shadow:0 0 20px rgba(0,255,136,0.25),inset 0 0 20px rgba(0,255,136,0.04);}
32
- .glow-red{box-shadow:0 0 20px rgba(255,51,85,0.25),inset 0 0 20px rgba(255,51,85,0.04);}
33
- /* ── Header ── */
34
- header{position:sticky;top:0;z-index:50;background:rgba(5,5,8,0.85);backdrop-filter:blur(24px);border-bottom:1px solid rgba(0,255,136,0.1);}
35
- /* ── Eye SVG animation ── */
36
- @keyframes scanPulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:0.5;transform:scale(1.15);}}
37
- @keyframes eyeScan{0%{stroke-dashoffset:100;}100%{stroke-dashoffset:0;}}
38
- .eye-scan{animation:scanPulse 2.5s ease-in-out infinite;}
39
- /* ── Status dot ── */
40
- @keyframes statusBlink{0%,100%{opacity:1;box-shadow:0 0 6px var(--g);}50%{opacity:0.4;box-shadow:none;}}
41
- .status-dot{width:8px;height:8px;border-radius:50%;background:var(--g);animation:statusBlink 2s ease-in-out infinite;}
42
- /* ── Drop zone border animation ── */
43
- @keyframes borderDash{to{stroke-dashoffset:-20;}}
44
- #dropZone{border:2px dashed rgba(0,255,136,0.25);border-radius:16px;cursor:pointer;min-height:200px;display:flex;align-items:center;justify-content:center;transition:all 0.35s ease;position:relative;overflow:hidden;}
45
- #dropZone:hover,#dropZone.drag-over{border-color:var(--g);background:rgba(0,255,136,0.04);box-shadow:0 0 40px rgba(0,255,136,0.2),inset 0 0 30px rgba(0,255,136,0.06);transform:scale(1.01);}
46
- /* ── Orbit ring ── */
47
- @keyframes orbitRing{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
48
- .orbit{animation:orbitRing 3s linear infinite;transform-origin:center;}
49
- /* ── Analyze button ── */
50
- #analyzeBtn{width:100%;padding:16px;border-radius:12px;font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:14px;letter-spacing:0.12em;text-transform:uppercase;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--muted);cursor:not-allowed;transition:all 0.3s ease;position:relative;overflow:hidden;}
51
- #analyzeBtn.active{background:linear-gradient(135deg,#00ff88,#00cc66);color:#050508;border-color:transparent;cursor:pointer;box-shadow:0 0 30px rgba(0,255,136,0.4);}
52
- #analyzeBtn.active:hover{transform:translateY(-2px) scale(1.01);box-shadow:0 0 50px rgba(0,255,136,0.6);}
53
- #analyzeBtn.active::after{content:'';position:absolute;top:0;left:-100%;width:60%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);animation:shimmer 2s ease infinite;}
54
- @keyframes shimmer{to{left:150%;}}
55
- #analyzeBtn:disabled{opacity:0.45;cursor:not-allowed;transform:none!important;}
56
- </style>
57
-
58
- <style>
59
- /* ── Loading ── */
60
- @keyframes radarSpin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
61
- @keyframes radarPulse{0%,100%{opacity:0.8;transform:scale(1);}50%{opacity:0.3;transform:scale(1.05);}}
62
- .radar-ring{border-radius:50%;border:1px solid rgba(0,255,136,0.2);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}
63
- .radar-sweep{position:absolute;top:50%;left:50%;width:50%;height:2px;transform-origin:left center;background:linear-gradient(90deg,transparent,var(--g));animation:radarSpin 2s linear infinite;}
64
- /* ── Agent cards ── */
65
- .agent-card{transition:all 0.4s ease;border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:12px 16px;background:rgba(255,255,255,0.02);}
66
- .agent-card.active{border-color:var(--g);background:rgba(0,255,136,0.06);box-shadow:0 0 20px rgba(0,255,136,0.15);}
67
- .agent-card.active .agent-dot{background:var(--g);box-shadow:0 0 8px var(--g);}
68
- .agent-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.15);transition:all 0.4s ease;flex-shrink:0;}
69
- /* ── Progress bar ── */
70
- .prog-track{height:4px;background:rgba(255,255,255,0.06);border-radius:99px;overflow:hidden;}
71
- .prog-fill{height:100%;border-radius:99px;background:linear-gradient(90deg,#00aaff,var(--g));box-shadow:0 0 12px var(--g);transition:width 0.5s ease;}
72
- /* ── Verdict ── */
73
- @keyframes glitch{0%,100%{text-shadow:0 0 10px var(--r);}20%{text-shadow:-2px 0 #ff0,2px 0 #0ff,0 0 20px var(--r);}40%{text-shadow:2px 0 #ff0,-2px 0 #0ff,0 0 10px var(--r);}60%{text-shadow:0 0 30px var(--r);}}
74
- .glitch-text{animation:glitch 3s ease-in-out infinite;}
75
- @keyframes fadeUp{from{opacity:0;transform:translateY(24px);}to{opacity:1;transform:none;}}
76
- .fade-up{animation:fadeUp 0.6s cubic-bezier(0.16,1,0.3,1) both;}
77
- @keyframes slideInLeft{from{opacity:0;transform:translateX(-20px);}to{opacity:1;transform:none;}}
78
- /* ── Verdict badge ── */
79
- .verdict-badge{width:110px;height:110px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:2.8rem;transition:all 0.5s;border:2px solid var(--border);}
80
- @keyframes pulseFake{0%,100%{box-shadow:0 0 20px rgba(255,51,85,0.3);}50%{box-shadow:0 0 50px rgba(255,51,85,0.7);}}
81
- @keyframes pulseReal{0%,100%{box-shadow:0 0 20px rgba(0,255,136,0.3);}50%{box-shadow:0 0 50px rgba(0,255,136,0.7);}}
82
- .verdict-badge.fake{border-color:var(--r);animation:pulseFake 2s infinite;}
83
- .verdict-badge.real{border-color:var(--g);animation:pulseReal 2s infinite;}
84
- /* ── Confidence arc (CSS semicircle gauge) ── */
85
- .gauge-wrap{position:relative;width:200px;height:110px;overflow:hidden;margin:0 auto;}
86
- .gauge-bg{width:200px;height:200px;border-radius:50%;border:12px solid rgba(255,255,255,0.06);position:absolute;top:0;left:0;clip-path:inset(0 0 50% 0);}
87
- .gauge-fill{width:200px;height:200px;border-radius:50%;border:12px solid transparent;position:absolute;top:0;left:0;clip-path:inset(0 0 50% 0);transition:transform 1.2s cubic-bezier(0.22,1,0.36,1);}
88
- .gauge-fill.fake{border-color:var(--r);box-shadow:0 0 20px rgba(255,51,85,0.5);}
89
- .gauge-fill.real{border-color:var(--g);box-shadow:0 0 20px rgba(0,255,136,0.5);}
90
- /* ── Conf bar ── */
91
- .conf-track{height:10px;background:rgba(255,255,255,0.05);border-radius:99px;overflow:hidden;border:1px solid rgba(255,255,255,0.05);}
92
- .conf-fill{height:100%;border-radius:99px;transition:width 1.2s cubic-bezier(0.22,1,0.36,1);}
93
- .conf-fill.fake{background:linear-gradient(90deg,#880022,var(--r));box-shadow:0 0 15px rgba(255,51,85,0.6);}
94
- .conf-fill.real{background:linear-gradient(90deg,#00aaff,var(--g));box-shadow:0 0 15px rgba(0,255,136,0.6);}
95
- /* ── Risk needle ── */
96
- #riskNeedle{display:inline-block;padding:4px 14px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:0.15em;border:1px solid;transition:all 0.5s;}
97
- /* ── Insight bullets ── */
98
- .insight-item{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.05);font-size:13px;line-height:1.6;color:#a0aec0;animation:slideInLeft 0.4s ease both;}
99
- .insight-item:hover{background:rgba(255,255,255,0.04);}
100
- .insight-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;margin-top:6px;}
101
- /* ── Timeline bars ── */
102
- .bar-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;min-width:0;position:relative;}
103
- .bar-outer{width:100%;background:rgba(255,255,255,0.05);border-radius:4px 4px 0 0;overflow:hidden;position:relative;}
104
- .bar-inner{width:100%;border-radius:4px 4px 0 0;transition:height 0.8s cubic-bezier(0.22,1,0.36,1);}
105
- .bar-wrap:hover .bar-tooltip{opacity:1;transform:translateY(-4px);}
106
- .bar-tooltip{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:rgba(10,10,20,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:4px 8px;font-size:10px;white-space:nowrap;pointer-events:none;opacity:0;transition:all 0.2s ease;z-index:10;font-family:'JetBrains Mono',monospace;}
107
- /* ── Meta grid ── */
108
- .meta-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);}
109
- .meta-row:last-child{border-bottom:none;}
110
- /* ── Error ── */
111
- #errorSection{border-color:rgba(255,51,85,0.3)!important;background:rgba(255,51,85,0.04)!important;}
112
- /* ── Stat pills ── */
113
- .stat-pill{display:inline-flex;align-items:center;gap:8px;padding:8px 18px;border-radius:99px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);font-size:12px;font-weight:600;letter-spacing:0.05em;color:#a0aec0;}
114
- .stat-pill .pip{width:6px;height:6px;border-radius:50%;background:var(--g);box-shadow:0 0 6px var(--g);}
115
- /* ── Section label ── */
116
- .sec-label{font-size:10px;font-weight:700;letter-spacing:0.2em;text-transform:uppercase;color:#00aaff;display:flex;align-items:center;gap:8px;margin-bottom:14px;}
117
- .sec-label::before{content:'';display:inline-block;width:3px;height:14px;background:#00aaff;border-radius:2px;box-shadow:0 0 8px #00aaff;}
118
- /* ── Model badge ── */
119
- #modelBadge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:99px;background:rgba(0,255,136,0.08);border:1px solid rgba(0,255,136,0.25);font-size:11px;font-weight:600;letter-spacing:0.1em;color:var(--g);}
120
- </style>
121
-
122
-
123
- <!-- Particle canvas + grid -->
124
- <canvas id="particles"></canvas>
125
- <div class="grid-bg"></div>
126
-
127
- <!-- ══ HEADER ══ -->
128
- <header>
129
- <div style="max-width:1100px;margin:0 auto;padding:0 24px;height:64px;display:flex;align-items:center;justify-content:space-between;">
130
- <div style="display:flex;align-items:center;gap:14px;">
131
- <!-- Animated eye logo -->
132
- <div class="eye-scan" style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;">
133
- <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
134
- <ellipse cx="18" cy="18" rx="14" ry="9" stroke="#00ff88" stroke-width="1.5" opacity="0.9"/>
135
- <circle cx="18" cy="18" r="5" fill="#00ff88" opacity="0.9"/>
136
- <circle cx="18" cy="18" r="2.5" fill="#050508"/>
137
- <path d="M4 18 Q18 4 32 18" stroke="#00ff88" stroke-width="0.8" stroke-dasharray="4 3" opacity="0.4">
138
- <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="8s" repeatCount="indefinite"/>
139
- </path>
140
- </svg>
141
- </div>
142
- <div>
143
- <div style="font-size:13px;font-weight:700;letter-spacing:0.18em;color:#fff;">DEEPFAKE AUTHENTICATOR</div>
144
- <div style="font-size:9px;letter-spacing:0.25em;color:var(--muted);font-family:'JetBrains Mono',monospace;">AI-POWERED VIDEO FORENSICS</div>
145
- </div>
146
- </div>
147
- <div id="modelBadge">
148
- <div class="status-dot"></div>
149
- <span>ViT ENSEMBLE</span>
150
- </div>
151
- </div>
152
- </header>
153
-
154
- <!-- ══ MAIN ══ -->
155
- <div style="position:relative;z-index:1;max-width:860px;margin:0 auto;padding:48px 24px 80px;">
156
-
157
- <!-- Hero -->
158
- <div class="fade-up" style="text-align:center;margin-bottom:48px;animation-delay:0.1s;">
159
- <h1 style="font-size:clamp(2rem,5vw,3.5rem);font-weight:700;line-height:1.1;margin-bottom:16px;background:linear-gradient(135deg,#fff 30%,var(--g));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">Is This Video Real?</h1>
160
- <p style="color:var(--muted);font-size:15px;max-width:480px;margin:0 auto 28px;line-height:1.7;">Advanced AI ensemble detects facial manipulation with 99%+ accuracy using dual Vision Transformer models.</p>
161
- <div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:center;">
162
- <div class="stat-pill"><span class="pip"></span>2 ViT Models</div>
163
- <div class="stat-pill"><span class="pip"></span>40 Frame Analysis</div>
164
- <div class="stat-pill"><span class="pip"></span>&lt; 15s Detection</div>
165
- </div>
166
- </div>
167
-
168
- <!-- ══ UPLOAD SECTION ══ -->
169
- <section id="uploadSection" class="glass fade-up" style="padding:32px;margin-bottom:24px;animation-delay:0.2s;">
170
- <div id="dropZone">
171
- <!-- Upload prompt -->
172
- <div id="uploadPrompt" style="text-align:center;padding:40px 24px;">
173
- <div style="position:relative;width:80px;height:80px;margin:0 auto 20px;">
174
- <div style="position:absolute;inset:0;border-radius:50%;border:1px dashed rgba(0,255,136,0.3);" class="orbit"></div>
175
- <div style="position:absolute;inset:8px;border-radius:50%;border:1px solid rgba(0,255,136,0.15);"></div>
176
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
177
- <svg width="32" height="32" fill="none" stroke="#00ff88" stroke-width="1.5" viewBox="0 0 24 24" opacity="0.8">
178
- <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/>
179
- </svg>
180
- </div>
181
- </div>
182
- <h3 style="font-size:17px;font-weight:600;color:#fff;margin-bottom:8px;">Drop Video for Forensic Analysis</h3>
183
- <p style="font-size:13px;color:var(--muted);">Drag &amp; drop or click to browse</p>
184
- <p style="font-size:11px;color:var(--muted);margin-top:12px;opacity:0.6;font-family:'JetBrains Mono',monospace;">MP4 · AVI · MOV · MKV · WebM — Max 100MB</p>
185
- </div>
186
- <!-- File chosen state -->
187
- <div id="fileChosen" class="hidden" style="width:100%;padding:28px 32px;display:flex;align-items:center;gap:20px;">
188
- <div style="width:56px;height:56px;border-radius:12px;background:rgba(0,255,136,0.1);border:1px solid rgba(0,255,136,0.3);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 0 20px rgba(0,255,136,0.15);">
189
- <svg width="28" height="28" fill="none" stroke="#00ff88" stroke-width="1.5" viewBox="0 0 24 24">
190
- <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"/>
191
- </svg>
192
- </div>
193
- <div style="flex:1;min-width:0;">
194
- <p id="chosenName" style="font-size:14px;font-weight:600;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
195
- <p id="chosenSize" style="font-size:12px;color:var(--muted);margin-top:4px;font-family:'JetBrains Mono',monospace;"></p>
196
- </div>
197
- <button id="clearBtn" style="padding:8px;color:var(--muted);background:none;border:none;cursor:pointer;border-radius:8px;transition:all 0.2s;" onmouseover="this.style.color='#ff3355';this.style.background='rgba(255,51,85,0.1)'" onmouseout="this.style.color='var(--muted)';this.style.background='none'">
198
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
199
- </button>
200
- </div>
201
- </div>
202
- <input type="file" id="fileInput" accept="video/*" style="display:none;"/>
203
- <div style="margin-top:20px;">
204
- <button id="analyzeBtn" disabled>
205
- <span id="analyzeBtnText">Analyze Video</span>
206
- </button>
207
- </div>
208
- </section>
209
-
210
- <!-- ══ LOADING SECTION ══ -->
211
- <section id="loadingSection" class="hidden glass fade-up" style="padding:48px 32px;text-align:center;margin-bottom:24px;">
212
- <!-- Radar -->
213
- <div style="position:relative;width:140px;height:140px;margin:0 auto 36px;">
214
- <div class="radar-ring" style="width:140px;height:140px;"></div>
215
- <div class="radar-ring" style="width:100px;height:100px;"></div>
216
- <div class="radar-ring" style="width:60px;height:60px;"></div>
217
- <div class="radar-sweep" style="width:70px;top:50%;left:50%;transform-origin:left center;"></div>
218
- <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:12px;height:12px;border-radius:50%;background:var(--g);box-shadow:0 0 15px var(--g);"></div>
219
- </div>
220
- <!-- Status -->
221
- <p id="loadingStatus" style="font-size:11px;font-weight:700;letter-spacing:0.2em;color:#00aaff;text-transform:uppercase;margin-bottom:20px;font-family:'JetBrains Mono',monospace;">Initializing sequence...</p>
222
- <!-- Progress -->
223
- <div style="max-width:400px;margin:0 auto 32px;">
224
- <div class="prog-track">
225
- <div id="progressBar" style="width:0%;" class="prog-fill"></div>
226
- </div>
227
- </div>
228
- <!-- Agent cards -->
229
- <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;max-width:500px;margin:0 auto;">
230
- <div id="ag0" class="agent-card" style="display:flex;align-items:center;gap:10px;">
231
- <div class="agent-dot"></div>
232
- <div style="text-align:left;">
233
- <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Frame Extractor</div>
234
- <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Sampling keyframes</div>
235
- </div>
236
- </div>
237
- <div id="ag1" class="agent-card" style="display:flex;align-items:center;gap:10px;">
238
- <div class="agent-dot"></div>
239
- <div style="text-align:left;">
240
- <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Face Detector</div>
241
- <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Isolating regions</div>
242
- </div>
243
- </div>
244
- <div id="ag2" class="agent-card" style="display:flex;align-items:center;gap:10px;">
245
- <div class="agent-dot"></div>
246
- <div style="text-align:left;">
247
- <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">ViT Ensemble</div>
248
- <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Neural inference</div>
249
- </div>
250
- </div>
251
- <div id="ag3" class="agent-card" style="display:flex;align-items:center;gap:10px;">
252
- <div class="agent-dot"></div>
253
- <div style="text-align:left;">
254
- <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Report Builder</div>
255
- <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Compiling results</div>
256
- </div>
257
- </div>
258
- </div>
259
- </section>
260
-
261
- <!-- ══ RESULT SECTION ══ -->
262
- <section id="resultSection" class="hidden fade-up" style="display:flex;flex-direction:column;gap:20px;">
263
-
264
- <!-- Verdict Card -->
265
- <div id="verdictCard" class="glass" style="padding:40px;display:flex;flex-wrap:wrap;align-items:center;gap:36px;">
266
- <!-- Badge + label -->
267
- <div style="display:flex;flex-direction:column;align-items:center;flex-shrink:0;">
268
- <div id="verdictBadge" class="verdict-badge" style="margin-bottom:14px;background:rgba(5,5,8,0.8);">
269
- <span id="verdictEmoji" style="font-size:2.5rem;"></span>
270
- </div>
271
- <div id="verdictLabel" style="font-size:28px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;"></div>
272
- <div style="font-size:9px;color:var(--muted);letter-spacing:0.25em;margin-top:4px;font-family:'JetBrains Mono',monospace;">VERDICT</div>
273
- </div>
274
- <!-- Confidence + risk -->
275
- <div style="flex:1;min-width:220px;">
276
- <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
277
- <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;">Confidence Score</span>
278
- <span id="confValue" style="font-size:36px;font-weight:700;font-family:'JetBrains Mono',monospace;"></span>
279
- </div>
280
- <div class="conf-track" style="margin-bottom:20px;">
281
- <div id="confBar" class="conf-fill" style="width:0%;"></div>
282
- </div>
283
- <div class="glass-sm" style="padding:14px 18px;display:flex;align-items:center;justify-content:space-between;">
284
- <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;">Risk Assessment</span>
285
- <span id="riskNeedle"></span>
286
- </div>
287
- <div style="margin-top:12px;text-align:right;">
288
- <span id="riskLabel" style="font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace;"></span>
289
- </div>
290
- </div>
291
- </div>
292
-
293
- <!-- Insights + Meta -->
294
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
295
- <div class="glass" style="padding:24px;">
296
- <div class="sec-label">Analysis Insights</div>
297
- <div id="detailsList" style="display:flex;flex-direction:column;gap:10px;"></div>
298
- </div>
299
- <div class="glass" style="padding:24px;display:flex;flex-direction:column;justify-content:space-between;">
300
- <div>
301
- <div class="sec-label">Video Metadata</div>
302
- <div id="metaGrid"></div>
303
- </div>
304
- <div style="margin-top:16px;padding-top:14px;border-top:1px solid rgba(255,255,255,0.05);display:flex;align-items:center;gap:8px;">
305
- <div style="width:6px;height:6px;border-radius:50%;background:var(--g);box-shadow:0 0 6px var(--g);"></div>
306
- <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">Models: ViT Ensemble</span>
307
- </div>
308
- </div>
309
- </div>
310
-
311
- <!-- Frame Timeline -->
312
- <div class="glass" style="padding:24px;">
313
- <div class="sec-label">Frame Analysis Timeline</div>
314
- <div id="timelineChart" style="display:flex;align-items:flex-end;gap:3px;height:80px;position:relative;padding-bottom:20px;"></div>
315
- <div style="display:flex;justify-content:space-between;margin-top:8px;">
316
- <span style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Frame 0</span>
317
- <span style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Frame 40</span>
318
- </div>
319
- </div>
320
-
321
- <!-- ══ AUDIO ANALYSIS CARD ══ -->
322
- <div id="audioSection" class="hidden glass fade-up" style="padding:28px;">
323
- <div class="sec-label" style="margin-bottom:18px;">
324
- <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink:0;"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/></svg>
325
- VOICE AUTHENTICITY ANALYSIS
326
- </div>
327
- <div style="display:flex;flex-wrap:wrap;align-items:center;gap:20px;margin-bottom:20px;">
328
- <div id="audioBadge" style="padding:8px 18px;border-radius:99px;font-size:12px;font-weight:700;letter-spacing:0.1em;border:1px solid;transition:all .4s;"></div>
329
- <div style="flex:1;min-width:160px;">
330
- <div class="conf-track">
331
- <div id="audioBar" class="conf-fill" style="width:0%;transition:width 1.2s cubic-bezier(0.22,1,0.36,1);"></div>
332
- </div>
333
- </div>
334
- </div>
335
- <div style="display:flex;gap:16px;margin-bottom:20px;flex-wrap:wrap;">
336
- <div class="glass-sm" style="padding:12px 18px;flex:1;min-width:120px;text-align:center;">
337
- <div style="font-size:10px;color:var(--muted);letter-spacing:.1em;margin-bottom:4px;">WAV2VEC2 MODEL</div>
338
- <div id="audioModelScore" style="font-size:22px;font-weight:700;font-family:'JetBrains Mono',monospace;color:#fff;"></div>
339
- </div>
340
- <div class="glass-sm" style="padding:12px 18px;flex:1;min-width:120px;text-align:center;">
341
- <div style="font-size:10px;color:var(--muted);letter-spacing:.1em;margin-bottom:4px;">SIGNAL HEURISTICS</div>
342
- <div id="audioHeurScore" style="font-size:22px;font-weight:700;font-family:'JetBrains Mono',monospace;color:#fff;"></div>
343
- </div>
344
- </div>
345
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
346
- <div>
347
- <div style="font-size:10px;color:var(--muted);letter-spacing:.12em;margin-bottom:10px;">VOICE INSIGHTS</div>
348
- <div id="audioDetailsList" style="display:flex;flex-direction:column;gap:8px;"></div>
349
- </div>
350
- <div>
351
- <div style="font-size:10px;color:var(--muted);letter-spacing:.12em;margin-bottom:10px;">SIGNAL FEATURES</div>
352
- <div id="audioFeatGrid"></div>
353
- </div>
354
- </div>
355
- </div>
356
-
357
- <!-- Reset -->
358
- <div style="text-align:center;margin-top:8px;">
359
- <button onclick="resetAll()" style="padding:12px 28px;font-size:11px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);background:none;border:1px solid rgba(255,255,255,0.1);border-radius:10px;cursor:pointer;transition:all 0.3s;font-family:'Space Grotesk',sans-serif;" onmouseover="this.style.color='var(--g)';this.style.borderColor='rgba(0,255,136,0.4)';this.style.background='rgba(0,255,136,0.05)'" onmouseout="this.style.color='var(--muted)';this.style.borderColor='rgba(255,255,255,0.1)';this.style.background='none'">
360
- ↻ &nbsp;Analyze Another Video
361
- </button>
362
- </div>
363
- </section>
364
-
365
- <!-- ══ ERROR SECTION ══ -->
366
- <section id="errorSection" class="hidden glass fade-up" style="padding:28px 32px;margin-bottom:24px;">
367
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
368
- <svg width="22" height="22" fill="none" stroke="#ff3355" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
369
- <span style="font-size:12px;font-weight:700;letter-spacing:0.2em;color:#ff3355;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">Analysis Failed</span>
370
- </div>
371
- <p id="errorMsg" style="font-size:13px;color:var(--muted);margin-left:34px;line-height:1.6;"></p>
372
- <div style="margin-left:34px;margin-top:16px;">
373
- <button onclick="resetAll()" style="font-size:11px;color:rgba(255,255,255,0.4);background:none;border:none;cursor:pointer;letter-spacing:0.15em;text-transform:uppercase;font-family:'Space Grotesk',sans-serif;transition:color 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,0.4)'">↻ Try Again</button>
374
- </div>
375
- </section>
376
-
377
- </div><!-- /main -->
378
-
379
- <footer style="position:relative;z-index:1;text-align:center;padding:24px;font-size:10px;color:rgba(90,106,122,0.5);letter-spacing:0.2em;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">
380
- Deepfake Authenticator &nbsp;·&nbsp; Secure Local Inference &nbsp;·&nbsp; ViT Ensemble v2
381
- </footer>
382
-
383
- <script>
384
- // ── Particle background ──────────────────────────────────────────────────────
385
- (function(){
386
- const c=document.getElementById('particles');
387
- const ctx=c.getContext('2d');
388
- let W,H,pts=[];
389
- function resize(){W=c.width=window.innerWidth;H=c.height=window.innerHeight;}
390
- resize();window.addEventListener('resize',resize);
391
- for(let i=0;i<60;i++)pts.push({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-0.5)*0.3,vy:(Math.random()-0.5)*0.3,r:Math.random()*1.5+0.5,a:Math.random()});
392
- function draw(){
393
- ctx.clearRect(0,0,W,H);
394
- pts.forEach(p=>{
395
- p.x+=p.vx;p.y+=p.vy;
396
- if(p.x<0||p.x>W)p.vx*=-1;
397
- if(p.y<0||p.y>H)p.vy*=-1;
398
- ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
399
- ctx.fillStyle=`rgba(0,255,136,${0.15+p.a*0.2})`;ctx.fill();
400
- });
401
- // draw connections
402
- for(let i=0;i<pts.length;i++)for(let j=i+1;j<pts.length;j++){
403
- const dx=pts[i].x-pts[j].x,dy=pts[i].y-pts[j].y,d=Math.sqrt(dx*dx+dy*dy);
404
- if(d<120){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[j].x,pts[j].y);ctx.strokeStyle=`rgba(0,255,136,${0.06*(1-d/120)})`;ctx.lineWidth=0.5;ctx.stroke();}
405
- }
406
- requestAnimationFrame(draw);
407
- }
408
- draw();
409
- })();
410
- </script>
411
- <script src="script.js?v=3"></script>
412
- </body>
413
- </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Deepfake Authenticator</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json CHANGED
@@ -8,13 +8,18 @@
8
  "name": "frontend",
9
  "version": "0.0.0",
10
  "dependencies": {
 
 
11
  "@tailwindcss/vite": "^4.2.4",
12
  "autoprefixer": "^10.5.0",
 
13
  "postcss": "^8.5.10",
14
  "react": "^19.2.5",
15
  "react-dom": "^19.2.5",
16
  "styled-components": "^6.4.1",
17
- "tailwindcss": "^4.2.4"
 
 
18
  },
19
  "devDependencies": {
20
  "@eslint/js": "^10.0.1",
@@ -220,6 +225,15 @@
220
  "node": ">=6.0.0"
221
  }
222
  },
 
 
 
 
 
 
 
 
 
223
  "node_modules/@babel/template": {
224
  "version": "7.28.6",
225
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -268,6 +282,12 @@
268
  "node": ">=6.9.0"
269
  }
270
  },
 
 
 
 
 
 
271
  "node_modules/@emnapi/core": {
272
  "version": "1.10.0",
273
  "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -553,6 +573,24 @@
553
  "@jridgewell/sourcemap-codec": "^1.4.14"
554
  }
555
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  "node_modules/@napi-rs/wasm-runtime": {
557
  "version": "1.1.4",
558
  "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -575,11 +613,100 @@
575
  "version": "0.127.0",
576
  "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
577
  "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
 
578
  "license": "MIT",
579
  "funding": {
580
  "url": "https://github.com/sponsors/Boshen"
581
  }
582
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  "node_modules/@rolldown/binding-android-arm64": {
584
  "version": "1.0.0-rc.17",
585
  "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
@@ -587,6 +714,7 @@
587
  "cpu": [
588
  "arm64"
589
  ],
 
590
  "license": "MIT",
591
  "optional": true,
592
  "os": [
@@ -603,6 +731,7 @@
603
  "cpu": [
604
  "arm64"
605
  ],
 
606
  "license": "MIT",
607
  "optional": true,
608
  "os": [
@@ -619,6 +748,7 @@
619
  "cpu": [
620
  "x64"
621
  ],
 
622
  "license": "MIT",
623
  "optional": true,
624
  "os": [
@@ -635,6 +765,7 @@
635
  "cpu": [
636
  "x64"
637
  ],
 
638
  "license": "MIT",
639
  "optional": true,
640
  "os": [
@@ -651,6 +782,7 @@
651
  "cpu": [
652
  "arm"
653
  ],
 
654
  "license": "MIT",
655
  "optional": true,
656
  "os": [
@@ -667,6 +799,7 @@
667
  "cpu": [
668
  "arm64"
669
  ],
 
670
  "license": "MIT",
671
  "optional": true,
672
  "os": [
@@ -683,6 +816,7 @@
683
  "cpu": [
684
  "arm64"
685
  ],
 
686
  "license": "MIT",
687
  "optional": true,
688
  "os": [
@@ -699,6 +833,7 @@
699
  "cpu": [
700
  "ppc64"
701
  ],
 
702
  "license": "MIT",
703
  "optional": true,
704
  "os": [
@@ -715,6 +850,7 @@
715
  "cpu": [
716
  "s390x"
717
  ],
 
718
  "license": "MIT",
719
  "optional": true,
720
  "os": [
@@ -731,6 +867,7 @@
731
  "cpu": [
732
  "x64"
733
  ],
 
734
  "license": "MIT",
735
  "optional": true,
736
  "os": [
@@ -747,6 +884,7 @@
747
  "cpu": [
748
  "x64"
749
  ],
 
750
  "license": "MIT",
751
  "optional": true,
752
  "os": [
@@ -763,6 +901,7 @@
763
  "cpu": [
764
  "arm64"
765
  ],
 
766
  "license": "MIT",
767
  "optional": true,
768
  "os": [
@@ -779,6 +918,7 @@
779
  "cpu": [
780
  "wasm32"
781
  ],
 
782
  "license": "MIT",
783
  "optional": true,
784
  "dependencies": {
@@ -797,6 +937,7 @@
797
  "cpu": [
798
  "arm64"
799
  ],
 
800
  "license": "MIT",
801
  "optional": true,
802
  "os": [
@@ -813,6 +954,7 @@
813
  "cpu": [
814
  "x64"
815
  ],
 
816
  "license": "MIT",
817
  "optional": true,
818
  "os": [
@@ -1086,6 +1228,12 @@
1086
  "vite": "^5.2.0 || ^6 || ^7 || ^8"
1087
  }
1088
  },
 
 
 
 
 
 
1089
  "node_modules/@tybys/wasm-util": {
1090
  "version": "0.10.1",
1091
  "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1096,6 +1244,12 @@
1096
  "tslib": "^2.4.0"
1097
  }
1098
  },
 
 
 
 
 
 
1099
  "node_modules/@types/esrecurse": {
1100
  "version": "4.3.1",
1101
  "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -1117,6 +1271,12 @@
1117
  "dev": true,
1118
  "license": "MIT"
1119
  },
 
 
 
 
 
 
1120
  "node_modules/@types/react": {
1121
  "version": "19.2.14",
1122
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -1137,6 +1297,59 @@
1137
  "@types/react": "^19.2.0"
1138
  }
1139
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1140
  "node_modules/@vitejs/plugin-react": {
1141
  "version": "6.0.1",
1142
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
@@ -1249,6 +1462,26 @@
1249
  "node": "18 || 20 || >=22"
1250
  }
1251
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  "node_modules/baseline-browser-mapping": {
1253
  "version": "2.10.21",
1254
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
@@ -1261,6 +1494,15 @@
1261
  "node": ">=6.0.0"
1262
  }
1263
  },
 
 
 
 
 
 
 
 
 
1264
  "node_modules/brace-expansion": {
1265
  "version": "5.0.5",
1266
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
@@ -1307,6 +1549,30 @@
1307
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1308
  }
1309
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1310
  "node_modules/camelize": {
1311
  "version": "1.0.1",
1312
  "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
@@ -1316,6 +1582,19 @@
1316
  "url": "https://github.com/sponsors/ljharb"
1317
  }
1318
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1319
  "node_modules/caniuse-lite": {
1320
  "version": "1.0.30001790",
1321
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
@@ -1343,11 +1622,28 @@
1343
  "dev": true,
1344
  "license": "MIT"
1345
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1346
  "node_modules/cross-spawn": {
1347
  "version": "7.0.6",
1348
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1349
  "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1350
- "dev": true,
1351
  "license": "MIT",
1352
  "dependencies": {
1353
  "path-key": "^3.1.0",
@@ -1409,6 +1705,15 @@
1409
  "dev": true,
1410
  "license": "MIT"
1411
  },
 
 
 
 
 
 
 
 
 
1412
  "node_modules/detect-libc": {
1413
  "version": "2.1.2",
1414
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1418,6 +1723,12 @@
1418
  "node": ">=8"
1419
  }
1420
  },
 
 
 
 
 
 
1421
  "node_modules/electron-to-chromium": {
1422
  "version": "1.5.344",
1423
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
@@ -1666,6 +1977,7 @@
1666
  "version": "6.5.0",
1667
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1668
  "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
 
1669
  "license": "MIT",
1670
  "engines": {
1671
  "node": ">=12.0.0"
@@ -1679,6 +1991,12 @@
1679
  }
1680
  }
1681
  },
 
 
 
 
 
 
1682
  "node_modules/file-entry-cache": {
1683
  "version": "8.0.0",
1684
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -1743,10 +2061,38 @@
1743
  "url": "https://github.com/sponsors/rawify"
1744
  }
1745
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1746
  "node_modules/fsevents": {
1747
  "version": "2.3.3",
1748
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1749
  "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 
1750
  "hasInstallScript": true,
1751
  "license": "MIT",
1752
  "optional": true,
@@ -1793,6 +2139,12 @@
1793
  "url": "https://github.com/sponsors/sindresorhus"
1794
  }
1795
  },
 
 
 
 
 
 
1796
  "node_modules/graceful-fs": {
1797
  "version": "4.2.11",
1798
  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1816,6 +2168,32 @@
1816
  "hermes-estree": "0.25.1"
1817
  }
1818
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1819
  "node_modules/ignore": {
1820
  "version": "5.3.2",
1821
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1826,6 +2204,12 @@
1826
  "node": ">= 4"
1827
  }
1828
  },
 
 
 
 
 
 
1829
  "node_modules/imurmurhash": {
1830
  "version": "0.1.4",
1831
  "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -1859,13 +2243,30 @@
1859
  "node": ">=0.10.0"
1860
  }
1861
  },
 
 
 
 
 
 
1862
  "node_modules/isexe": {
1863
  "version": "2.0.0",
1864
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1865
  "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1866
- "dev": true,
1867
  "license": "ISC"
1868
  },
 
 
 
 
 
 
 
 
 
 
 
 
1869
  "node_modules/jiti": {
1870
  "version": "2.6.1",
1871
  "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -1953,6 +2354,15 @@
1953
  "node": ">= 0.8.0"
1954
  }
1955
  },
 
 
 
 
 
 
 
 
 
1956
  "node_modules/lightningcss": {
1957
  "version": "1.32.0",
1958
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -2228,6 +2638,16 @@
2228
  "yallist": "^3.0.2"
2229
  }
2230
  },
 
 
 
 
 
 
 
 
 
 
2231
  "node_modules/magic-string": {
2232
  "version": "0.30.21",
2233
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2237,6 +2657,21 @@
2237
  "@jridgewell/sourcemap-codec": "^1.5.5"
2238
  }
2239
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2240
  "node_modules/minimatch": {
2241
  "version": "10.2.5",
2242
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -2253,6 +2688,21 @@
2253
  "url": "https://github.com/sponsors/isaacs"
2254
  }
2255
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2256
  "node_modules/ms": {
2257
  "version": "2.1.3",
2258
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2355,7 +2805,6 @@
2355
  "version": "3.1.1",
2356
  "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2357
  "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2358
- "dev": true,
2359
  "license": "MIT",
2360
  "engines": {
2361
  "node": ">=8"
@@ -2371,6 +2820,7 @@
2371
  "version": "4.0.4",
2372
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2373
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
 
2374
  "license": "MIT",
2375
  "engines": {
2376
  "node": ">=12"
@@ -2413,6 +2863,12 @@
2413
  "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2414
  "license": "MIT"
2415
  },
 
 
 
 
 
 
2416
  "node_modules/prelude-ls": {
2417
  "version": "1.2.1",
2418
  "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2423,6 +2879,16 @@
2423
  "node": ">= 0.8.0"
2424
  }
2425
  },
 
 
 
 
 
 
 
 
 
 
2426
  "node_modules/punycode": {
2427
  "version": "2.3.1",
2428
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2454,10 +2920,35 @@
2454
  "react": "^19.2.5"
2455
  }
2456
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2457
  "node_modules/rolldown": {
2458
  "version": "1.0.0-rc.17",
2459
  "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
2460
  "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
 
2461
  "license": "MIT",
2462
  "dependencies": {
2463
  "@oxc-project/types": "=0.127.0",
@@ -2491,6 +2982,7 @@
2491
  "version": "1.0.0-rc.17",
2492
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
2493
  "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
 
2494
  "license": "MIT"
2495
  },
2496
  "node_modules/scheduler": {
@@ -2513,7 +3005,6 @@
2513
  "version": "2.0.0",
2514
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2515
  "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2516
- "dev": true,
2517
  "license": "MIT",
2518
  "dependencies": {
2519
  "shebang-regex": "^3.0.0"
@@ -2526,7 +3017,6 @@
2526
  "version": "3.0.0",
2527
  "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2528
  "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2529
- "dev": true,
2530
  "license": "MIT",
2531
  "engines": {
2532
  "node": ">=8"
@@ -2541,6 +3031,32 @@
2541
  "node": ">=0.10.0"
2542
  }
2543
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2544
  "node_modules/styled-components": {
2545
  "version": "6.4.1",
2546
  "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.1.tgz",
@@ -2583,6 +3099,15 @@
2583
  "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
2584
  "license": "MIT"
2585
  },
 
 
 
 
 
 
 
 
 
2586
  "node_modules/tailwindcss": {
2587
  "version": "4.2.4",
2588
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
@@ -2602,10 +3127,49 @@
2602
  "url": "https://opencollective.com/webpack"
2603
  }
2604
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2605
  "node_modules/tinyglobby": {
2606
  "version": "0.2.16",
2607
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2608
  "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
 
2609
  "license": "MIT",
2610
  "dependencies": {
2611
  "fdir": "^6.5.0",
@@ -2618,12 +3182,78 @@
2618
  "url": "https://github.com/sponsors/SuperchupuDev"
2619
  }
2620
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2621
  "node_modules/tslib": {
2622
  "version": "2.8.1",
2623
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2624
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2625
- "license": "0BSD",
2626
- "optional": true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2627
  },
2628
  "node_modules/type-check": {
2629
  "version": "0.4.0",
@@ -2678,10 +3308,29 @@
2678
  "punycode": "^2.1.0"
2679
  }
2680
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2681
  "node_modules/vite": {
2682
  "version": "8.0.10",
2683
  "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
2684
  "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
 
2685
  "license": "MIT",
2686
  "dependencies": {
2687
  "lightningcss": "^1.32.0",
@@ -2755,11 +3404,21 @@
2755
  }
2756
  }
2757
  },
 
 
 
 
 
 
 
 
 
 
 
2758
  "node_modules/which": {
2759
  "version": "2.0.2",
2760
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2761
  "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2762
- "dev": true,
2763
  "license": "ISC",
2764
  "dependencies": {
2765
  "isexe": "^2.0.0"
@@ -2823,6 +3482,35 @@
2823
  "peerDependencies": {
2824
  "zod": "^3.25.0 || ^4.0.0"
2825
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2826
  }
2827
  }
2828
  }
 
8
  "name": "frontend",
9
  "version": "0.0.0",
10
  "dependencies": {
11
+ "@react-three/drei": "^10.7.7",
12
+ "@react-three/fiber": "^9.6.0",
13
  "@tailwindcss/vite": "^4.2.4",
14
  "autoprefixer": "^10.5.0",
15
+ "framer-motion": "^12.38.0",
16
  "postcss": "^8.5.10",
17
  "react": "^19.2.5",
18
  "react-dom": "^19.2.5",
19
  "styled-components": "^6.4.1",
20
+ "tailwindcss": "^4.2.4",
21
+ "three": "^0.184.0",
22
+ "zustand": "^5.0.12"
23
  },
24
  "devDependencies": {
25
  "@eslint/js": "^10.0.1",
 
225
  "node": ">=6.0.0"
226
  }
227
  },
228
+ "node_modules/@babel/runtime": {
229
+ "version": "7.29.2",
230
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
231
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
232
+ "license": "MIT",
233
+ "engines": {
234
+ "node": ">=6.9.0"
235
+ }
236
+ },
237
  "node_modules/@babel/template": {
238
  "version": "7.28.6",
239
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
 
282
  "node": ">=6.9.0"
283
  }
284
  },
285
+ "node_modules/@dimforge/rapier3d-compat": {
286
+ "version": "0.12.0",
287
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
288
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
289
+ "license": "Apache-2.0"
290
+ },
291
  "node_modules/@emnapi/core": {
292
  "version": "1.10.0",
293
  "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
 
573
  "@jridgewell/sourcemap-codec": "^1.4.14"
574
  }
575
  },
576
+ "node_modules/@mediapipe/tasks-vision": {
577
+ "version": "0.10.17",
578
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
579
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
580
+ "license": "Apache-2.0"
581
+ },
582
+ "node_modules/@monogrid/gainmap-js": {
583
+ "version": "3.4.0",
584
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
585
+ "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
586
+ "license": "MIT",
587
+ "dependencies": {
588
+ "promise-worker-transferable": "^1.0.4"
589
+ },
590
+ "peerDependencies": {
591
+ "three": ">= 0.159.0"
592
+ }
593
+ },
594
  "node_modules/@napi-rs/wasm-runtime": {
595
  "version": "1.1.4",
596
  "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
 
613
  "version": "0.127.0",
614
  "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
615
  "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
616
+ "dev": true,
617
  "license": "MIT",
618
  "funding": {
619
  "url": "https://github.com/sponsors/Boshen"
620
  }
621
  },
622
+ "node_modules/@react-three/drei": {
623
+ "version": "10.7.7",
624
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
625
+ "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
626
+ "license": "MIT",
627
+ "dependencies": {
628
+ "@babel/runtime": "^7.26.0",
629
+ "@mediapipe/tasks-vision": "0.10.17",
630
+ "@monogrid/gainmap-js": "^3.0.6",
631
+ "@use-gesture/react": "^10.3.1",
632
+ "camera-controls": "^3.1.0",
633
+ "cross-env": "^7.0.3",
634
+ "detect-gpu": "^5.0.56",
635
+ "glsl-noise": "^0.0.0",
636
+ "hls.js": "^1.5.17",
637
+ "maath": "^0.10.8",
638
+ "meshline": "^3.3.1",
639
+ "stats-gl": "^2.2.8",
640
+ "stats.js": "^0.17.0",
641
+ "suspend-react": "^0.1.3",
642
+ "three-mesh-bvh": "^0.8.3",
643
+ "three-stdlib": "^2.35.6",
644
+ "troika-three-text": "^0.52.4",
645
+ "tunnel-rat": "^0.1.2",
646
+ "use-sync-external-store": "^1.4.0",
647
+ "utility-types": "^3.11.0",
648
+ "zustand": "^5.0.1"
649
+ },
650
+ "peerDependencies": {
651
+ "@react-three/fiber": "^9.0.0",
652
+ "react": "^19",
653
+ "react-dom": "^19",
654
+ "three": ">=0.159"
655
+ },
656
+ "peerDependenciesMeta": {
657
+ "react-dom": {
658
+ "optional": true
659
+ }
660
+ }
661
+ },
662
+ "node_modules/@react-three/fiber": {
663
+ "version": "9.6.0",
664
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.0.tgz",
665
+ "integrity": "sha512-90abYK2q5/qDM+GACs9zRvc5KhEEpEWqWlHSd64zTPNxg+9wCJvTfyD9x2so7hlQhjRYO1Fa6flR3BC/kpTFkA==",
666
+ "license": "MIT",
667
+ "dependencies": {
668
+ "@babel/runtime": "^7.17.8",
669
+ "@types/webxr": "*",
670
+ "base64-js": "^1.5.1",
671
+ "buffer": "^6.0.3",
672
+ "its-fine": "^2.0.0",
673
+ "react-use-measure": "^2.1.7",
674
+ "scheduler": "^0.27.0",
675
+ "suspend-react": "^0.1.3",
676
+ "use-sync-external-store": "^1.4.0",
677
+ "zustand": "^5.0.3"
678
+ },
679
+ "peerDependencies": {
680
+ "expo": ">=43.0",
681
+ "expo-asset": ">=8.4",
682
+ "expo-file-system": ">=11.0",
683
+ "expo-gl": ">=11.0",
684
+ "react": ">=19 <19.3",
685
+ "react-dom": ">=19 <19.3",
686
+ "react-native": ">=0.78",
687
+ "three": ">=0.156"
688
+ },
689
+ "peerDependenciesMeta": {
690
+ "expo": {
691
+ "optional": true
692
+ },
693
+ "expo-asset": {
694
+ "optional": true
695
+ },
696
+ "expo-file-system": {
697
+ "optional": true
698
+ },
699
+ "expo-gl": {
700
+ "optional": true
701
+ },
702
+ "react-dom": {
703
+ "optional": true
704
+ },
705
+ "react-native": {
706
+ "optional": true
707
+ }
708
+ }
709
+ },
710
  "node_modules/@rolldown/binding-android-arm64": {
711
  "version": "1.0.0-rc.17",
712
  "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
 
714
  "cpu": [
715
  "arm64"
716
  ],
717
+ "dev": true,
718
  "license": "MIT",
719
  "optional": true,
720
  "os": [
 
731
  "cpu": [
732
  "arm64"
733
  ],
734
+ "dev": true,
735
  "license": "MIT",
736
  "optional": true,
737
  "os": [
 
748
  "cpu": [
749
  "x64"
750
  ],
751
+ "dev": true,
752
  "license": "MIT",
753
  "optional": true,
754
  "os": [
 
765
  "cpu": [
766
  "x64"
767
  ],
768
+ "dev": true,
769
  "license": "MIT",
770
  "optional": true,
771
  "os": [
 
782
  "cpu": [
783
  "arm"
784
  ],
785
+ "dev": true,
786
  "license": "MIT",
787
  "optional": true,
788
  "os": [
 
799
  "cpu": [
800
  "arm64"
801
  ],
802
+ "dev": true,
803
  "license": "MIT",
804
  "optional": true,
805
  "os": [
 
816
  "cpu": [
817
  "arm64"
818
  ],
819
+ "dev": true,
820
  "license": "MIT",
821
  "optional": true,
822
  "os": [
 
833
  "cpu": [
834
  "ppc64"
835
  ],
836
+ "dev": true,
837
  "license": "MIT",
838
  "optional": true,
839
  "os": [
 
850
  "cpu": [
851
  "s390x"
852
  ],
853
+ "dev": true,
854
  "license": "MIT",
855
  "optional": true,
856
  "os": [
 
867
  "cpu": [
868
  "x64"
869
  ],
870
+ "dev": true,
871
  "license": "MIT",
872
  "optional": true,
873
  "os": [
 
884
  "cpu": [
885
  "x64"
886
  ],
887
+ "dev": true,
888
  "license": "MIT",
889
  "optional": true,
890
  "os": [
 
901
  "cpu": [
902
  "arm64"
903
  ],
904
+ "dev": true,
905
  "license": "MIT",
906
  "optional": true,
907
  "os": [
 
918
  "cpu": [
919
  "wasm32"
920
  ],
921
+ "dev": true,
922
  "license": "MIT",
923
  "optional": true,
924
  "dependencies": {
 
937
  "cpu": [
938
  "arm64"
939
  ],
940
+ "dev": true,
941
  "license": "MIT",
942
  "optional": true,
943
  "os": [
 
954
  "cpu": [
955
  "x64"
956
  ],
957
+ "dev": true,
958
  "license": "MIT",
959
  "optional": true,
960
  "os": [
 
1228
  "vite": "^5.2.0 || ^6 || ^7 || ^8"
1229
  }
1230
  },
1231
+ "node_modules/@tweenjs/tween.js": {
1232
+ "version": "23.1.3",
1233
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
1234
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
1235
+ "license": "MIT"
1236
+ },
1237
  "node_modules/@tybys/wasm-util": {
1238
  "version": "0.10.1",
1239
  "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
 
1244
  "tslib": "^2.4.0"
1245
  }
1246
  },
1247
+ "node_modules/@types/draco3d": {
1248
+ "version": "1.4.10",
1249
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
1250
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
1251
+ "license": "MIT"
1252
+ },
1253
  "node_modules/@types/esrecurse": {
1254
  "version": "4.3.1",
1255
  "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
 
1271
  "dev": true,
1272
  "license": "MIT"
1273
  },
1274
+ "node_modules/@types/offscreencanvas": {
1275
+ "version": "2019.7.3",
1276
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
1277
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
1278
+ "license": "MIT"
1279
+ },
1280
  "node_modules/@types/react": {
1281
  "version": "19.2.14",
1282
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
 
1297
  "@types/react": "^19.2.0"
1298
  }
1299
  },
1300
+ "node_modules/@types/react-reconciler": {
1301
+ "version": "0.28.9",
1302
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
1303
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
1304
+ "license": "MIT",
1305
+ "peerDependencies": {
1306
+ "@types/react": "*"
1307
+ }
1308
+ },
1309
+ "node_modules/@types/stats.js": {
1310
+ "version": "0.17.4",
1311
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
1312
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
1313
+ "license": "MIT"
1314
+ },
1315
+ "node_modules/@types/three": {
1316
+ "version": "0.184.0",
1317
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz",
1318
+ "integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==",
1319
+ "license": "MIT",
1320
+ "dependencies": {
1321
+ "@dimforge/rapier3d-compat": "~0.12.0",
1322
+ "@tweenjs/tween.js": "~23.1.3",
1323
+ "@types/stats.js": "*",
1324
+ "@types/webxr": ">=0.5.17",
1325
+ "fflate": "~0.8.2",
1326
+ "meshoptimizer": "~1.1.1"
1327
+ }
1328
+ },
1329
+ "node_modules/@types/webxr": {
1330
+ "version": "0.5.24",
1331
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
1332
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
1333
+ "license": "MIT"
1334
+ },
1335
+ "node_modules/@use-gesture/core": {
1336
+ "version": "10.3.1",
1337
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
1338
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
1339
+ "license": "MIT"
1340
+ },
1341
+ "node_modules/@use-gesture/react": {
1342
+ "version": "10.3.1",
1343
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
1344
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
1345
+ "license": "MIT",
1346
+ "dependencies": {
1347
+ "@use-gesture/core": "10.3.1"
1348
+ },
1349
+ "peerDependencies": {
1350
+ "react": ">= 16.8.0"
1351
+ }
1352
+ },
1353
  "node_modules/@vitejs/plugin-react": {
1354
  "version": "6.0.1",
1355
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
 
1462
  "node": "18 || 20 || >=22"
1463
  }
1464
  },
1465
+ "node_modules/base64-js": {
1466
+ "version": "1.5.1",
1467
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
1468
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
1469
+ "funding": [
1470
+ {
1471
+ "type": "github",
1472
+ "url": "https://github.com/sponsors/feross"
1473
+ },
1474
+ {
1475
+ "type": "patreon",
1476
+ "url": "https://www.patreon.com/feross"
1477
+ },
1478
+ {
1479
+ "type": "consulting",
1480
+ "url": "https://feross.org/support"
1481
+ }
1482
+ ],
1483
+ "license": "MIT"
1484
+ },
1485
  "node_modules/baseline-browser-mapping": {
1486
  "version": "2.10.21",
1487
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
 
1494
  "node": ">=6.0.0"
1495
  }
1496
  },
1497
+ "node_modules/bidi-js": {
1498
+ "version": "1.0.3",
1499
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
1500
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
1501
+ "license": "MIT",
1502
+ "dependencies": {
1503
+ "require-from-string": "^2.0.2"
1504
+ }
1505
+ },
1506
  "node_modules/brace-expansion": {
1507
  "version": "5.0.5",
1508
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
 
1549
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1550
  }
1551
  },
1552
+ "node_modules/buffer": {
1553
+ "version": "6.0.3",
1554
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
1555
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
1556
+ "funding": [
1557
+ {
1558
+ "type": "github",
1559
+ "url": "https://github.com/sponsors/feross"
1560
+ },
1561
+ {
1562
+ "type": "patreon",
1563
+ "url": "https://www.patreon.com/feross"
1564
+ },
1565
+ {
1566
+ "type": "consulting",
1567
+ "url": "https://feross.org/support"
1568
+ }
1569
+ ],
1570
+ "license": "MIT",
1571
+ "dependencies": {
1572
+ "base64-js": "^1.3.1",
1573
+ "ieee754": "^1.2.1"
1574
+ }
1575
+ },
1576
  "node_modules/camelize": {
1577
  "version": "1.0.1",
1578
  "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
 
1582
  "url": "https://github.com/sponsors/ljharb"
1583
  }
1584
  },
1585
+ "node_modules/camera-controls": {
1586
+ "version": "3.1.0",
1587
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz",
1588
+ "integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==",
1589
+ "license": "MIT",
1590
+ "engines": {
1591
+ "node": ">=20.11.0",
1592
+ "npm": ">=10.8.2"
1593
+ },
1594
+ "peerDependencies": {
1595
+ "three": ">=0.126.1"
1596
+ }
1597
+ },
1598
  "node_modules/caniuse-lite": {
1599
  "version": "1.0.30001790",
1600
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
 
1622
  "dev": true,
1623
  "license": "MIT"
1624
  },
1625
+ "node_modules/cross-env": {
1626
+ "version": "7.0.3",
1627
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
1628
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
1629
+ "license": "MIT",
1630
+ "dependencies": {
1631
+ "cross-spawn": "^7.0.1"
1632
+ },
1633
+ "bin": {
1634
+ "cross-env": "src/bin/cross-env.js",
1635
+ "cross-env-shell": "src/bin/cross-env-shell.js"
1636
+ },
1637
+ "engines": {
1638
+ "node": ">=10.14",
1639
+ "npm": ">=6",
1640
+ "yarn": ">=1"
1641
+ }
1642
+ },
1643
  "node_modules/cross-spawn": {
1644
  "version": "7.0.6",
1645
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1646
  "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
 
1647
  "license": "MIT",
1648
  "dependencies": {
1649
  "path-key": "^3.1.0",
 
1705
  "dev": true,
1706
  "license": "MIT"
1707
  },
1708
+ "node_modules/detect-gpu": {
1709
+ "version": "5.0.70",
1710
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
1711
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
1712
+ "license": "MIT",
1713
+ "dependencies": {
1714
+ "webgl-constants": "^1.1.1"
1715
+ }
1716
+ },
1717
  "node_modules/detect-libc": {
1718
  "version": "2.1.2",
1719
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
 
1723
  "node": ">=8"
1724
  }
1725
  },
1726
+ "node_modules/draco3d": {
1727
+ "version": "1.5.7",
1728
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
1729
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
1730
+ "license": "Apache-2.0"
1731
+ },
1732
  "node_modules/electron-to-chromium": {
1733
  "version": "1.5.344",
1734
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
 
1977
  "version": "6.5.0",
1978
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1979
  "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1980
+ "dev": true,
1981
  "license": "MIT",
1982
  "engines": {
1983
  "node": ">=12.0.0"
 
1991
  }
1992
  }
1993
  },
1994
+ "node_modules/fflate": {
1995
+ "version": "0.8.2",
1996
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
1997
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
1998
+ "license": "MIT"
1999
+ },
2000
  "node_modules/file-entry-cache": {
2001
  "version": "8.0.0",
2002
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
 
2061
  "url": "https://github.com/sponsors/rawify"
2062
  }
2063
  },
2064
+ "node_modules/framer-motion": {
2065
+ "version": "12.38.0",
2066
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
2067
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
2068
+ "license": "MIT",
2069
+ "dependencies": {
2070
+ "motion-dom": "^12.38.0",
2071
+ "motion-utils": "^12.36.0",
2072
+ "tslib": "^2.4.0"
2073
+ },
2074
+ "peerDependencies": {
2075
+ "@emotion/is-prop-valid": "*",
2076
+ "react": "^18.0.0 || ^19.0.0",
2077
+ "react-dom": "^18.0.0 || ^19.0.0"
2078
+ },
2079
+ "peerDependenciesMeta": {
2080
+ "@emotion/is-prop-valid": {
2081
+ "optional": true
2082
+ },
2083
+ "react": {
2084
+ "optional": true
2085
+ },
2086
+ "react-dom": {
2087
+ "optional": true
2088
+ }
2089
+ }
2090
+ },
2091
  "node_modules/fsevents": {
2092
  "version": "2.3.3",
2093
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
2094
  "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2095
+ "dev": true,
2096
  "hasInstallScript": true,
2097
  "license": "MIT",
2098
  "optional": true,
 
2139
  "url": "https://github.com/sponsors/sindresorhus"
2140
  }
2141
  },
2142
+ "node_modules/glsl-noise": {
2143
+ "version": "0.0.0",
2144
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
2145
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
2146
+ "license": "MIT"
2147
+ },
2148
  "node_modules/graceful-fs": {
2149
  "version": "4.2.11",
2150
  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
 
2168
  "hermes-estree": "0.25.1"
2169
  }
2170
  },
2171
+ "node_modules/hls.js": {
2172
+ "version": "1.6.16",
2173
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
2174
+ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
2175
+ "license": "Apache-2.0"
2176
+ },
2177
+ "node_modules/ieee754": {
2178
+ "version": "1.2.1",
2179
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
2180
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
2181
+ "funding": [
2182
+ {
2183
+ "type": "github",
2184
+ "url": "https://github.com/sponsors/feross"
2185
+ },
2186
+ {
2187
+ "type": "patreon",
2188
+ "url": "https://www.patreon.com/feross"
2189
+ },
2190
+ {
2191
+ "type": "consulting",
2192
+ "url": "https://feross.org/support"
2193
+ }
2194
+ ],
2195
+ "license": "BSD-3-Clause"
2196
+ },
2197
  "node_modules/ignore": {
2198
  "version": "5.3.2",
2199
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
2204
  "node": ">= 4"
2205
  }
2206
  },
2207
+ "node_modules/immediate": {
2208
+ "version": "3.0.6",
2209
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
2210
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
2211
+ "license": "MIT"
2212
+ },
2213
  "node_modules/imurmurhash": {
2214
  "version": "0.1.4",
2215
  "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
 
2243
  "node": ">=0.10.0"
2244
  }
2245
  },
2246
+ "node_modules/is-promise": {
2247
+ "version": "2.2.2",
2248
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
2249
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
2250
+ "license": "MIT"
2251
+ },
2252
  "node_modules/isexe": {
2253
  "version": "2.0.0",
2254
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
2255
  "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
 
2256
  "license": "ISC"
2257
  },
2258
+ "node_modules/its-fine": {
2259
+ "version": "2.0.0",
2260
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
2261
+ "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
2262
+ "license": "MIT",
2263
+ "dependencies": {
2264
+ "@types/react-reconciler": "^0.28.9"
2265
+ },
2266
+ "peerDependencies": {
2267
+ "react": "^19.0.0"
2268
+ }
2269
+ },
2270
  "node_modules/jiti": {
2271
  "version": "2.6.1",
2272
  "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
 
2354
  "node": ">= 0.8.0"
2355
  }
2356
  },
2357
+ "node_modules/lie": {
2358
+ "version": "3.3.0",
2359
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
2360
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
2361
+ "license": "MIT",
2362
+ "dependencies": {
2363
+ "immediate": "~3.0.5"
2364
+ }
2365
+ },
2366
  "node_modules/lightningcss": {
2367
  "version": "1.32.0",
2368
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
 
2638
  "yallist": "^3.0.2"
2639
  }
2640
  },
2641
+ "node_modules/maath": {
2642
+ "version": "0.10.8",
2643
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
2644
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
2645
+ "license": "MIT",
2646
+ "peerDependencies": {
2647
+ "@types/three": ">=0.134.0",
2648
+ "three": ">=0.134.0"
2649
+ }
2650
+ },
2651
  "node_modules/magic-string": {
2652
  "version": "0.30.21",
2653
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
 
2657
  "@jridgewell/sourcemap-codec": "^1.5.5"
2658
  }
2659
  },
2660
+ "node_modules/meshline": {
2661
+ "version": "3.3.1",
2662
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
2663
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
2664
+ "license": "MIT",
2665
+ "peerDependencies": {
2666
+ "three": ">=0.137"
2667
+ }
2668
+ },
2669
+ "node_modules/meshoptimizer": {
2670
+ "version": "1.1.1",
2671
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
2672
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
2673
+ "license": "MIT"
2674
+ },
2675
  "node_modules/minimatch": {
2676
  "version": "10.2.5",
2677
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
 
2688
  "url": "https://github.com/sponsors/isaacs"
2689
  }
2690
  },
2691
+ "node_modules/motion-dom": {
2692
+ "version": "12.38.0",
2693
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
2694
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
2695
+ "license": "MIT",
2696
+ "dependencies": {
2697
+ "motion-utils": "^12.36.0"
2698
+ }
2699
+ },
2700
+ "node_modules/motion-utils": {
2701
+ "version": "12.36.0",
2702
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
2703
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
2704
+ "license": "MIT"
2705
+ },
2706
  "node_modules/ms": {
2707
  "version": "2.1.3",
2708
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 
2805
  "version": "3.1.1",
2806
  "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2807
  "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
 
2808
  "license": "MIT",
2809
  "engines": {
2810
  "node": ">=8"
 
2820
  "version": "4.0.4",
2821
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2822
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2823
+ "dev": true,
2824
  "license": "MIT",
2825
  "engines": {
2826
  "node": ">=12"
 
2863
  "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2864
  "license": "MIT"
2865
  },
2866
+ "node_modules/potpack": {
2867
+ "version": "1.0.2",
2868
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
2869
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
2870
+ "license": "ISC"
2871
+ },
2872
  "node_modules/prelude-ls": {
2873
  "version": "1.2.1",
2874
  "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
 
2879
  "node": ">= 0.8.0"
2880
  }
2881
  },
2882
+ "node_modules/promise-worker-transferable": {
2883
+ "version": "1.0.4",
2884
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
2885
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
2886
+ "license": "Apache-2.0",
2887
+ "dependencies": {
2888
+ "is-promise": "^2.1.0",
2889
+ "lie": "^3.0.2"
2890
+ }
2891
+ },
2892
  "node_modules/punycode": {
2893
  "version": "2.3.1",
2894
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
 
2920
  "react": "^19.2.5"
2921
  }
2922
  },
2923
+ "node_modules/react-use-measure": {
2924
+ "version": "2.1.7",
2925
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
2926
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
2927
+ "license": "MIT",
2928
+ "peerDependencies": {
2929
+ "react": ">=16.13",
2930
+ "react-dom": ">=16.13"
2931
+ },
2932
+ "peerDependenciesMeta": {
2933
+ "react-dom": {
2934
+ "optional": true
2935
+ }
2936
+ }
2937
+ },
2938
+ "node_modules/require-from-string": {
2939
+ "version": "2.0.2",
2940
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
2941
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
2942
+ "license": "MIT",
2943
+ "engines": {
2944
+ "node": ">=0.10.0"
2945
+ }
2946
+ },
2947
  "node_modules/rolldown": {
2948
  "version": "1.0.0-rc.17",
2949
  "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
2950
  "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
2951
+ "dev": true,
2952
  "license": "MIT",
2953
  "dependencies": {
2954
  "@oxc-project/types": "=0.127.0",
 
2982
  "version": "1.0.0-rc.17",
2983
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
2984
  "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
2985
+ "dev": true,
2986
  "license": "MIT"
2987
  },
2988
  "node_modules/scheduler": {
 
3005
  "version": "2.0.0",
3006
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
3007
  "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
 
3008
  "license": "MIT",
3009
  "dependencies": {
3010
  "shebang-regex": "^3.0.0"
 
3017
  "version": "3.0.0",
3018
  "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
3019
  "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
 
3020
  "license": "MIT",
3021
  "engines": {
3022
  "node": ">=8"
 
3031
  "node": ">=0.10.0"
3032
  }
3033
  },
3034
+ "node_modules/stats-gl": {
3035
+ "version": "2.4.2",
3036
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
3037
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
3038
+ "license": "MIT",
3039
+ "dependencies": {
3040
+ "@types/three": "*",
3041
+ "three": "^0.170.0"
3042
+ },
3043
+ "peerDependencies": {
3044
+ "@types/three": "*",
3045
+ "three": "*"
3046
+ }
3047
+ },
3048
+ "node_modules/stats-gl/node_modules/three": {
3049
+ "version": "0.170.0",
3050
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
3051
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
3052
+ "license": "MIT"
3053
+ },
3054
+ "node_modules/stats.js": {
3055
+ "version": "0.17.0",
3056
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
3057
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
3058
+ "license": "MIT"
3059
+ },
3060
  "node_modules/styled-components": {
3061
  "version": "6.4.1",
3062
  "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.1.tgz",
 
3099
  "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
3100
  "license": "MIT"
3101
  },
3102
+ "node_modules/suspend-react": {
3103
+ "version": "0.1.3",
3104
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
3105
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
3106
+ "license": "MIT",
3107
+ "peerDependencies": {
3108
+ "react": ">=17.0"
3109
+ }
3110
+ },
3111
  "node_modules/tailwindcss": {
3112
  "version": "4.2.4",
3113
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
 
3127
  "url": "https://opencollective.com/webpack"
3128
  }
3129
  },
3130
+ "node_modules/three": {
3131
+ "version": "0.184.0",
3132
+ "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
3133
+ "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
3134
+ "license": "MIT"
3135
+ },
3136
+ "node_modules/three-mesh-bvh": {
3137
+ "version": "0.8.3",
3138
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
3139
+ "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
3140
+ "license": "MIT",
3141
+ "peerDependencies": {
3142
+ "three": ">= 0.159.0"
3143
+ }
3144
+ },
3145
+ "node_modules/three-stdlib": {
3146
+ "version": "2.36.1",
3147
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
3148
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
3149
+ "license": "MIT",
3150
+ "dependencies": {
3151
+ "@types/draco3d": "^1.4.0",
3152
+ "@types/offscreencanvas": "^2019.6.4",
3153
+ "@types/webxr": "^0.5.2",
3154
+ "draco3d": "^1.4.1",
3155
+ "fflate": "^0.6.9",
3156
+ "potpack": "^1.0.1"
3157
+ },
3158
+ "peerDependencies": {
3159
+ "three": ">=0.128.0"
3160
+ }
3161
+ },
3162
+ "node_modules/three-stdlib/node_modules/fflate": {
3163
+ "version": "0.6.10",
3164
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
3165
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
3166
+ "license": "MIT"
3167
+ },
3168
  "node_modules/tinyglobby": {
3169
  "version": "0.2.16",
3170
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
3171
  "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
3172
+ "dev": true,
3173
  "license": "MIT",
3174
  "dependencies": {
3175
  "fdir": "^6.5.0",
 
3182
  "url": "https://github.com/sponsors/SuperchupuDev"
3183
  }
3184
  },
3185
+ "node_modules/troika-three-text": {
3186
+ "version": "0.52.4",
3187
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
3188
+ "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
3189
+ "license": "MIT",
3190
+ "dependencies": {
3191
+ "bidi-js": "^1.0.2",
3192
+ "troika-three-utils": "^0.52.4",
3193
+ "troika-worker-utils": "^0.52.0",
3194
+ "webgl-sdf-generator": "1.1.1"
3195
+ },
3196
+ "peerDependencies": {
3197
+ "three": ">=0.125.0"
3198
+ }
3199
+ },
3200
+ "node_modules/troika-three-utils": {
3201
+ "version": "0.52.4",
3202
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
3203
+ "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
3204
+ "license": "MIT",
3205
+ "peerDependencies": {
3206
+ "three": ">=0.125.0"
3207
+ }
3208
+ },
3209
+ "node_modules/troika-worker-utils": {
3210
+ "version": "0.52.0",
3211
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
3212
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
3213
+ "license": "MIT"
3214
+ },
3215
  "node_modules/tslib": {
3216
  "version": "2.8.1",
3217
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
3218
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
3219
+ "license": "0BSD"
3220
+ },
3221
+ "node_modules/tunnel-rat": {
3222
+ "version": "0.1.2",
3223
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
3224
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
3225
+ "license": "MIT",
3226
+ "dependencies": {
3227
+ "zustand": "^4.3.2"
3228
+ }
3229
+ },
3230
+ "node_modules/tunnel-rat/node_modules/zustand": {
3231
+ "version": "4.5.7",
3232
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
3233
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
3234
+ "license": "MIT",
3235
+ "dependencies": {
3236
+ "use-sync-external-store": "^1.2.2"
3237
+ },
3238
+ "engines": {
3239
+ "node": ">=12.7.0"
3240
+ },
3241
+ "peerDependencies": {
3242
+ "@types/react": ">=16.8",
3243
+ "immer": ">=9.0.6",
3244
+ "react": ">=16.8"
3245
+ },
3246
+ "peerDependenciesMeta": {
3247
+ "@types/react": {
3248
+ "optional": true
3249
+ },
3250
+ "immer": {
3251
+ "optional": true
3252
+ },
3253
+ "react": {
3254
+ "optional": true
3255
+ }
3256
+ }
3257
  },
3258
  "node_modules/type-check": {
3259
  "version": "0.4.0",
 
3308
  "punycode": "^2.1.0"
3309
  }
3310
  },
3311
+ "node_modules/use-sync-external-store": {
3312
+ "version": "1.6.0",
3313
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
3314
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
3315
+ "license": "MIT",
3316
+ "peerDependencies": {
3317
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
3318
+ }
3319
+ },
3320
+ "node_modules/utility-types": {
3321
+ "version": "3.11.0",
3322
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
3323
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
3324
+ "license": "MIT",
3325
+ "engines": {
3326
+ "node": ">= 4"
3327
+ }
3328
+ },
3329
  "node_modules/vite": {
3330
  "version": "8.0.10",
3331
  "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
3332
  "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
3333
+ "dev": true,
3334
  "license": "MIT",
3335
  "dependencies": {
3336
  "lightningcss": "^1.32.0",
 
3404
  }
3405
  }
3406
  },
3407
+ "node_modules/webgl-constants": {
3408
+ "version": "1.1.1",
3409
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
3410
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
3411
+ },
3412
+ "node_modules/webgl-sdf-generator": {
3413
+ "version": "1.1.1",
3414
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
3415
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
3416
+ "license": "MIT"
3417
+ },
3418
  "node_modules/which": {
3419
  "version": "2.0.2",
3420
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
3421
  "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
 
3422
  "license": "ISC",
3423
  "dependencies": {
3424
  "isexe": "^2.0.0"
 
3482
  "peerDependencies": {
3483
  "zod": "^3.25.0 || ^4.0.0"
3484
  }
3485
+ },
3486
+ "node_modules/zustand": {
3487
+ "version": "5.0.12",
3488
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
3489
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
3490
+ "license": "MIT",
3491
+ "engines": {
3492
+ "node": ">=12.20.0"
3493
+ },
3494
+ "peerDependencies": {
3495
+ "@types/react": ">=18.0.0",
3496
+ "immer": ">=9.0.6",
3497
+ "react": ">=18.0.0",
3498
+ "use-sync-external-store": ">=1.2.0"
3499
+ },
3500
+ "peerDependenciesMeta": {
3501
+ "@types/react": {
3502
+ "optional": true
3503
+ },
3504
+ "immer": {
3505
+ "optional": true
3506
+ },
3507
+ "react": {
3508
+ "optional": true
3509
+ },
3510
+ "use-sync-external-store": {
3511
+ "optional": true
3512
+ }
3513
+ }
3514
  }
3515
  }
3516
  }
frontend/package.json CHANGED
@@ -10,13 +10,18 @@
10
  "preview": "vite preview"
11
  },
12
  "dependencies": {
 
 
13
  "@tailwindcss/vite": "^4.2.4",
14
  "autoprefixer": "^10.5.0",
 
15
  "postcss": "^8.5.10",
16
  "react": "^19.2.5",
17
  "react-dom": "^19.2.5",
18
  "styled-components": "^6.4.1",
19
- "tailwindcss": "^4.2.4"
 
 
20
  },
21
  "devDependencies": {
22
  "@eslint/js": "^10.0.1",
 
10
  "preview": "vite preview"
11
  },
12
  "dependencies": {
13
+ "@react-three/drei": "^10.7.7",
14
+ "@react-three/fiber": "^9.6.0",
15
  "@tailwindcss/vite": "^4.2.4",
16
  "autoprefixer": "^10.5.0",
17
+ "framer-motion": "^12.38.0",
18
  "postcss": "^8.5.10",
19
  "react": "^19.2.5",
20
  "react-dom": "^19.2.5",
21
  "styled-components": "^6.4.1",
22
+ "tailwindcss": "^4.2.4",
23
+ "three": "^0.184.0",
24
+ "zustand": "^5.0.12"
25
  },
26
  "devDependencies": {
27
  "@eslint/js": "^10.0.1",
frontend/src/App.tsx ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, Suspense, lazy } from 'react'
2
+ import { motion, AnimatePresence } from 'framer-motion'
3
+ import { useStore } from './store'
4
+ import { checkHealth } from './api'
5
+ import UploadZone from './components/UploadZone'
6
+ import ResultPanel from './components/ResultPanel'
7
+
8
+ // Lazy-load the heavy 3D component
9
+ const DeepfakeOrb = lazy(() => import('./components/DeepfakeOrb'))
10
+
11
+ export default function App() {
12
+ const { state, result, error, reset } = useStore()
13
+ const verdict = result?.result ?? null
14
+
15
+ // Health check on mount
16
+ useEffect(() => {
17
+ const badge = document.getElementById('modelBadge')
18
+ checkHealth()
19
+ .then((d) => {
20
+ if (badge) {
21
+ badge.textContent = d.model.toUpperCase()
22
+ badge.style.color = '#00ff88bb'
23
+ badge.style.borderColor = '#00ff8844'
24
+ }
25
+ })
26
+ .catch(() => {
27
+ if (badge) {
28
+ badge.textContent = 'SERVER OFFLINE'
29
+ badge.style.color = '#ff3355bb'
30
+ badge.style.borderColor = '#ff335544'
31
+ }
32
+ })
33
+ }, [])
34
+
35
+ const showUpload = state === 'idle' || state === 'loading'
36
+ const showResult = state === 'result'
37
+ const showError = state === 'error'
38
+
39
+ return (
40
+ <div className="min-h-screen text-white overflow-x-hidden" style={{ background: '#050508' }}>
41
+
42
+ {/* ── Sticky header ── */}
43
+ <header className="sticky top-0 z-50 border-b border-white/5"
44
+ style={{ background: 'rgba(5,5,8,0.85)', backdropFilter: 'blur(20px)' }}>
45
+ <div className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
46
+ <div className="flex items-center gap-3">
47
+ <motion.div
48
+ className="w-9 h-9 rounded-lg border border-[#00ff8855] flex items-center justify-center"
49
+ animate={{ boxShadow: ['0 0 8px #00ff8820', '0 0 20px #00ff8840', '0 0 8px #00ff8820'] }}
50
+ transition={{ duration: 2.5, repeat: Infinity }}
51
+ >
52
+ <svg width="18" height="18" fill="none" stroke="#00ff88" strokeWidth="1.5" viewBox="0 0 24 24">
53
+ <path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.964-7.178z"/>
54
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
55
+ </svg>
56
+ </motion.div>
57
+ <div>
58
+ <p className="text-sm font-bold tracking-widest" style={{ color: '#00ff88', textShadow: '0 0 12px #00ff8855' }}>
59
+ DEEPFAKE AUTHENTICATOR
60
+ </p>
61
+ <p className="text-[9px] tracking-widest text-white/30 font-mono">AI-POWERED VIDEO FORENSICS</p>
62
+ </div>
63
+ </div>
64
+ <div
65
+ id="modelBadge"
66
+ className="text-[10px] px-3 py-1.5 rounded-full border font-mono tracking-wider transition-all duration-500"
67
+ style={{ borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.3)' }}
68
+ >
69
+ CONNECTING...
70
+ </div>
71
+ </div>
72
+ </header>
73
+
74
+ {/* ── Main layout ── */}
75
+ <main className="max-w-5xl mx-auto px-6 py-10">
76
+ <div className="flex flex-col lg:flex-row gap-10 items-start">
77
+
78
+ {/* ── Left: 3D orb + hero ── */}
79
+ <div className="w-full lg:w-80 flex-shrink-0 flex flex-col items-center gap-6 lg:sticky lg:top-24">
80
+ {/* 3D Orb */}
81
+ <div className="w-64 h-64 relative">
82
+ <Suspense fallback={
83
+ <div className="w-full h-full rounded-full border border-[#00ff8830] flex items-center justify-center">
84
+ <div className="w-16 h-16 rounded-full border-2 border-[#00ff88] border-t-transparent animate-spin" />
85
+ </div>
86
+ }>
87
+ <DeepfakeOrb state={state} verdict={verdict} />
88
+ </Suspense>
89
+ </div>
90
+
91
+ {/* Hero text */}
92
+ <AnimatePresence mode="wait">
93
+ {showUpload && (
94
+ <motion.div
95
+ key="hero"
96
+ className="text-center"
97
+ initial={{ opacity: 0, y: 10 }}
98
+ animate={{ opacity: 1, y: 0 }}
99
+ exit={{ opacity: 0, y: -10 }}
100
+ >
101
+ <h1 className="text-2xl font-bold mb-2 leading-tight"
102
+ style={{ background: 'linear-gradient(135deg, #fff 30%, #00ff88)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
103
+ Is This Video Real?
104
+ </h1>
105
+ <p className="text-white/40 text-xs leading-relaxed max-w-xs">
106
+ Dual ViT ensemble + Wav2Vec2 audio analysis detects facial manipulation and AI-generated voices.
107
+ </p>
108
+ {/* Stat pills */}
109
+ <div className="flex flex-wrap gap-2 justify-center mt-4">
110
+ {['2 ViT Models', '40 Frames', 'Audio AI', '< 20s'].map((s) => (
111
+ <span key={s} className="text-[10px] px-2.5 py-1 rounded-full border border-white/8 text-white/40 bg-white/3">
112
+ {s}
113
+ </span>
114
+ ))}
115
+ </div>
116
+ </motion.div>
117
+ )}
118
+
119
+ {showResult && verdict && (
120
+ <motion.div
121
+ key="verdict-summary"
122
+ className="text-center"
123
+ initial={{ opacity: 0, scale: 0.9 }}
124
+ animate={{ opacity: 1, scale: 1 }}
125
+ exit={{ opacity: 0 }}
126
+ >
127
+ <p className="text-3xl font-bold tracking-widest mb-1"
128
+ style={{ color: verdict === 'FAKE' ? '#ff3355' : '#00ff88', textShadow: `0 0 20px ${verdict === 'FAKE' ? '#ff335566' : '#00ff8866'}` }}>
129
+ {verdict === 'FAKE' ? 'DEEPFAKE' : 'AUTHENTIC'}
130
+ </p>
131
+ <p className="text-white/30 text-xs font-mono">{result?.confidence}% confidence</p>
132
+ </motion.div>
133
+ )}
134
+ </AnimatePresence>
135
+ </div>
136
+
137
+ {/* ── Right: Upload / Result / Error ── */}
138
+ <div className="flex-1 min-w-0">
139
+ <AnimatePresence mode="wait">
140
+ {showUpload && (
141
+ <motion.div key="upload" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
142
+ <UploadZone />
143
+ </motion.div>
144
+ )}
145
+
146
+ {showResult && (
147
+ <motion.div key="result" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
148
+ <ResultPanel />
149
+ </motion.div>
150
+ )}
151
+
152
+ {showError && (
153
+ <motion.div
154
+ key="error"
155
+ className="rounded-2xl border border-red-500/20 bg-red-500/5 p-6"
156
+ initial={{ opacity: 0, scale: 0.95 }}
157
+ animate={{ opacity: 1, scale: 1 }}
158
+ exit={{ opacity: 0 }}
159
+ >
160
+ <div className="flex items-center gap-3 mb-3">
161
+ <span className="text-xl">⚠️</span>
162
+ <span className="text-red-400 font-bold text-sm tracking-wider uppercase">Analysis Failed</span>
163
+ </div>
164
+ <p className="text-white/50 text-sm leading-relaxed">{error}</p>
165
+ <button
166
+ onClick={reset}
167
+ className="mt-4 text-xs text-white/30 hover:text-white transition-colors font-mono tracking-wider"
168
+ >
169
+ ↻ Try again
170
+ </button>
171
+ </motion.div>
172
+ )}
173
+ </AnimatePresence>
174
+ </div>
175
+ </div>
176
+ </main>
177
+
178
+ <footer className="text-center py-8 text-[10px] text-white/15 font-mono tracking-widest border-t border-white/3">
179
+ DEEPFAKE AUTHENTICATOR · MEDIAPIPE + VIT ENSEMBLE + WAV2VEC2 · LOCAL INFERENCE
180
+ </footer>
181
+ </div>
182
+ )
183
+ }
frontend/src/api.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AnalysisResult } from './store'
2
+
3
+ const BASE = import.meta.env.DEV ? 'http://localhost:8000' : window.location.origin
4
+
5
+ export async function analyzeVideo(file: File): Promise<AnalysisResult> {
6
+ const fd = new FormData()
7
+ fd.append('file', file)
8
+ const res = await fetch(`${BASE}/analyze`, { method: 'POST', body: fd })
9
+ if (!res.ok) {
10
+ const err = await res.json().catch(() => ({}))
11
+ throw new Error((err as any).detail || `Server error ${res.status}`)
12
+ }
13
+ return res.json()
14
+ }
15
+
16
+ export async function checkHealth(): Promise<{ status: string; model: string; ready: boolean }> {
17
+ const res = await fetch(`${BASE}/health`)
18
+ if (!res.ok) throw new Error('Server offline')
19
+ return res.json()
20
+ }
frontend/src/components/DeepfakeOrb.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useMemo } from 'react'
2
+ import { Canvas, useFrame } from '@react-three/fiber'
3
+ import { Sphere, MeshDistortMaterial, Float, Stars } from '@react-three/drei'
4
+ import * as THREE from 'three'
5
+ import type { AppState, Verdict } from '../store'
6
+
7
+ // ── Inner animated orb ────────────────────────────────────────────────────────
8
+ function Orb({ state, verdict }: { state: AppState; verdict: Verdict }) {
9
+ const meshRef = useRef<THREE.Mesh>(null)
10
+ const ringRef = useRef<THREE.Mesh>(null)
11
+ const ring2Ref = useRef<THREE.Mesh>(null)
12
+
13
+ const isFake = verdict === 'FAKE'
14
+ const isReal = verdict === 'REAL'
15
+ const isLoading = state === 'loading'
16
+
17
+ const color = isFake ? '#ff3355' : isReal ? '#00ff88' : '#00aaff'
18
+ const emissive = isFake ? '#ff0033' : isReal ? '#00cc66' : '#0066ff'
19
+
20
+ useFrame((_, delta) => {
21
+ if (!meshRef.current) return
22
+ meshRef.current.rotation.y += delta * (isLoading ? 1.5 : 0.4)
23
+ meshRef.current.rotation.x += delta * (isLoading ? 0.8 : 0.15)
24
+ if (ringRef.current) ringRef.current.rotation.z += delta * 0.6
25
+ if (ring2Ref.current) ring2Ref.current.rotation.x += delta * 0.4
26
+ })
27
+
28
+ // Particle ring
29
+ const particles = useMemo(() => {
30
+ const count = 120
31
+ const positions = new Float32Array(count * 3)
32
+ for (let i = 0; i < count; i++) {
33
+ const angle = (i / count) * Math.PI * 2
34
+ const radius = 1.8 + Math.random() * 0.4
35
+ positions[i * 3] = Math.cos(angle) * radius
36
+ positions[i * 3 + 1] = (Math.random() - 0.5) * 0.3
37
+ positions[i * 3 + 2] = Math.sin(angle) * radius
38
+ }
39
+ return positions
40
+ }, [])
41
+
42
+ return (
43
+ <group>
44
+ {/* Stars background */}
45
+ <Stars radius={8} depth={4} count={300} factor={1} fade speed={isLoading ? 3 : 0.5} />
46
+
47
+ {/* Main orb */}
48
+ <Float speed={isLoading ? 4 : 1.5} rotationIntensity={isLoading ? 1 : 0.3} floatIntensity={isLoading ? 1.5 : 0.5}>
49
+ <Sphere ref={meshRef} args={[1, 64, 64]}>
50
+ <MeshDistortMaterial
51
+ color={color}
52
+ emissive={emissive}
53
+ emissiveIntensity={isLoading ? 0.8 : 0.4}
54
+ distort={isLoading ? 0.6 : isFake ? 0.45 : 0.25}
55
+ speed={isLoading ? 4 : 2}
56
+ roughness={0.1}
57
+ metalness={0.8}
58
+ transparent
59
+ opacity={0.9}
60
+ />
61
+ </Sphere>
62
+ </Float>
63
+
64
+ {/* Orbit ring 1 */}
65
+ <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]}>
66
+ <torusGeometry args={[1.6, 0.015, 8, 100]} />
67
+ <meshBasicMaterial color={color} transparent opacity={0.5} />
68
+ </mesh>
69
+
70
+ {/* Orbit ring 2 */}
71
+ <mesh ref={ring2Ref} rotation={[Math.PI / 3, Math.PI / 4, 0]}>
72
+ <torusGeometry args={[1.9, 0.008, 8, 100]} />
73
+ <meshBasicMaterial color={color} transparent opacity={0.25} />
74
+ </mesh>
75
+
76
+ {/* Particle ring */}
77
+ <points>
78
+ <bufferGeometry>
79
+ <bufferAttribute attach="attributes-position" args={[particles, 3]} />
80
+ </bufferGeometry>
81
+ <pointsMaterial color={color} size={0.025} transparent opacity={0.6} />
82
+ </points>
83
+
84
+ {/* Ambient + point lights */}
85
+ <ambientLight intensity={0.2} />
86
+ <pointLight position={[3, 3, 3]} intensity={2} color={color} />
87
+ <pointLight position={[-3, -3, -3]} intensity={1} color={emissive} />
88
+ </group>
89
+ )
90
+ }
91
+
92
+ // ── Exported canvas wrapper ───────────────────────────────────────────────────
93
+ export default function DeepfakeOrb({ state, verdict }: { state: AppState; verdict: Verdict }) {
94
+ return (
95
+ <Canvas
96
+ camera={{ position: [0, 0, 4], fov: 50 }}
97
+ style={{ width: '100%', height: '100%' }}
98
+ gl={{ antialias: true, alpha: true }}
99
+ >
100
+ <Orb state={state} verdict={verdict} />
101
+ </Canvas>
102
+ )
103
+ }
frontend/src/components/ResultPanel.tsx ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion, AnimatePresence } from 'framer-motion'
2
+ import { useStore } from '../store'
3
+ import type { FramePoint, AudioResult } from '../store'
4
+
5
+ // ── Animated confidence arc ───────────────────────────────────────────────────
6
+ function ConfidenceArc({ pct, isFake }: { pct: number; isFake: boolean }) {
7
+ const r = 54
8
+ const circ = 2 * Math.PI * r
9
+ const half = circ / 2
10
+ const fill = (pct / 100) * half
11
+ const color = isFake ? '#ff3355' : '#00ff88'
12
+
13
+ return (
14
+ <div className="relative w-40 h-20 mx-auto">
15
+ <svg width="160" height="80" viewBox="0 0 160 80">
16
+ {/* Track */}
17
+ <path
18
+ d={`M 16 80 A ${r} ${r} 0 0 1 144 80`}
19
+ fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="10" strokeLinecap="round"
20
+ />
21
+ {/* Fill */}
22
+ <motion.path
23
+ d={`M 16 80 A ${r} ${r} 0 0 1 144 80`}
24
+ fill="none"
25
+ stroke={color}
26
+ strokeWidth="10"
27
+ strokeLinecap="round"
28
+ strokeDasharray={`${half} ${half}`}
29
+ initial={{ strokeDashoffset: half }}
30
+ animate={{ strokeDashoffset: half - fill }}
31
+ transition={{ duration: 1.4, ease: [0.22, 1, 0.36, 1] }}
32
+ style={{ filter: `drop-shadow(0 0 8px ${color})` }}
33
+ />
34
+ </svg>
35
+ <div className="absolute inset-0 flex flex-col items-center justify-end pb-1">
36
+ <motion.span
37
+ className="text-2xl font-bold font-mono"
38
+ style={{ color }}
39
+ initial={{ opacity: 0 }}
40
+ animate={{ opacity: 1 }}
41
+ transition={{ delay: 0.5 }}
42
+ >
43
+ {pct}%
44
+ </motion.span>
45
+ </div>
46
+ </div>
47
+ )
48
+ }
49
+
50
+ // ── Frame timeline ────────────────────────────────────────────────────────────
51
+ function Timeline({ frames, isFake }: { frames: FramePoint[]; isFake: boolean }) {
52
+ if (!frames.length) return (
53
+ <p className="text-white/30 text-xs font-mono text-center py-4">No per-frame data</p>
54
+ )
55
+
56
+ const color = isFake ? '#ff3355' : '#00ff88'
57
+ const maxH = 60
58
+
59
+ return (
60
+ <div className="flex items-end gap-1 overflow-x-auto pb-1" style={{ height: maxH + 24 + 'px', position: 'relative' }}>
61
+ {/* 60% threshold line */}
62
+ <div
63
+ className="absolute left-0 right-0 pointer-events-none"
64
+ style={{ bottom: 24 + maxH * 0.6, height: 1, background: 'rgba(255,170,0,0.35)' }}
65
+ >
66
+ <span className="absolute right-0 -top-4 text-[9px] text-yellow-400/60 font-mono">60%</span>
67
+ </div>
68
+
69
+ {frames.map((pt, i) => {
70
+ const h = Math.max(3, (pt.fake_pct / 100) * maxH)
71
+ const hot = pt.fake_pct >= 60
72
+ return (
73
+ <div key={i} className="group relative flex-shrink-0" style={{ width: 10, height: maxH }}>
74
+ {/* Tooltip */}
75
+ <div className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-[#0a0a14] border border-white/10 rounded px-1.5 py-0.5 text-[9px] font-mono whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none">
76
+ F{pt.frame}: {pt.fake_pct}%
77
+ </div>
78
+ <motion.div
79
+ className="absolute bottom-0 w-full rounded-t"
80
+ style={{
81
+ background: hot
82
+ ? `linear-gradient(to top, ${color}, rgba(255,255,255,0.3))`
83
+ : 'rgba(255,255,255,0.1)',
84
+ boxShadow: hot ? `0 0 6px ${color}66` : 'none',
85
+ }}
86
+ initial={{ height: 0 }}
87
+ animate={{ height: h }}
88
+ transition={{ duration: 0.6, delay: i * 0.02, ease: [0.22, 1, 0.36, 1] }}
89
+ />
90
+ </div>
91
+ )
92
+ })}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ // ── Audio card ────────────────────────────────────────────────────────────────
98
+ function AudioCard({ audio }: { audio: AudioResult }) {
99
+ if (!audio?.available) return null
100
+
101
+ const isAI = audio.result === 'AI_VOICE'
102
+ const color = isAI ? '#ff3355' : '#00ff88'
103
+ const colorDim = isAI ? 'rgba(255,51,85,0.12)' : 'rgba(0,255,136,0.12)'
104
+
105
+ return (
106
+ <motion.div
107
+ className="rounded-2xl border p-5"
108
+ style={{ borderColor: `${color}33`, background: colorDim }}
109
+ initial={{ opacity: 0, y: 16 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ delay: 0.4 }}
112
+ >
113
+ <div className="flex items-center gap-3 mb-4">
114
+ <div className="w-1 h-5 rounded-full" style={{ background: '#00aaff', boxShadow: '0 0 8px #00aaff' }} />
115
+ <span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Voice Authenticity</span>
116
+ </div>
117
+
118
+ <div className="flex items-center gap-3 mb-4">
119
+ <span
120
+ className="px-3 py-1.5 rounded-full text-xs font-bold tracking-wider border"
121
+ style={{ color, borderColor: `${color}55`, background: `${color}12` }}
122
+ >
123
+ {isAI ? '🤖 AI VOICE DETECTED' : '🎙️ HUMAN VOICE'}
124
+ </span>
125
+ <span className="text-2xl font-bold font-mono ml-auto" style={{ color }}>
126
+ {audio.confidence}%
127
+ </span>
128
+ </div>
129
+
130
+ {/* Bar */}
131
+ <div className="h-2 rounded-full bg-white/5 overflow-hidden mb-4">
132
+ <motion.div
133
+ className="h-full rounded-full"
134
+ style={{
135
+ background: isAI ? 'linear-gradient(90deg,#880022,#ff3355)' : 'linear-gradient(90deg,#00aaff,#00ff88)',
136
+ boxShadow: `0 0 10px ${color}66`,
137
+ }}
138
+ initial={{ width: 0 }}
139
+ animate={{ width: `${audio.confidence}%` }}
140
+ transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
141
+ />
142
+ </div>
143
+
144
+ {/* Scores */}
145
+ <div className="grid grid-cols-2 gap-3 mb-4">
146
+ {[
147
+ { label: 'Wav2Vec2 Model', val: `${audio.model_score}%` },
148
+ { label: 'Signal Heuristics', val: `${audio.heuristic_score}%` },
149
+ ].map(({ label, val }) => (
150
+ <div key={label} className="rounded-xl bg-white/3 border border-white/5 p-3 text-center">
151
+ <p className="text-[10px] text-white/40 tracking-wider mb-1">{label}</p>
152
+ <p className="text-lg font-bold font-mono text-white">{val}</p>
153
+ </div>
154
+ ))}
155
+ </div>
156
+
157
+ {/* Details */}
158
+ <div className="space-y-2">
159
+ {audio.details.map((d, i) => (
160
+ <motion.div
161
+ key={i}
162
+ className="flex items-start gap-2.5 text-xs text-white/60 bg-white/2 rounded-lg px-3 py-2 border-l-2"
163
+ style={{ borderLeftColor: color }}
164
+ initial={{ opacity: 0, x: -8 }}
165
+ animate={{ opacity: 1, x: 0 }}
166
+ transition={{ delay: 0.5 + i * 0.07 }}
167
+ >
168
+ <span className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1" style={{ background: color, boxShadow: `0 0 6px ${color}` }} />
169
+ {d}
170
+ </motion.div>
171
+ ))}
172
+ </div>
173
+
174
+ {/* Features */}
175
+ {audio.features && Object.keys(audio.features).length > 0 && (
176
+ <div className="mt-4 pt-4 border-t border-white/5">
177
+ <p className="text-[10px] text-white/30 tracking-widest uppercase mb-2">Signal Features</p>
178
+ <div className="grid grid-cols-2 gap-2">
179
+ {Object.entries(audio.features).map(([k, v]) => (
180
+ <div key={k} className="flex justify-between text-[10px]">
181
+ <span className="text-white/30 capitalize">{k.replace(/_/g, ' ')}</span>
182
+ <span className="text-white/70 font-mono">{typeof v === 'number' ? v.toFixed(3) : v}</span>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </div>
187
+ )}
188
+ </motion.div>
189
+ )
190
+ }
191
+
192
+ // ── Main result panel ─────────────────────────────────────────────────────────
193
+ export default function ResultPanel() {
194
+ const { result, reset } = useStore()
195
+ if (!result) return null
196
+
197
+ const isFake = result.result === 'FAKE'
198
+ const color = isFake ? '#ff3355' : '#00ff88'
199
+ const colorDim = isFake ? 'rgba(255,51,85,0.08)' : 'rgba(0,255,136,0.08)'
200
+
201
+ return (
202
+ <AnimatePresence>
203
+ <motion.div
204
+ className="w-full max-w-2xl mx-auto space-y-4"
205
+ initial={{ opacity: 0 }}
206
+ animate={{ opacity: 1 }}
207
+ exit={{ opacity: 0 }}
208
+ >
209
+ {/* Verdict card */}
210
+ <motion.div
211
+ className="rounded-2xl border-2 p-6"
212
+ style={{ borderColor: `${color}55`, background: colorDim, boxShadow: `0 0 50px ${color}15` }}
213
+ initial={{ scale: 0.95, opacity: 0 }}
214
+ animate={{ scale: 1, opacity: 1 }}
215
+ transition={{ type: 'spring', stiffness: 200, damping: 20 }}
216
+ >
217
+ <div className="flex flex-wrap items-center gap-6">
218
+ {/* Badge */}
219
+ <div className="text-center flex-shrink-0">
220
+ <motion.div
221
+ className="w-24 h-24 rounded-full border-2 flex items-center justify-center mx-auto mb-3 text-4xl"
222
+ style={{ borderColor: color, boxShadow: `0 0 30px ${color}44`, background: `${color}10` }}
223
+ animate={{ boxShadow: [`0 0 20px ${color}30`, `0 0 50px ${color}60`, `0 0 20px ${color}30`] }}
224
+ transition={{ duration: 2, repeat: Infinity }}
225
+ >
226
+ {isFake ? '⚠️' : '✅'}
227
+ </motion.div>
228
+ <motion.p
229
+ className="text-2xl font-bold tracking-widest"
230
+ style={{ color, textShadow: `0 0 20px ${color}66` }}
231
+ animate={isFake ? { textShadow: [`0 0 10px ${color}44`, `0 0 25px ${color}88`, `0 0 10px ${color}44`] } : {}}
232
+ transition={{ duration: 2.5, repeat: Infinity }}
233
+ >
234
+ {isFake ? 'DEEPFAKE' : 'AUTHENTIC'}
235
+ </motion.p>
236
+ <p className="text-[10px] text-white/30 tracking-widest mt-1">VERDICT</p>
237
+ </div>
238
+
239
+ {/* Confidence */}
240
+ <div className="flex-1 min-w-[200px]">
241
+ <p className="text-[10px] text-white/40 tracking-widest uppercase mb-3">Confidence Score</p>
242
+ <ConfidenceArc pct={result.confidence} isFake={isFake} />
243
+
244
+ {/* Risk pill */}
245
+ <div className="mt-4 flex items-center justify-between bg-white/3 rounded-xl px-4 py-3 border border-white/5">
246
+ <span className="text-[10px] text-white/40 tracking-wider uppercase">Risk Level</span>
247
+ <span
248
+ className="text-xs font-bold tracking-wider px-3 py-1 rounded-full border"
249
+ style={{
250
+ color: result.confidence < 35 ? '#00ff88' : result.confidence < 65 ? '#ffaa00' : '#ff3355',
251
+ borderColor: result.confidence < 35 ? '#00ff8844' : result.confidence < 65 ? '#ffaa0044' : '#ff335544',
252
+ background: result.confidence < 35 ? '#00ff8810' : result.confidence < 65 ? '#ffaa0010' : '#ff335510',
253
+ }}
254
+ >
255
+ {result.confidence < 35 ? 'LOW' : result.confidence < 65 ? 'MEDIUM' : result.confidence < 80 ? 'HIGH' : 'CRITICAL'}
256
+ </span>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </motion.div>
261
+
262
+ {/* Insights + Meta */}
263
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
264
+ {/* Insights */}
265
+ <div className="rounded-2xl border border-white/6 bg-white/2 p-5">
266
+ <div className="flex items-center gap-2 mb-4">
267
+ <div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
268
+ <span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Analysis Insights</span>
269
+ </div>
270
+ <div className="space-y-2">
271
+ {result.details.map((d, i) => (
272
+ <motion.div
273
+ key={i}
274
+ className="flex items-start gap-2.5 text-xs text-white/60 bg-white/2 rounded-lg px-3 py-2.5 border-l-2"
275
+ style={{ borderLeftColor: color }}
276
+ initial={{ opacity: 0, x: -10 }}
277
+ animate={{ opacity: 1, x: 0 }}
278
+ transition={{ delay: 0.1 + i * 0.07 }}
279
+ >
280
+ <span className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1" style={{ background: color }} />
281
+ {d}
282
+ </motion.div>
283
+ ))}
284
+ </div>
285
+ </div>
286
+
287
+ {/* Metadata */}
288
+ <div className="rounded-2xl border border-white/6 bg-white/2 p-5">
289
+ <div className="flex items-center gap-2 mb-4">
290
+ <div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
291
+ <span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Video Metadata</span>
292
+ </div>
293
+ <div className="space-y-2.5">
294
+ {[
295
+ ['Frames Analyzed', result.metadata.frames_analyzed],
296
+ ['Faces Detected', result.metadata.frames_with_faces],
297
+ ['Duration', `${result.metadata.video_duration_sec}s`],
298
+ ['FPS', result.metadata.video_fps],
299
+ ['Resolution', result.metadata.resolution],
300
+ ['Processing Time', `${result.processing_time_sec}s`],
301
+ ].map(([k, v]) => (
302
+ <div key={String(k)} className="flex justify-between items-center border-b border-white/4 pb-2 last:border-0 last:pb-0">
303
+ <span className="text-[11px] text-white/40">{k}</span>
304
+ <span className="text-[12px] font-bold font-mono text-white">{v ?? '—'}</span>
305
+ </div>
306
+ ))}
307
+ </div>
308
+ <div className="mt-4 pt-3 border-t border-white/5 flex items-center gap-2">
309
+ <div className="w-1.5 h-1.5 rounded-full bg-[#00ff88] shadow-[0_0_6px_#00ff88]" />
310
+ <span className="text-[10px] text-white/30 font-mono tracking-wider">ViT Ensemble · 2 Models</span>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ {/* Frame Timeline */}
316
+ <div className="rounded-2xl border border-white/6 bg-white/2 p-5">
317
+ <div className="flex items-center gap-2 mb-4">
318
+ <div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
319
+ <span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Frame Analysis Timeline</span>
320
+ <span className="text-[10px] text-white/20 font-mono ml-1">— {result.frame_timeline.length} frames</span>
321
+ </div>
322
+ <Timeline frames={result.frame_timeline} isFake={isFake} />
323
+ <div className="flex justify-between mt-2">
324
+ <span className="text-[10px] text-white/20 font-mono">Frame 0</span>
325
+ <span className="text-[10px] text-white/20 font-mono">Frame {result.frame_timeline.length}</span>
326
+ </div>
327
+ </div>
328
+
329
+ {/* Audio */}
330
+ {result.audio && <AudioCard audio={result.audio} />}
331
+
332
+ {/* Reset */}
333
+ <div className="text-center pt-2">
334
+ <motion.button
335
+ onClick={reset}
336
+ className="px-6 py-2.5 text-xs font-bold tracking-widest uppercase text-white/40 border border-white/10 rounded-xl hover:text-[#00ff88] hover:border-[#00ff8840] hover:bg-[#00ff8808] transition-all duration-200"
337
+ whileHover={{ scale: 1.02 }}
338
+ whileTap={{ scale: 0.98 }}
339
+ >
340
+ ↻ &nbsp;Analyze Another Video
341
+ </motion.button>
342
+ </div>
343
+ </motion.div>
344
+ </AnimatePresence>
345
+ )
346
+ }
frontend/src/components/UploadZone.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from 'react'
2
+ import { motion } from 'framer-motion'
3
+ import { useStore } from '../store'
4
+ import { analyzeVideo } from '../api'
5
+
6
+ const AGENTS = [
7
+ { icon: '🎬', label: 'Frame Extractor', sub: 'Sampling keyframes' },
8
+ { icon: '👤', label: 'Face Detector', sub: 'Isolating regions' },
9
+ { icon: '🧠', label: 'ViT Ensemble', sub: 'Neural inference' },
10
+ { icon: '📊', label: 'Report Builder', sub: 'Compiling results' },
11
+ ]
12
+
13
+ function fmtBytes(b: number) {
14
+ if (b < 1024) return `${b} B`
15
+ if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`
16
+ return `${(b / 1048576).toFixed(1)} MB`
17
+ }
18
+
19
+ export default function UploadZone() {
20
+ const { file, state, setFile, setState, setResult, setError, setAgentStep, reset } = useStore()
21
+ const inputRef = useRef<HTMLInputElement>(null)
22
+ const [dragging, setDragging] = useState(false)
23
+
24
+ const isLoading = state === 'loading'
25
+
26
+ function applyFile(f: File) {
27
+ if (f.type.startsWith('video/')) setFile(f)
28
+ else setError('Please select a valid video file (MP4, AVI, MOV, MKV, WebM).')
29
+ }
30
+
31
+ async function handleAnalyze() {
32
+ if (!file) return
33
+ setState('loading')
34
+ setAgentStep(0)
35
+
36
+ // Simulate agent steps
37
+ const delays = [0, 1800, 4200, 7500]
38
+ delays.forEach((d, i) => setTimeout(() => setAgentStep(i), d))
39
+
40
+ try {
41
+ const result = await analyzeVideo(file)
42
+ setResult(result)
43
+ } catch (e: any) {
44
+ setError(e.message || 'Analysis failed')
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="w-full max-w-xl mx-auto flex flex-col gap-5">
50
+ {/* Drop zone */}
51
+ <motion.div
52
+ className={`relative rounded-2xl border-2 border-dashed cursor-pointer transition-all duration-300 overflow-hidden
53
+ ${dragging ? 'border-[#00ff88] bg-[#00ff8808] shadow-[0_0_40px_#00ff8820]' : 'border-[#00ff8830] hover:border-[#00ff8866] hover:bg-[#00ff8806]'}
54
+ ${file ? 'py-6 px-8' : 'py-12 px-8'}`}
55
+ onClick={() => !file && inputRef.current?.click()}
56
+ onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
57
+ onDragLeave={() => setDragging(false)}
58
+ onDrop={(e) => {
59
+ e.preventDefault(); setDragging(false)
60
+ const f = e.dataTransfer.files[0]
61
+ if (f) applyFile(f)
62
+ }}
63
+ whileHover={{ scale: file ? 1 : 1.01 }}
64
+ whileTap={{ scale: 0.99 }}
65
+ >
66
+ <input
67
+ ref={inputRef}
68
+ type="file"
69
+ accept="video/*"
70
+ className="hidden"
71
+ onChange={(e) => e.target.files?.[0] && applyFile(e.target.files[0])}
72
+ />
73
+
74
+ {!file ? (
75
+ <motion.div className="text-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
76
+ {/* Orbit upload icon */}
77
+ <div className="relative w-20 h-20 mx-auto mb-5">
78
+ <motion.div
79
+ className="absolute inset-0 rounded-full border border-dashed border-[#00ff8840]"
80
+ animate={{ rotate: 360 }}
81
+ transition={{ duration: 8, repeat: Infinity, ease: 'linear' }}
82
+ />
83
+ <motion.div
84
+ className="absolute inset-3 rounded-full border border-[#00ff8820]"
85
+ animate={{ rotate: -360 }}
86
+ transition={{ duration: 5, repeat: Infinity, ease: 'linear' }}
87
+ />
88
+ <div className="absolute inset-0 flex items-center justify-center">
89
+ <svg width="28" height="28" fill="none" stroke="#00ff8899" strokeWidth="1.5" viewBox="0 0 24 24">
90
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/>
91
+ </svg>
92
+ </div>
93
+ </div>
94
+ <p className="text-white/80 font-semibold text-base mb-1">Drop video for forensic analysis</p>
95
+ <p className="text-white/40 text-sm">Drag & drop or click to browse</p>
96
+ <p className="text-white/20 text-xs mt-3 font-mono">MP4 · AVI · MOV · MKV · WebM — Max 100MB</p>
97
+ </motion.div>
98
+ ) : (
99
+ <motion.div
100
+ className="flex items-center gap-4"
101
+ initial={{ opacity: 0, y: 8 }}
102
+ animate={{ opacity: 1, y: 0 }}
103
+ >
104
+ <div className="w-12 h-12 rounded-xl bg-[#00ff8812] border border-[#00ff8830] flex items-center justify-center flex-shrink-0 shadow-[0_0_20px_#00ff8820]">
105
+ <svg width="22" height="22" fill="none" stroke="#00ff88" strokeWidth="1.5" viewBox="0 0 24 24">
106
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"/>
107
+ </svg>
108
+ </div>
109
+ <div className="flex-1 min-w-0">
110
+ <p className="text-white font-semibold text-sm truncate">{file.name}</p>
111
+ <p className="text-white/40 text-xs mt-0.5 font-mono">{fmtBytes(file.size)}</p>
112
+ </div>
113
+ <button
114
+ onClick={(e) => { e.stopPropagation(); reset() }}
115
+ className="p-2 text-white/30 hover:text-red-400 transition-colors rounded-lg hover:bg-red-400/10"
116
+ >
117
+ <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
118
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/>
119
+ </svg>
120
+ </button>
121
+ </motion.div>
122
+ )}
123
+ </motion.div>
124
+
125
+ {/* Analyze button */}
126
+ <motion.button
127
+ disabled={!file || isLoading}
128
+ onClick={handleAnalyze}
129
+ className={`w-full py-4 rounded-xl font-bold text-sm tracking-widest uppercase transition-all duration-300 relative overflow-hidden
130
+ ${file && !isLoading
131
+ ? 'bg-gradient-to-r from-[#00ff88] to-[#00cc66] text-[#050508] shadow-[0_0_30px_#00ff8840] cursor-pointer'
132
+ : 'bg-white/5 text-white/20 cursor-not-allowed border border-white/10'}`}
133
+ whileHover={file && !isLoading ? { scale: 1.02, boxShadow: '0 0 50px #00ff8860' } : {}}
134
+ whileTap={file && !isLoading ? { scale: 0.98 } : {}}
135
+ >
136
+ {/* Shimmer */}
137
+ {file && !isLoading && (
138
+ <motion.div
139
+ className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-12"
140
+ animate={{ x: ['-100%', '200%'] }}
141
+ transition={{ duration: 2.5, repeat: Infinity, ease: 'linear' }}
142
+ />
143
+ )}
144
+ <span className="relative z-10">
145
+ {isLoading ? '⏳ Analyzing...' : '▶ Analyze Video'}
146
+ </span>
147
+ </motion.button>
148
+
149
+ {/* Agent pipeline (loading state) */}
150
+ {isLoading && <AgentPipeline />}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ function AgentPipeline() {
156
+ const agentStep = useStore((s) => s.agentStep)
157
+
158
+ return (
159
+ <motion.div
160
+ initial={{ opacity: 0, y: 12 }}
161
+ animate={{ opacity: 1, y: 0 }}
162
+ className="grid grid-cols-2 gap-3"
163
+ >
164
+ {AGENTS.map((agent, i) => {
165
+ const active = i <= agentStep
166
+ const current = i === agentStep
167
+ return (
168
+ <motion.div
169
+ key={i}
170
+ className={`rounded-xl p-3 border transition-all duration-500 flex items-center gap-3
171
+ ${active
172
+ ? 'border-[#00ff8844] bg-[#00ff8808] shadow-[0_0_15px_#00ff8815]'
173
+ : 'border-white/5 bg-white/2'}`}
174
+ animate={current ? { scale: [1, 1.02, 1] } : {}}
175
+ transition={{ duration: 0.6, repeat: current ? Infinity : 0 }}
176
+ >
177
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 transition-all duration-500
178
+ ${active ? 'bg-[#00ff88] shadow-[0_0_8px_#00ff88]' : 'bg-white/15'}`} />
179
+ <div>
180
+ <p className="text-xs font-semibold text-white/80">{agent.label}</p>
181
+ <p className={`text-[10px] font-mono transition-colors duration-300 ${active ? 'text-[#00ff88]' : 'text-white/30'}`}>
182
+ {active ? (current ? agent.sub : 'Done ✓') : 'Waiting...'}
183
+ </p>
184
+ </div>
185
+ </motion.div>
186
+ )
187
+ })}
188
+ </motion.div>
189
+ )
190
+ }
frontend/src/index.css CHANGED
@@ -1,57 +1,43 @@
1
  @import "tailwindcss";
2
 
3
- :root {
4
- --primary: #00ff9c;
5
- --primary-dim: #00cc7d;
6
- --secondary: #00e5ff; /* soft cyan */
7
- --red: #ff4444;
8
- --red-dim: #cc2222;
9
- --bg-dark: #040906;
10
- --bg-gradient: radial-gradient(circle at top center, #001a11 0%, #040906 100%);
11
- --card-bg: rgba(10, 20, 15, 0.6);
12
- --card-bg-inner: rgba(15, 25, 20, 0.8);
13
- --border: rgba(0, 255, 156, 0.15);
14
- --border-hover: rgba(0, 255, 156, 0.4);
15
- --text: #e2e8f0;
16
- --muted: #849ca3;
17
- }
18
 
19
  body {
20
- background: var(--bg-dark);
21
- background-image: var(--bg-gradient);
22
- font-family: 'Inter', sans-serif;
23
- color: var(--text);
24
- min-height: 100vh;
25
- overflow-x: hidden;
26
- letter-spacing: 0.02em;
27
  }
28
 
29
- /* ── Scrollbar ── */
30
- ::-webkit-scrollbar { width: 6px; height: 6px; }
31
- ::-webkit-scrollbar-track { background: var(--bg-dark); }
32
- ::-webkit-scrollbar-thumb { background: rgba(0, 255, 156, 0.3); border-radius: 6px; }
33
 
34
- /* ── Scanner Background ── */
35
- .scanner {
36
- position: fixed; inset: 0; pointer-events: none; z-index: 100;
37
- background: linear-gradient(to bottom,
38
- transparent 0%, transparent 49.9%,
39
- rgba(0, 255, 156, 0.05) 50%, transparent 50.1%);
40
- background-size: 100% 4px;
41
- animation: scanMove 8s linear infinite;
42
- }
43
- @keyframes scanMove {
44
- 0% { background-position: 0 0; }
45
- 100% { background-position: 0 100vh; }
46
  }
47
 
48
- .text-glow {
49
- color: var(--primary);
50
- text-shadow: 0 0 10px rgba(0, 255, 156, 0.6);
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
- .fade-up { animation: fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
54
- @keyframes fadeUp {
55
- from { opacity: 0; transform: translateY(20px); }
56
- to { opacity: 1; transform: none; }
57
- }
 
1
  @import "tailwindcss";
2
 
3
+ * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  body {
6
+ background: #050508;
7
+ color: #e2e8f0;
8
+ font-family: 'Space Grotesk', 'Inter', sans-serif;
9
+ -webkit-font-smoothing: antialiased;
 
 
 
10
  }
11
 
12
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
13
+ ::-webkit-scrollbar-track { background: #050508; }
14
+ ::-webkit-scrollbar-thumb { background: rgba(0,255,136,0.2); border-radius: 4px; }
 
15
 
16
+ /* Animated grid background */
17
+ body::before {
18
+ content: '';
19
+ position: fixed;
20
+ inset: 0;
21
+ z-index: 0;
22
+ pointer-events: none;
23
+ background-image:
24
+ linear-gradient(rgba(0,255,136,0.025) 1px, transparent 1px),
25
+ linear-gradient(90deg, rgba(0,255,136,0.025) 1px, transparent 1px);
26
+ background-size: 60px 60px;
 
27
  }
28
 
29
+ /* Radial glow at top */
30
+ body::after {
31
+ content: '';
32
+ position: fixed;
33
+ top: -200px;
34
+ left: 50%;
35
+ transform: translateX(-50%);
36
+ width: 800px;
37
+ height: 600px;
38
+ background: radial-gradient(ellipse, rgba(0,255,136,0.06) 0%, transparent 70%);
39
+ pointer-events: none;
40
+ z-index: 0;
41
  }
42
 
43
+ main, header, footer { position: relative; z-index: 1; }
 
 
 
 
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/store.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand'
2
+
3
+ export type AppState = 'idle' | 'loading' | 'result' | 'error'
4
+ export type Verdict = 'FAKE' | 'REAL' | null
5
+
6
+ export interface FramePoint { frame: number; fake_pct: number }
7
+ export interface AudioResult {
8
+ available: boolean
9
+ result: string
10
+ confidence: number
11
+ fake_probability: number
12
+ model_score: number
13
+ heuristic_score: number
14
+ details: string[]
15
+ features: Record<string, number>
16
+ }
17
+ export interface AnalysisResult {
18
+ result: 'FAKE' | 'REAL'
19
+ confidence: number
20
+ details: string[]
21
+ frame_timeline: FramePoint[]
22
+ metadata: {
23
+ frames_analyzed: number
24
+ frames_with_faces: number
25
+ video_duration_sec: number
26
+ video_fps: number
27
+ resolution: string
28
+ }
29
+ processing_time_sec: number
30
+ audio: AudioResult
31
+ }
32
+
33
+ interface Store {
34
+ state: AppState
35
+ file: File | null
36
+ result: AnalysisResult | null
37
+ error: string | null
38
+ agentStep: number
39
+ setState: (s: AppState) => void
40
+ setFile: (f: File | null) => void
41
+ setResult: (r: AnalysisResult) => void
42
+ setError: (e: string) => void
43
+ setAgentStep: (n: number) => void
44
+ reset: () => void
45
+ }
46
+
47
+ export const useStore = create<Store>((set) => ({
48
+ state: 'idle',
49
+ file: null,
50
+ result: null,
51
+ error: null,
52
+ agentStep: -1,
53
+ setState: (state) => set({ state }),
54
+ setFile: (file) => set({ file }),
55
+ setResult: (result) => set({ result, state: 'result' }),
56
+ setError: (error) => set({ error, state: 'error' }),
57
+ setAgentStep: (agentStep) => set({ agentStep }),
58
+ reset: () => set({ state: 'idle', file: null, result: null, error: null, agentStep: -1 }),
59
+ }))
frontend/vite.config.js CHANGED
@@ -2,7 +2,17 @@ import { defineConfig } from 'vite'
2
  import react from '@vitejs/plugin-react'
3
  import tailwindcss from '@tailwindcss/vite'
4
 
5
- // https://vite.dev/config/
6
  export default defineConfig({
7
  plugins: [react(), tailwindcss()],
 
 
 
 
 
 
 
 
 
 
 
8
  })
 
2
  import react from '@vitejs/plugin-react'
3
  import tailwindcss from '@tailwindcss/vite'
4
 
 
5
  export default defineConfig({
6
  plugins: [react(), tailwindcss()],
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ '/analyze': 'http://localhost:8000',
11
+ '/health': 'http://localhost:8000',
12
+ },
13
+ },
14
+ build: {
15
+ outDir: '../frontend-dist',
16
+ emptyOutDir: true,
17
+ },
18
  })