MrTsp commited on
Commit
718a321
Β·
1 Parent(s): 84e98d0

Upload 3 files

Browse files
Files changed (3) hide show
  1. static/index.html +219 -0
  2. static/script.js +284 -0
  3. static/style.css +572 -0
static/index.html ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>DeepShield AI β€” Deepfake Detector</title>
7
+ <meta name="description" content="Upload a video and detect deepfakes instantly using DINO-G50 AI with confidence scores and per-frame analysis." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet" />
11
+ <link rel="stylesheet" href="style.css" />
12
+ </head>
13
+ <body>
14
+
15
+ <!-- Animated Background -->
16
+ <div class="bg-orbs">
17
+ <div class="orb orb-1"></div>
18
+ <div class="orb orb-2"></div>
19
+ <div class="orb orb-3"></div>
20
+ </div>
21
+
22
+ <!-- Navbar -->
23
+ <nav class="navbar">
24
+ <div class="nav-brand" onclick="location.reload()" style="cursor:pointer" title="Click to refresh">
25
+ <span class="brand-icon">πŸ›‘οΈ</span>
26
+ <span class="brand-name">DeepShield <span class="brand-accent">AI</span></span>
27
+ </div>
28
+ <div class="nav-right" style="display:flex; align-items:center; gap:16px;">
29
+ <div id="server-status" class="server-status status-checking">
30
+ <span class="status-dot"></span>
31
+ <span id="status-text">Checking API...</span>
32
+ </div>
33
+ <div class="nav-badge">DINO-G50 Powered</div>
34
+ </div>
35
+ </nav>
36
+
37
+ <!-- Hero -->
38
+ <header class="hero">
39
+ <div class="hero-tag">⚑ Real-time Detection</div>
40
+ <h1 class="hero-title">
41
+ Detect <span class="gradient-text">Deepfakes</span><br />Instantly
42
+ </h1>
43
+ <p class="hero-subtitle">
44
+ Upload any video. Our DINO-G50 Vision AI analyzes every frame<br />
45
+ and tells you exactly how likely a video is to be fake.
46
+ </p>
47
+ </header>
48
+
49
+ <!-- Main Card -->
50
+ <main class="main-card">
51
+
52
+ <!-- ── Upload Section ── -->
53
+ <section id="upload-section" class="upload-section">
54
+ <div
55
+ id="drop-zone"
56
+ class="drop-zone"
57
+ role="button"
58
+ tabindex="0"
59
+ aria-label="Upload video file"
60
+ ondragover="onDragOver(event)"
61
+ ondragleave="onDragLeave(event)"
62
+ ondrop="onDrop(event)"
63
+ onclick="document.getElementById('file-input').click()"
64
+ onkeypress="if(event.key==='Enter') document.getElementById('file-input').click()"
65
+ >
66
+ <div class="drop-icon">
67
+ <svg id="upload-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
68
+ <circle cx="32" cy="32" r="30" fill="rgba(139,92,246,0.12)" stroke="rgba(139,92,246,0.4)" stroke-width="1.5"/>
69
+ <path d="M32 44V28M32 28L24 36M32 28L40 36" stroke="url(#g1)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
70
+ <path d="M20 44h24" stroke="url(#g1)" stroke-width="2" stroke-linecap="round"/>
71
+ <defs>
72
+ <linearGradient id="g1" x1="20" y1="28" x2="44" y2="44" gradientUnits="userSpaceOnUse">
73
+ <stop stop-color="#8B5CF6"/><stop offset="1" stop-color="#06B6D4"/>
74
+ </linearGradient>
75
+ </defs>
76
+ </svg>
77
+ </div>
78
+ <p class="drop-title">Drop your video here</p>
79
+ <p class="drop-sub">or <span class="link-text">browse files</span></p>
80
+ <p class="drop-limits">MP4 Β· MOV Β· AVI Β· MKV &nbsp;|&nbsp; Max 30 MB &nbsp;|&nbsp; Max 60 sec</p>
81
+ </div>
82
+ <input
83
+ id="file-input"
84
+ type="file"
85
+ accept=".mp4,.mov,.avi,.mkv,video/*"
86
+ style="display:none"
87
+ onchange="onFileSelected(event)"
88
+ />
89
+
90
+ <!-- File Preview -->
91
+ <div id="file-preview" class="file-preview hidden">
92
+ <div class="file-info">
93
+ <span class="file-icon">🎬</span>
94
+ <div class="file-meta">
95
+ <span id="file-name" class="file-name"></span>
96
+ <span id="file-size" class="file-size"></span>
97
+ </div>
98
+ <button class="remove-btn" onclick="resetUpload()" aria-label="Remove file">βœ•</button>
99
+ </div>
100
+ <div id="video-container" class="video-container">
101
+ <video id="video-preview" class="video-preview" controls muted></video>
102
+ </div>
103
+ <button id="analyze-btn" class="analyze-btn" onclick="analyzeVideo()">
104
+ <span class="btn-icon">πŸ”</span>
105
+ <span>Analyze for Deepfakes</span>
106
+ </button>
107
+ </div>
108
+ </section>
109
+
110
+ <!-- ── Loading Section ── -->
111
+ <section id="loading-section" class="loading-section hidden">
112
+ <div class="loading-animation">
113
+ <div class="spinner-ring"></div>
114
+ <div class="spinner-ring ring-2"></div>
115
+ <div class="spinner-ring ring-3"></div>
116
+ <div class="spinner-center">πŸ€–</div>
117
+ </div>
118
+ <h3 class="loading-title">Analyzing Video...</h3>
119
+ <div class="loading-steps">
120
+ <div id="step-1" class="step active">
121
+ <span class="step-dot"></span>
122
+ <span>Extracting frames</span>
123
+ </div>
124
+ <div id="step-2" class="step">
125
+ <span class="step-dot"></span>
126
+ <span>Running DINOv2 inference</span>
127
+ </div>
128
+ <div id="step-3" class="step">
129
+ <span class="step-dot"></span>
130
+ <span>Generating results</span>
131
+ </div>
132
+ </div>
133
+ <p class="loading-note">⏳ This may take 30–90 seconds on CPU. Please wait…</p>
134
+ </section>
135
+
136
+ <!-- ── Results Section ── -->
137
+ <section id="results-section" class="results-section hidden">
138
+
139
+ <!-- Verdict Card -->
140
+ <div id="verdict-card" class="verdict-card">
141
+ <div class="verdict-left">
142
+ <div class="verdict-circle-wrap">
143
+ <svg class="verdict-ring" viewBox="0 0 120 120">
144
+ <circle cx="60" cy="60" r="50" class="ring-bg"/>
145
+ <circle id="ring-fill" cx="60" cy="60" r="50" class="ring-progress"/>
146
+ </svg>
147
+ <div class="verdict-inner">
148
+ <span id="verdict-pct" class="verdict-pct">0%</span>
149
+ <span id="verdict-label" class="verdict-label">FAKE</span>
150
+ </div>
151
+ </div>
152
+ <p class="verdict-desc">Probability of being fake</p>
153
+ </div>
154
+
155
+ <div class="verdict-right">
156
+ <div id="verdict-badge" class="verdict-badge">⚠ FAKE</div>
157
+ <div class="verdict-stats">
158
+ <div class="stat-row">
159
+ <span class="stat-label">🎭 Fake probability</span>
160
+ <span id="stat-fake" class="stat-val fake-val">β€”</span>
161
+ </div>
162
+ <div class="stat-row">
163
+ <span class="stat-label">βœ… Real probability</span>
164
+ <span id="stat-real" class="stat-val real-val">β€”</span>
165
+ </div>
166
+ <div class="stat-row">
167
+ <span class="stat-label">🎞 Frames analyzed</span>
168
+ <span id="stat-frames" class="stat-val">β€”</span>
169
+ </div>
170
+ <div class="stat-row">
171
+ <span class="stat-label">πŸ“ File size</span>
172
+ <span id="stat-size" class="stat-val">β€”</span>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ <!-- Per-Frame Chart -->
179
+ <div class="chart-card">
180
+ <h3 class="chart-title">πŸ“Š Per-Frame Detection Scores</h3>
181
+ <p class="chart-sub">Higher bar = more likely FAKE for that frame</p>
182
+ <div id="frame-chart" class="frame-chart"></div>
183
+ <div class="chart-legend">
184
+ <span class="legend-item"><span class="dot dot-fake"></span>Fake</span>
185
+ <span class="legend-item"><span class="dot dot-real"></span>Real</span>
186
+ <span class="legend-item"><span class="dot dot-thresh"></span>50% Threshold</span>
187
+ </div>
188
+ </div>
189
+
190
+ <!-- Actions -->
191
+ <div class="result-actions">
192
+ <button class="action-btn action-primary" onclick="resetUpload()">
193
+ πŸ”„ Analyze Another Video
194
+ </button>
195
+ <button class="action-btn action-secondary" onclick="copyResult()">
196
+ πŸ“‹ Copy Result
197
+ </button>
198
+ </div>
199
+ </section>
200
+
201
+ <!-- ── Error Section ── -->
202
+ <section id="error-section" class="error-section hidden">
203
+ <div class="error-icon">❌</div>
204
+ <h3 class="error-title">Analysis Failed</h3>
205
+ <p id="error-msg" class="error-msg"></p>
206
+ <button class="action-btn action-primary" onclick="resetUpload()">Try Again</button>
207
+ </section>
208
+
209
+ </main>
210
+
211
+ <!-- Footer -->
212
+ <footer class="footer">
213
+ <p><strong>MADE BY G50</strong></p>
214
+ <p class="footer-note">All Rights Reserved Β© G50 &nbsp;Β·&nbsp; Max 30 MB Β· 60 sec Β· Results are probabilistic, not legal evidence</p>
215
+ </footer>
216
+
217
+ <script src="script.js"></script>
218
+ </body>
219
+ </html>
static/script.js ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * DeepShield AI β€” script.js (Full-Stack HF Space version)
3
+ * API_URL = "" means: same server, same domain. No CORS needed!
4
+ */
5
+ const API_URL = ""; // Empty = same HF Space serves both UI and API
6
+
7
+ // ── Server Status Ping ──
8
+ async function checkServerStatus() {
9
+ const statusMenu = document.getElementById("server-status");
10
+ const statusText = document.getElementById("status-text");
11
+ const dropZone = document.getElementById("drop-zone");
12
+
13
+ if (!statusMenu) return;
14
+
15
+ try {
16
+ const res = await fetch(`${API_URL}/health`);
17
+ if (!res.ok) throw new Error("Server not OK");
18
+ const data = await res.json();
19
+
20
+ statusMenu.className = "server-status";
21
+
22
+ if (data.model_loaded === true) {
23
+ statusMenu.classList.add("status-connected");
24
+ statusText.textContent = "AI Ready βœ“";
25
+
26
+ dropZone.style.pointerEvents = "auto";
27
+ dropZone.style.opacity = "1";
28
+ document.querySelector(".drop-title").innerHTML = "Drop your video here";
29
+ document.querySelector(".drop-sub").innerHTML = 'or <span class="link-text">browse files</span>';
30
+ } else {
31
+ statusMenu.classList.add("status-error");
32
+ statusText.textContent = "Model Missing";
33
+
34
+ dropZone.style.pointerEvents = "none";
35
+ dropZone.style.opacity = "0.5";
36
+ document.querySelector(".drop-title").innerHTML = "⚠️ Model Not Uploaded";
37
+ document.querySelector(".drop-sub").textContent = "Admin: Upload best_model.pth to this HF Space.";
38
+ }
39
+ } catch (err) {
40
+ statusMenu.className = "server-status status-error";
41
+ statusText.textContent = "Server Waking Up...";
42
+
43
+ dropZone.style.pointerEvents = "none";
44
+ dropZone.style.opacity = "0.5";
45
+ document.querySelector(".drop-title").innerHTML = "⚠️ Server is starting...";
46
+ document.querySelector(".drop-sub").textContent = "Takes ~60 sec. Page will auto-refresh status.";
47
+ }
48
+ }
49
+
50
+ // Check on load, then every 10 seconds (also keeps server alive!)
51
+ checkServerStatus();
52
+ setInterval(checkServerStatus, 10000);
53
+
54
+ const MAX_FILE_MB = 30;
55
+ const MAX_FILE_BYTES = MAX_FILE_MB * 1024 * 1024;
56
+
57
+ let currentFile = null;
58
+ let lastResult = null;
59
+
60
+ function showSection(id) {
61
+ const sections = ["upload-section", "loading-section", "results-section", "error-section"];
62
+ sections.forEach(s => {
63
+ document.getElementById(s).classList.toggle("hidden", s !== id);
64
+ });
65
+ }
66
+
67
+ function onDragOver(e) {
68
+ e.preventDefault();
69
+ document.getElementById("drop-zone").classList.add("dragging");
70
+ }
71
+
72
+ function onDragLeave() {
73
+ document.getElementById("drop-zone").classList.remove("dragging");
74
+ }
75
+
76
+ function onDrop(e) {
77
+ e.preventDefault();
78
+ document.getElementById("drop-zone").classList.remove("dragging");
79
+ const file = e.dataTransfer?.files?.[0];
80
+ if (file) processFile(file);
81
+ }
82
+
83
+ function onFileSelected(e) {
84
+ const file = e.target.files?.[0];
85
+ if (file) processFile(file);
86
+ }
87
+
88
+ function processFile(file) {
89
+ const allowedExt = [".mp4", ".mov", ".avi", ".mkv"];
90
+ const ext = "." + file.name.split(".").pop().toLowerCase();
91
+ if (!allowedExt.includes(ext)) {
92
+ showError(`❌ Unsupported file type: "${ext}". Please upload MP4, MOV, AVI, or MKV.`);
93
+ return;
94
+ }
95
+
96
+ if (file.size > MAX_FILE_BYTES) {
97
+ const sizeMB = (file.size / 1024 / 1024).toFixed(1);
98
+ showError(`❌ File too large (${sizeMB} MB). Maximum allowed: ${MAX_FILE_MB} MB.`);
99
+ return;
100
+ }
101
+
102
+ currentFile = file;
103
+ document.getElementById("file-name").textContent = file.name;
104
+ document.getElementById("file-size").textContent = formatBytes(file.size);
105
+
106
+ const url = URL.createObjectURL(file);
107
+ document.getElementById("video-preview").src = url;
108
+
109
+ document.getElementById("drop-zone").classList.add("hidden");
110
+ document.getElementById("file-preview").classList.remove("hidden");
111
+ }
112
+
113
+ function resetUpload() {
114
+ currentFile = null;
115
+ lastResult = null;
116
+
117
+ document.getElementById("file-input").value = "";
118
+
119
+ const video = document.getElementById("video-preview");
120
+ if (video.src) URL.revokeObjectURL(video.src);
121
+ video.src = "";
122
+
123
+ document.getElementById("ring-fill").style.strokeDashoffset = "314";
124
+ document.getElementById("frame-chart").innerHTML = "";
125
+
126
+ document.getElementById("drop-zone").classList.remove("hidden");
127
+ document.getElementById("file-preview").classList.add("hidden");
128
+ showSection("upload-section");
129
+
130
+ const btn = document.getElementById("analyze-btn");
131
+ if (btn) {
132
+ btn.disabled = false;
133
+ btn.innerHTML = '<span class="btn-icon">πŸ”</span><span>Analyze for Deepfakes</span>';
134
+ }
135
+ }
136
+
137
+ async function analyzeVideo() {
138
+ if (!currentFile) return;
139
+
140
+ const btn = document.getElementById("analyze-btn");
141
+ btn.disabled = true;
142
+ btn.innerHTML = '<span class="btn-icon">⏳</span><span>Uploading...</span>';
143
+
144
+ showSection("loading-section");
145
+ animateLoadingSteps();
146
+
147
+ const formData = new FormData();
148
+ formData.append("file", currentFile);
149
+
150
+ try {
151
+ const response = await fetch(`${API_URL}/predict`, {
152
+ method: "POST",
153
+ body: formData,
154
+ });
155
+
156
+ if (!response.ok) {
157
+ const errData = await response.json().catch(() => ({ detail: "Unknown server error." }));
158
+ throw new Error(errData.detail || `Server error: ${response.status}`);
159
+ }
160
+
161
+ const data = await response.json();
162
+ lastResult = data;
163
+ renderResults(data);
164
+ showSection("results-section");
165
+
166
+ } catch (err) {
167
+ console.error("Analysis failed:", err);
168
+ let msg = err.message || "Analysis failed.";
169
+ if (msg.includes("fetch") || msg.includes("NetworkError") || msg.includes("Failed to fetch")) {
170
+ msg = "⚠️ Cannot reach the AI server. The server might be waking up. Please wait ~60 sec and try again.";
171
+ }
172
+ showError(msg);
173
+ }
174
+ }
175
+
176
+ function renderResults(data) {
177
+ const isFake = data.verdict === "FAKE";
178
+ const fakePct = data.fake_probability;
179
+ const realPct = data.real_probability;
180
+
181
+ const card = document.getElementById("verdict-card");
182
+ card.classList.remove("is-fake", "is-real");
183
+ card.classList.add(isFake ? "is-fake" : "is-real");
184
+
185
+ const ring = document.getElementById("ring-fill");
186
+ const circumference = 314;
187
+ ring.style.stroke = isFake ? "#ef4444" : "#22c55e";
188
+ setTimeout(() => {
189
+ ring.style.strokeDashoffset = circumference - (fakePct / 100) * circumference;
190
+ }, 100);
191
+
192
+ document.getElementById("verdict-pct").textContent = `${fakePct}%`;
193
+ const lbl = document.getElementById("verdict-label");
194
+ lbl.textContent = data.verdict;
195
+ lbl.style.color = isFake ? "#f87171" : "#4ade80";
196
+
197
+ const badge = document.getElementById("verdict-badge");
198
+ badge.textContent = isFake ? "⚠️ FAKE DETECTED" : "βœ… REAL VIDEO";
199
+ badge.className = "verdict-badge " + (isFake ? "fake" : "real");
200
+
201
+ document.getElementById("stat-fake").textContent = `${fakePct}%`;
202
+ document.getElementById("stat-real").textContent = `${realPct}%`;
203
+ document.getElementById("stat-frames").textContent = `${data.frame_count} frames`;
204
+ document.getElementById("stat-size").textContent = `${data.file_size_mb} MB`;
205
+
206
+ renderFrameChart(data.per_frame_scores || []);
207
+ }
208
+
209
+ function renderFrameChart(scores) {
210
+ const container = document.getElementById("frame-chart");
211
+ container.innerHTML = "";
212
+
213
+ if (!scores.length) {
214
+ container.innerHTML = '<p style="color:var(--text-sub);font-size:13px;padding:20px 0;">No per-frame data.</p>';
215
+ return;
216
+ }
217
+
218
+ scores.forEach((score, i) => {
219
+ const isFakeBar = score > 50;
220
+ const wrap = document.createElement("div");
221
+ wrap.className = "bar-wrap";
222
+
223
+ const bar = document.createElement("div");
224
+ bar.className = `bar ${isFakeBar ? "bar-fake" : "bar-real"}`;
225
+ bar.style.height = "0%";
226
+ bar.setAttribute("data-tip", `Frame ${i + 1}: ${score}%`);
227
+
228
+ wrap.appendChild(bar);
229
+ container.appendChild(wrap);
230
+
231
+ setTimeout(() => { bar.style.height = `${Math.max(4, score)}%`; }, 100 + i * 30);
232
+ });
233
+ }
234
+
235
+ function showError(msg) {
236
+ document.getElementById("error-msg").textContent = msg;
237
+ showSection("error-section");
238
+ }
239
+
240
+ function animateLoadingSteps() {
241
+ const steps = ["step-1", "step-2", "step-3"];
242
+ steps.forEach(s => {
243
+ document.getElementById(s).classList.remove("active", "done");
244
+ });
245
+
246
+ let i = 0;
247
+ function next() {
248
+ if (i > 0) {
249
+ document.getElementById(steps[i - 1]).classList.remove("active");
250
+ document.getElementById(steps[i - 1]).classList.add("done");
251
+ }
252
+ if (i < steps.length) {
253
+ document.getElementById(steps[i]).classList.add("active");
254
+ i++;
255
+ setTimeout(next, i < steps.length ? 4000 : 99999);
256
+ }
257
+ }
258
+ next();
259
+ }
260
+
261
+ function copyResult() {
262
+ if (!lastResult) return;
263
+ const { verdict, fake_probability, real_probability, frame_count, filename } = lastResult;
264
+ const text =
265
+ `DeepShield AI β€” DINO-G50 Result\n` +
266
+ `File: ${filename}\n` +
267
+ `Verdict: ${verdict}\n` +
268
+ `Fake: ${fake_probability}% | Real: ${real_probability}%\n` +
269
+ `Frames Analyzed: ${frame_count}`;
270
+
271
+ navigator.clipboard?.writeText(text).then(() => {
272
+ const btn = document.querySelector(".action-secondary");
273
+ if (btn) {
274
+ const orig = btn.textContent;
275
+ btn.textContent = "βœ… Copied!";
276
+ setTimeout(() => { btn.textContent = orig; }, 2000);
277
+ }
278
+ }).catch(() => alert(text));
279
+ }
280
+
281
+ function formatBytes(bytes) {
282
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
283
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
284
+ }
static/style.css ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ──────────────────────────────────────────
2
+ DeepShield AI β€” style.css
3
+ Premium dark glassmorphism design
4
+ ────────────────────────────────────────── */
5
+
6
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
7
+
8
+ :root {
9
+ --bg: #08080f;
10
+ --surface: rgba(255,255,255,0.04);
11
+ --surface-2: rgba(255,255,255,0.07);
12
+ --border: rgba(255,255,255,0.08);
13
+ --border-glow: rgba(139,92,246,0.35);
14
+ --text: #f0f0ff;
15
+ --text-sub: rgba(240,240,255,0.55);
16
+ --purple: #8B5CF6;
17
+ --cyan: #06B6D4;
18
+ --green: #22c55e;
19
+ --red: #ef4444;
20
+ --orange: #f97316;
21
+ --radius: 18px;
22
+ --radius-sm: 10px;
23
+ --transition: 0.3s cubic-bezier(0.4,0,0.2,1);
24
+ }
25
+
26
+ html { scroll-behavior: smooth; }
27
+
28
+ body {
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: 'Inter', sans-serif;
32
+ font-size: 15px;
33
+ line-height: 1.6;
34
+ min-height: 100vh;
35
+ overflow-x: hidden;
36
+ }
37
+
38
+ /* ── Background Orbs ── */
39
+ .bg-orbs { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
40
+ .orb {
41
+ position: absolute;
42
+ border-radius: 50%;
43
+ filter: blur(80px);
44
+ opacity: 0.18;
45
+ animation: drift 12s ease-in-out infinite alternate;
46
+ }
47
+ .orb-1 { width: 500px; height: 500px; background: var(--purple); top: -150px; left: -100px; animation-delay: 0s; }
48
+ .orb-2 { width: 400px; height: 400px; background: var(--cyan); bottom: -100px; right: -80px; animation-delay: -5s; }
49
+ .orb-3 { width: 300px; height: 300px; background: #ec4899; top: 40%; left: 60%; animation-delay: -9s; }
50
+
51
+ @keyframes drift {
52
+ from { transform: translate(0, 0) scale(1); }
53
+ to { transform: translate(30px, 20px) scale(1.08); }
54
+ }
55
+
56
+ /* ── Navbar ── */
57
+ .navbar {
58
+ position: sticky;
59
+ top: 0;
60
+ z-index: 100;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ padding: 14px 32px;
65
+ background: rgba(8,8,15,0.85);
66
+ backdrop-filter: blur(20px);
67
+ border-bottom: 1px solid var(--border);
68
+ }
69
+ .nav-brand { display: flex; align-items: center; gap: 10px; }
70
+ .brand-icon { font-size: 24px; }
71
+ .brand-name {
72
+ font-family: 'Space Grotesk', sans-serif;
73
+ font-size: 20px;
74
+ font-weight: 700;
75
+ color: var(--text);
76
+ }
77
+ .brand-accent { color: var(--purple); }
78
+ .nav-badge {
79
+ font-size: 11px;
80
+ font-weight: 600;
81
+ padding: 4px 12px;
82
+ border-radius: 99px;
83
+ background: rgba(139,92,246,0.15);
84
+ border: 1px solid rgba(139,92,246,0.3);
85
+ color: var(--purple);
86
+ letter-spacing: 0.5px;
87
+ text-transform: uppercase;
88
+ }
89
+
90
+ /* ── Server Status Badge ── */
91
+ .server-status {
92
+ display: flex; align-items: center; gap: 8px;
93
+ font-size: 12px; font-weight: 600;
94
+ padding: 4px 12px; border-radius: 99px;
95
+ background: var(--surface-2); border: 1px solid var(--border);
96
+ transition: all 0.3s;
97
+ }
98
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; }
99
+ .status-checking { color: #facc15; border-color: rgba(250,204,21,0.3); background: rgba(250,204,21,0.1); }
100
+ .status-checking .status-dot { background: #facc15; animation: pulse-dot 1s infinite alternate; }
101
+ .status-connected { color: #4ade80; border-color: rgba(74,222,128,0.3); background: rgba(74,222,128,0.1); }
102
+ .status-connected .status-dot { background: #4ade80; box-shadow: 0 0 8px #4ade80; }
103
+ .status-error { color: #f87171; border-color: rgba(248,113,113,0.3); background: rgba(248,113,113,0.1); }
104
+ .status-error .status-dot { background: #f87171; box-shadow: 0 0 8px #f87171; }
105
+
106
+
107
+ /* ── Hero ── */
108
+ .hero {
109
+ text-align: center;
110
+ padding: 60px 24px 36px;
111
+ position: relative;
112
+ z-index: 1;
113
+ }
114
+ .hero-tag {
115
+ display: inline-block;
116
+ font-size: 12px;
117
+ font-weight: 600;
118
+ text-transform: uppercase;
119
+ letter-spacing: 1.5px;
120
+ padding: 6px 16px;
121
+ border-radius: 99px;
122
+ background: rgba(6,182,212,0.1);
123
+ border: 1px solid rgba(6,182,212,0.25);
124
+ color: var(--cyan);
125
+ margin-bottom: 20px;
126
+ }
127
+ .hero-title {
128
+ font-family: 'Space Grotesk', sans-serif;
129
+ font-size: clamp(36px, 6vw, 64px);
130
+ font-weight: 700;
131
+ line-height: 1.1;
132
+ margin-bottom: 18px;
133
+ letter-spacing: -1px;
134
+ }
135
+ .gradient-text {
136
+ background: linear-gradient(135deg, var(--purple), var(--cyan));
137
+ -webkit-background-clip: text;
138
+ -webkit-text-fill-color: transparent;
139
+ background-clip: text;
140
+ }
141
+ .hero-subtitle {
142
+ color: var(--text-sub);
143
+ font-size: 17px;
144
+ max-width: 540px;
145
+ margin: 0 auto;
146
+ line-height: 1.7;
147
+ }
148
+
149
+ /* ── Main Card ── */
150
+ .main-card {
151
+ position: relative;
152
+ z-index: 1;
153
+ max-width: 820px;
154
+ margin: 0 auto 60px;
155
+ padding: 40px 32px;
156
+ background: var(--surface);
157
+ border: 1px solid var(--border);
158
+ border-radius: 28px;
159
+ backdrop-filter: blur(24px);
160
+ box-shadow: 0 32px 80px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03) inset;
161
+ }
162
+
163
+ /* ── Drop Zone ── */
164
+ .drop-zone {
165
+ display: flex;
166
+ flex-direction: column;
167
+ align-items: center;
168
+ justify-content: center;
169
+ gap: 10px;
170
+ padding: 52px 32px;
171
+ border: 2px dashed rgba(139,92,246,0.3);
172
+ border-radius: var(--radius);
173
+ background: rgba(139,92,246,0.04);
174
+ cursor: pointer;
175
+ transition: all var(--transition);
176
+ outline: none;
177
+ }
178
+ .drop-zone:hover, .drop-zone:focus, .drop-zone.dragging {
179
+ border-color: var(--purple);
180
+ background: rgba(139,92,246,0.1);
181
+ box-shadow: 0 0 0 4px rgba(139,92,246,0.12);
182
+ transform: translateY(-2px);
183
+ }
184
+ .drop-icon svg { width: 64px; height: 64px; }
185
+ .drop-title { font-size: 19px; font-weight: 600; color: var(--text); }
186
+ .drop-sub { color: var(--text-sub); }
187
+ .link-text { color: var(--purple); text-decoration: underline; cursor: pointer; }
188
+ .drop-limits {
189
+ font-size: 12px;
190
+ color: var(--text-sub);
191
+ opacity: 0.7;
192
+ margin-top: 4px;
193
+ letter-spacing: 0.3px;
194
+ }
195
+
196
+ /* ── File Preview ── */
197
+ .file-preview { display: flex; flex-direction: column; gap: 18px; margin-top: 20px; }
198
+ .file-info {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 12px;
202
+ padding: 14px 18px;
203
+ background: var(--surface-2);
204
+ border: 1px solid var(--border);
205
+ border-radius: var(--radius-sm);
206
+ }
207
+ .file-icon { font-size: 28px; }
208
+ .file-meta { flex: 1; display: flex; flex-direction: column; }
209
+ .file-name { font-weight: 600; font-size: 14px; color: var(--text); word-break: break-all; }
210
+ .file-size { font-size: 12px; color: var(--text-sub); }
211
+ .remove-btn {
212
+ background: none;
213
+ border: 1px solid var(--border);
214
+ color: var(--text-sub);
215
+ width: 32px; height: 32px;
216
+ border-radius: 50%;
217
+ font-size: 14px;
218
+ cursor: pointer;
219
+ transition: all var(--transition);
220
+ display: flex; align-items: center; justify-content: center;
221
+ }
222
+ .remove-btn:hover { background: rgba(239,68,68,0.1); border-color: var(--red); color: var(--red); }
223
+
224
+ .video-container {
225
+ border-radius: var(--radius-sm);
226
+ overflow: hidden;
227
+ background: #000;
228
+ border: 1px solid var(--border);
229
+ }
230
+ .video-preview { width: 100%; max-height: 300px; display: block; }
231
+
232
+ .analyze-btn {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ gap: 10px;
237
+ width: 100%;
238
+ padding: 16px;
239
+ font-size: 16px;
240
+ font-weight: 700;
241
+ border: none;
242
+ border-radius: var(--radius-sm);
243
+ background: linear-gradient(135deg, var(--purple), var(--cyan));
244
+ color: #fff;
245
+ cursor: pointer;
246
+ transition: all var(--transition);
247
+ letter-spacing: 0.3px;
248
+ box-shadow: 0 8px 24px rgba(139,92,246,0.35);
249
+ }
250
+ .analyze-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(139,92,246,0.45); }
251
+ .analyze-btn:active { transform: translateY(0); }
252
+ .analyze-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
253
+ .btn-icon { font-size: 18px; }
254
+
255
+ /* ── Loading ── */
256
+ .loading-section {
257
+ display: flex;
258
+ flex-direction: column;
259
+ align-items: center;
260
+ gap: 24px;
261
+ padding: 40px 0;
262
+ }
263
+ .loading-animation {
264
+ position: relative;
265
+ width: 100px;
266
+ height: 100px;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ }
271
+ .spinner-ring {
272
+ position: absolute;
273
+ inset: 0;
274
+ border-radius: 50%;
275
+ border: 3px solid transparent;
276
+ border-top-color: var(--purple);
277
+ animation: spin 1s linear infinite;
278
+ }
279
+ .ring-2 {
280
+ inset: 8px;
281
+ border-top-color: var(--cyan);
282
+ animation-duration: 1.4s;
283
+ animation-direction: reverse;
284
+ }
285
+ .ring-3 {
286
+ inset: 16px;
287
+ border-top-color: rgba(139,92,246,0.4);
288
+ animation-duration: 2s;
289
+ }
290
+ .spinner-center { font-size: 28px; z-index: 1; }
291
+ @keyframes spin { to { transform: rotate(360deg); } }
292
+
293
+ .loading-title { font-size: 22px; font-weight: 700; text-align: center; }
294
+ .loading-steps { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 280px; }
295
+ .step {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 12px;
299
+ font-size: 14px;
300
+ color: var(--text-sub);
301
+ transition: all var(--transition);
302
+ }
303
+ .step.active { color: var(--text); }
304
+ .step-dot {
305
+ width: 8px; height: 8px;
306
+ border-radius: 50%;
307
+ background: var(--border);
308
+ transition: all var(--transition);
309
+ flex-shrink: 0;
310
+ }
311
+ .step.active .step-dot {
312
+ background: var(--purple);
313
+ box-shadow: 0 0 8px var(--purple);
314
+ animation: pulse-dot 1s ease-in-out infinite;
315
+ }
316
+ .step.done .step-dot { background: var(--green); animation: none; }
317
+ .step.done { color: var(--green); }
318
+ @keyframes pulse-dot { 0%,100% { transform: scale(1); } 50% { transform: scale(1.4); } }
319
+ .loading-note { font-size: 12px; color: var(--text-sub); text-align: center; opacity: 0.7; }
320
+
321
+ /* ── Results ── */
322
+ .results-section { display: flex; flex-direction: column; gap: 24px; }
323
+
324
+ .verdict-card {
325
+ display: flex;
326
+ gap: 32px;
327
+ padding: 28px;
328
+ border-radius: var(--radius);
329
+ border: 1px solid var(--border-glow);
330
+ background: var(--surface-2);
331
+ align-items: center;
332
+ flex-wrap: wrap;
333
+ }
334
+ .verdict-card.is-fake { border-color: rgba(239,68,68,0.4); background: rgba(239,68,68,0.04); }
335
+ .verdict-card.is-real { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.04); }
336
+
337
+ .verdict-left {
338
+ display: flex;
339
+ flex-direction: column;
340
+ align-items: center;
341
+ gap: 10px;
342
+ min-width: 160px;
343
+ }
344
+ .verdict-circle-wrap { position: relative; width: 140px; height: 140px; }
345
+ .verdict-ring { width: 100%; height: 100%; transform: rotate(-90deg); }
346
+ .ring-bg { fill: none; stroke: rgba(255,255,255,0.06); stroke-width: 8; }
347
+ .ring-progress {
348
+ fill: none;
349
+ stroke: var(--purple);
350
+ stroke-width: 8;
351
+ stroke-linecap: round;
352
+ stroke-dasharray: 314;
353
+ stroke-dashoffset: 314;
354
+ transition: stroke-dashoffset 1.2s cubic-bezier(0.4,0,0.2,1), stroke 0.4s;
355
+ }
356
+ .verdict-inner {
357
+ position: absolute;
358
+ inset: 0;
359
+ display: flex;
360
+ flex-direction: column;
361
+ align-items: center;
362
+ justify-content: center;
363
+ }
364
+ .verdict-pct {
365
+ font-family: 'Space Grotesk', sans-serif;
366
+ font-size: 30px;
367
+ font-weight: 700;
368
+ line-height: 1;
369
+ }
370
+ .verdict-label {
371
+ font-size: 13px;
372
+ font-weight: 700;
373
+ letter-spacing: 2px;
374
+ text-transform: uppercase;
375
+ margin-top: 2px;
376
+ }
377
+ .verdict-desc { font-size: 12px; color: var(--text-sub); text-align: center; }
378
+
379
+ .verdict-right { flex: 1; min-width: 220px; }
380
+ .verdict-badge {
381
+ display: inline-block;
382
+ font-size: 20px;
383
+ font-weight: 800;
384
+ font-family: 'Space Grotesk', sans-serif;
385
+ padding: 8px 20px;
386
+ border-radius: var(--radius-sm);
387
+ margin-bottom: 18px;
388
+ letter-spacing: 1px;
389
+ }
390
+ .verdict-badge.fake {
391
+ background: rgba(239,68,68,0.15);
392
+ border: 1px solid rgba(239,68,68,0.4);
393
+ color: #f87171;
394
+ }
395
+ .verdict-badge.real {
396
+ background: rgba(34,197,94,0.15);
397
+ border: 1px solid rgba(34,197,94,0.4);
398
+ color: #4ade80;
399
+ }
400
+
401
+ .verdict-stats { display: flex; flex-direction: column; gap: 10px; }
402
+ .stat-row {
403
+ display: flex;
404
+ justify-content: space-between;
405
+ align-items: center;
406
+ padding: 10px 14px;
407
+ border-radius: 8px;
408
+ background: var(--surface);
409
+ border: 1px solid var(--border);
410
+ font-size: 14px;
411
+ }
412
+ .stat-label { color: var(--text-sub); }
413
+ .stat-val { font-weight: 700; }
414
+ .fake-val { color: #f87171; }
415
+ .real-val { color: #4ade80; }
416
+
417
+ /* ── Frame Chart ── */
418
+ .chart-card {
419
+ padding: 24px;
420
+ background: var(--surface-2);
421
+ border: 1px solid var(--border);
422
+ border-radius: var(--radius);
423
+ }
424
+ .chart-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
425
+ .chart-sub { font-size: 12px; color: var(--text-sub); margin-bottom: 18px; }
426
+ .frame-chart {
427
+ display: flex;
428
+ align-items: flex-end;
429
+ gap: 4px;
430
+ height: 120px;
431
+ position: relative;
432
+ padding: 0 4px;
433
+ }
434
+ .frame-chart::after {
435
+ content: "50%";
436
+ position: absolute;
437
+ right: 0;
438
+ top: 50%;
439
+ transform: translateY(-50%);
440
+ font-size: 10px;
441
+ color: var(--text-sub);
442
+ opacity: 0.6;
443
+ }
444
+ .frame-chart::before {
445
+ content: "";
446
+ position: absolute;
447
+ left: 4px; right: 24px;
448
+ top: 50%;
449
+ border-top: 1px dashed rgba(255,255,255,0.12);
450
+ }
451
+ .bar-wrap {
452
+ flex: 1;
453
+ display: flex;
454
+ align-items: flex-end;
455
+ height: 100%;
456
+ position: relative;
457
+ }
458
+ .bar {
459
+ width: 100%;
460
+ border-radius: 4px 4px 0 0;
461
+ min-height: 4px;
462
+ transition: height 0.8s cubic-bezier(0.4,0,0.2,1);
463
+ cursor: default;
464
+ position: relative;
465
+ }
466
+ .bar::after {
467
+ content: attr(data-tip);
468
+ position: absolute;
469
+ bottom: calc(100% + 4px);
470
+ left: 50%;
471
+ transform: translateX(-50%);
472
+ background: rgba(0,0,0,0.85);
473
+ color: var(--text);
474
+ font-size: 10px;
475
+ padding: 2px 6px;
476
+ border-radius: 4px;
477
+ white-space: nowrap;
478
+ opacity: 0;
479
+ pointer-events: none;
480
+ transition: opacity 0.2s;
481
+ }
482
+ .bar:hover::after { opacity: 1; }
483
+ .bar.bar-fake { background: linear-gradient(to top, #ef4444, #f97316); }
484
+ .bar.bar-real { background: linear-gradient(to top, #22c55e, #06B6D4); }
485
+
486
+ .chart-legend {
487
+ display: flex;
488
+ gap: 18px;
489
+ margin-top: 12px;
490
+ }
491
+ .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-sub); }
492
+ .dot { width: 10px; height: 10px; border-radius: 3px; }
493
+ .dot-fake { background: #ef4444; }
494
+ .dot-real { background: #22c55e; }
495
+ .dot-thresh { background: transparent; border: 1px dashed rgba(255,255,255,0.3); }
496
+
497
+ /* ── Actions ── */
498
+ .result-actions { display: flex; gap: 12px; flex-wrap: wrap; }
499
+ .action-btn {
500
+ flex: 1;
501
+ min-width: 160px;
502
+ padding: 14px 20px;
503
+ font-size: 15px;
504
+ font-weight: 600;
505
+ border-radius: var(--radius-sm);
506
+ cursor: pointer;
507
+ transition: all var(--transition);
508
+ border: none;
509
+ outline: none;
510
+ letter-spacing: 0.2px;
511
+ }
512
+ .action-primary {
513
+ background: linear-gradient(135deg, var(--purple), var(--cyan));
514
+ color: #fff;
515
+ box-shadow: 0 6px 20px rgba(139,92,246,0.3);
516
+ }
517
+ .action-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 28px rgba(139,92,246,0.4); }
518
+ .action-secondary {
519
+ background: var(--surface-2);
520
+ color: var(--text);
521
+ border: 1px solid var(--border);
522
+ }
523
+ .action-secondary:hover { background: var(--surface); border-color: var(--purple); }
524
+
525
+ /* ── Error ── */
526
+ .error-section {
527
+ display: flex;
528
+ flex-direction: column;
529
+ align-items: center;
530
+ gap: 16px;
531
+ padding: 40px 0;
532
+ text-align: center;
533
+ }
534
+ .error-icon { font-size: 48px; }
535
+ .error-title { font-size: 22px; font-weight: 700; color: var(--red); }
536
+ .error-msg { color: var(--text-sub); max-width: 400px; font-size: 14px; }
537
+
538
+ /* ── Footer ── */
539
+ .footer {
540
+ text-align: center;
541
+ padding: 28px 24px 40px;
542
+ color: var(--text-sub);
543
+ font-size: 13px;
544
+ position: relative;
545
+ z-index: 1;
546
+ line-height: 1.8;
547
+ }
548
+ .footer-note { font-size: 11px; opacity: 0.5; }
549
+
550
+ /* ── Utility ── */
551
+ .hidden { display: none !important; }
552
+
553
+ /* ── Responsive ── */
554
+ @media (max-width: 600px) {
555
+ .main-card { padding: 24px 16px; border-radius: 20px; margin: 0 12px 40px; }
556
+ .verdict-card { flex-direction: column; align-items: center; text-align: center; }
557
+ .verdict-right { width: 100%; }
558
+ .hero { padding: 40px 16px 24px; }
559
+ .navbar { padding: 12px 16px; }
560
+ .result-actions { flex-direction: column; }
561
+ }
562
+
563
+ /* ── Entrance Animation ── */
564
+ @keyframes fadeUp {
565
+ from { opacity: 0; transform: translateY(20px); }
566
+ to { opacity: 1; transform: translateY(0); }
567
+ }
568
+ .results-section > * {
569
+ animation: fadeUp 0.5s ease both;
570
+ }
571
+ .results-section > *:nth-child(2) { animation-delay: 0.1s; }
572
+ .results-section > *:nth-child(3) { animation-delay: 0.2s; }