Harshasnade commited on
Commit
ee00155
·
0 Parent(s):

Initialize clean space deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +80 -0
  2. Dockerfile +42 -0
  3. HF_DEPLOYMENT_STEPS.md +48 -0
  4. README.md +61 -0
  5. backend/app.py +618 -0
  6. backend/checkers/__init__.py +0 -0
  7. backend/checkers/metadata_checker.py +153 -0
  8. backend/checkers/watermark_checker.py +61 -0
  9. backend/database.py +253 -0
  10. backend/requirements_web.txt +12 -0
  11. frontend/accuracy_icon.png +3 -0
  12. frontend/analysis.html +491 -0
  13. frontend/analytics_icon.png +3 -0
  14. frontend/animations.css +440 -0
  15. frontend/assets/demo_part1.mov +3 -0
  16. frontend/assets/demo_part2.mov +3 -0
  17. frontend/assets/displacement.png +3 -0
  18. frontend/assets/extension_demo.mov +3 -0
  19. frontend/assets/gemini_reveal.png +3 -0
  20. frontend/comparison_real.png +3 -0
  21. frontend/config.js +6 -0
  22. frontend/deep_learning_icon.png +3 -0
  23. frontend/extension.css +93 -0
  24. frontend/favicon.ico +3 -0
  25. frontend/hero_reveal.js +214 -0
  26. frontend/history.css +629 -0
  27. frontend/history.html +237 -0
  28. frontend/index.html +447 -0
  29. frontend/loader.css +167 -0
  30. frontend/loader.js +236 -0
  31. frontend/logo.ico +3 -0
  32. frontend/logo.svg +0 -0
  33. frontend/manifest.json +90 -0
  34. frontend/mobile.js +227 -0
  35. frontend/motion.js +122 -0
  36. frontend/offline.html +170 -0
  37. frontend/orbit.css +274 -0
  38. frontend/orbit_interaction.js +112 -0
  39. frontend/pwa.css +271 -0
  40. frontend/pwa.js +255 -0
  41. frontend/realtime_analysis_icon.png +3 -0
  42. frontend/responsive-additions.css +898 -0
  43. frontend/responsive-pages.css +350 -0
  44. frontend/script.js +0 -0
  45. frontend/scroll_indicator.css +96 -0
  46. frontend/service-worker.js +219 -0
  47. frontend/style.css +0 -0
  48. frontend/three_bg.js +265 -0
  49. frontend/vercel.json +5 -0
  50. frontend/video_player.css +160 -0
.gitignore ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ env/
8
+ venv/
9
+ .venv/
10
+ pip-log.txt
11
+ pip-delete-this-directory.txt
12
+
13
+ # Database
14
+ database.db
15
+ *.sqlite3
16
+
17
+ # OS
18
+ .DS_Store
19
+ .DS_Store?
20
+ ._*
21
+ .Trashes
22
+ ehthumbs.db
23
+ Thumbs.db
24
+
25
+ # Logs
26
+ *.log
27
+
28
+ # Environment Variables
29
+ .env
30
+ .env.local
31
+
32
+ # Editor
33
+ .vscode/
34
+ .idea/
35
+
36
+ # Project Specific
37
+ test_images/
38
+ uploads/
39
+ history_uploads/
40
+ feedback_images/
41
+ video_batch_results.csv
42
+ *.bak
43
+
44
+
45
+ # Documentation (Ignore all)
46
+ *.md
47
+ documentation/
48
+
49
+ # Specific Files
50
+ LICENSE
51
+ PULL_REQUEST_TEMPLATE.md
52
+ bug_report.yml
53
+ config.yml
54
+ feature_request.yml
55
+ model/DETAILED_HISTORY.md
56
+ model/FACEFORENSICS_GUIDE.md
57
+ model/MODEL_CARD.md
58
+ model/TRAINING_HISTORY.md
59
+ CONTRIBUTING.md
60
+ CODE_OF_CONDUCT.md
61
+ SECURITY.md
62
+ CHANGELOG.md
63
+ README.md
64
+ LICENSE
65
+ .github/
66
+ .gitattributes
67
+
68
+ # Large Model Files
69
+ # *.safetensors <-- Commented out to allow Git LFS
70
+ *.pth
71
+ *.pt
72
+ # model/results/
73
+ !model/results/checkpoints/
74
+ model/results/
75
+ model/checkpoints/
76
+
77
+ # Project Specific
78
+ visualizations/
79
+ generate_visualizations.py
80
+ plots/!README.md
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ # Explicitly set the port for Hugging Face Spaces
4
+ ENV PORT=7860
5
+
6
+ # Set working directory to /code
7
+ WORKDIR /code
8
+
9
+ # Copy specific requirement file
10
+ COPY backend/requirements_web.txt /code/requirements.txt
11
+
12
+ # Install system dependencies
13
+ RUN apt-get update && apt-get install -y \
14
+ libgl1 \
15
+ ffmpeg \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Install python dependencies
19
+ RUN pip install --no-cache-dir -r /code/requirements.txt
20
+
21
+ # Copy the entire repository
22
+ COPY . /code
23
+
24
+ # Create necessary directories that the app writes to
25
+ RUN mkdir -p /code/backend/uploads \
26
+ /code/backend/history_uploads \
27
+ /code/backend/feedback_images \
28
+ /code/model/results/checkpoints
29
+
30
+ # Set permissions for writable directories (required for Spaces running as non-root)
31
+ RUN chmod -R 777 /code/backend/uploads \
32
+ /code/backend/history_uploads \
33
+ /code/backend/feedback_images
34
+
35
+ # Ensure database exists or is writable
36
+ RUN touch /code/backend/database.db && chmod 777 /code/backend/database.db
37
+
38
+ # Expose the port
39
+ EXPOSE 7860
40
+
41
+ # Run the backend app
42
+ CMD ["python", "backend/app.py"]
HF_DEPLOYMENT_STEPS.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Deployment Guide (Post-Merge)
2
+
3
+ Since you have already merged your PR, follow these specific steps to deploy the entire application (Frontend + Backend) to a single Hugging Face Space.
4
+
5
+ ## Step 1: Prepare the Frontend Configuration
6
+ Ensure your frontend knows to talk to the backend on the same host.
7
+ 1. Open [config.js](file:///Users/harshvardhan/Developer/Deepfake Project /Morden Detections system/frontend/config.js).
8
+ 2. The `API_BASE_URL` logic already handles this correctly by checking if it's on a local or production host. No changes should be needed if you use the direct Space URL.
9
+
10
+ ## Step 2: Configure Hugging Face Remote
11
+ If you haven't already linked your local repository to your Hugging Face space, run these commands:
12
+
13
+ ```bash
14
+ # Install Git LFS to handle the large model files properly
15
+ git lfs install
16
+ git lfs track "*.safetensors"
17
+
18
+ # Add the Hugging Face Space as a git remote
19
+ # Replace USERNAME and SPACE_NAME with your actual details
20
+ git remote add space https://huggingface.co/spaces/USERNAME/SPACE_NAME
21
+ ```
22
+
23
+ ## Step 3: Deploy the Unified System
24
+ Hugging Face uses the [Dockerfile](file:///Users/harshvardhan/Developer/Deepfake Project /Morden Detections system/Dockerfile) in the root directory to build your app.
25
+
26
+ ```bash
27
+ # Add all changes
28
+ git add .
29
+
30
+ # Commit (if not already committed)
31
+ git commit -m "Prepare for deployment"
32
+
33
+ # Push to Hugging Face
34
+ # This will trigger the build and deploy process on HF
35
+ git push space Harshvardhan:main
36
+ ```
37
+ > [!NOTE]
38
+ > If your Space uses a different main branch name (like `main`), use `git push space Harshvardhan:main`.
39
+
40
+ ## Step 4: Verification
41
+ 1. **Monitor Build**: Go to your Hugging Face Space page and click the "Logs" tab.
42
+ 2. **Port Check**: Ensure the app is listening on port `7860`. The `Dockerfile` and `app.py` are already configured for this.
43
+ 3. **Model Loading**: Check the logs to ensure the `Mark-V.safetensors` model loads correctly on startup.
44
+
45
+ ## Summary of "The Difference"
46
+ - **Unified Hosting**: Unlike Vercel (Frontend only), a Hugging Face Docker Space hosts both your Flask API and your HTML files simultaneously.
47
+ - **Port 7860**: Hugging Face specifically looks for traffic on port `7860`.
48
+ - **Persistent Storage**: Remember that files saved to `uploads/` on HF are ephemeral unless you use a HF Dataset or Persistent Storage volume.
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Deepfake Detection Model
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_file: backend/app.py
8
+ app_port: 7860
9
+ pinned: false
10
+ ---
11
+
12
+ # DeepGuard: AI-Powered Deepfake Detection
13
+
14
+ ![Accuracy](https://img.shields.io/badge/Accuracy-96.97%25-brightgreen)
15
+ ![Model](https://img.shields.io/badge/Model-Mark--V-blue)
16
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
17
+
18
+ **DeepGuard** is a state-of-the-art, privacy-focused tool designed to detect AI-generated images with **96.97% accuracy**. It runs entirely on your local machine using a Hybrid Multi-Branch Neural Network.
19
+
20
+ ![Radar Chart](model/visualizations/6_model_radar_comparison.png)
21
+
22
+ ## 🚀 Quick Links
23
+
24
+ * **[📝 Overview & How it Works](Documentation/OVERVIEW.md)**
25
+ * **[⚡ Getting Started Guide](Documentation/GETTING_STARTED.md)**
26
+ * **[🏗️ System Architecture](Documentation/ARCHITECTURE.md)**
27
+ * **[🔒 Security & Privacy](Documentation/SECURITY.md)**
28
+ * **[🛠️ Backend API](Documentation/BACKEND.md)**
29
+ * **[🎨 Frontend Guide](Documentation/FRONTEND.md)**
30
+
31
+ ## 🏆 Current Performance (Mark-V)
32
+
33
+ | Metric | Score | Note |
34
+ | :--- | :--- | :--- |
35
+ | **Accuracy** | **96.97%** | Tested on Universal Dataset |
36
+ | **Reliability** | **Generative** | Wide coverage of generation methods |
37
+ | **FPS** | **~25** | Real-time analysis on GPU |
38
+
39
+ ## 📦 Features
40
+
41
+ * **Multi-Branch Detection**: Combines RGB, Frequency (FFT), Patch analysis, and Vision Transformers.
42
+ * **Defense-in-Depth**: Automatically detects C2PA credentials and invisible watermarks (Stable Diffusion).
43
+ * **Local-First**: No data ever leaves your computer.
44
+ * **History Tracking**: Keep a local log of your scans.
45
+
46
+ ## 💻 Quick Install
47
+
48
+ ```bash
49
+ git clone https://github.com/your-username/DeepGuard.git
50
+ cd DeepGuard/backend
51
+ python -m venv venv
52
+ source venv/bin/activate
53
+ pip install -r requirements_web.txt
54
+ python app.py
55
+ ```
56
+
57
+ Open `http://localhost:7860` in your browser.
58
+
59
+ ---
60
+
61
+ For full documentation, please visit the **[Documentation Folder](Documentation/)**.
backend/app.py ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory, Response, make_response
2
+ from flask_cors import CORS
3
+ import sys
4
+ import os
5
+ import re
6
+ import mimetypes
7
+ import subprocess
8
+
9
+ # Add model directory to path
10
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'model')))
11
+ import datetime
12
+ import torch
13
+ import cv2
14
+ import os
15
+ import numpy as np
16
+ import ssl
17
+ import base64
18
+ from werkzeug.utils import secure_filename
19
+ import io
20
+ from PIL import Image
21
+ from src import video_inference
22
+
23
+ # Disable SSL verification
24
+ ssl._create_default_https_context = ssl._create_unverified_context
25
+ import albumentations as A
26
+ from albumentations.pytorch import ToTensorV2
27
+ from albumentations.pytorch import ToTensorV2
28
+ from src.models import DeepfakeDetector
29
+ from src.config import Config
30
+ from checkers import metadata_checker
31
+ from checkers import watermark_checker
32
+ import database
33
+
34
+ try:
35
+ from safetensors.torch import load_file
36
+ SAFETENSORS_AVAILABLE = True
37
+ except ImportError:
38
+ SAFETENSORS_AVAILABLE = False
39
+
40
+ app = Flask(__name__, static_folder='../frontend', static_url_path='')
41
+ CORS(app)
42
+
43
+ # Configuration
44
+ UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')
45
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp', 'mp4', 'avi', 'mov', 'webm'}
46
+ HISTORY_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'history_uploads')
47
+ FEEDBACK_FOLDER = os.path.join(os.path.dirname(__file__), 'feedback_images')
48
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
49
+ os.makedirs(HISTORY_FOLDER, exist_ok=True)
50
+ os.makedirs(FEEDBACK_FOLDER, exist_ok=True)
51
+
52
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
53
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
54
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # Increase to 500MB for video
55
+
56
+ # Global model and transform
57
+ # Global model and transform
58
+ device = torch.device(Config.DEVICE)
59
+ model = None
60
+ video_model_onnx = None # Dedicated optimized model for video
61
+ transform = None
62
+
63
+ def get_transform():
64
+ return A.Compose([
65
+ A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),
66
+ A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
67
+ ToTensorV2(),
68
+ ])
69
+
70
+ def load_model():
71
+ """Load the trained deepfake detection model"""
72
+ global model, transform, video_model_onnx
73
+
74
+ checkpoint_dir = Config.CHECKPOINT_DIR
75
+ target_model_name = "Mark-V.safetensors"
76
+ checkpoint_path = os.path.join(checkpoint_dir, target_model_name)
77
+
78
+ print(f"Using device: {device}")
79
+
80
+ # 1. Load PyTorch Model (Required for single image Image Heatmaps)
81
+ model = DeepfakeDetector(pretrained=True)
82
+ model.to(device)
83
+ model.eval()
84
+
85
+ if not os.path.exists(checkpoint_path):
86
+ print(f"❌ CRITICAL ERROR: Model file not found at: {checkpoint_path}")
87
+ model = None
88
+ transform = get_transform()
89
+ return model, transform
90
+
91
+ try:
92
+ print(f"Loading PyTorch checkpoint: {checkpoint_path}")
93
+ if checkpoint_path.endswith(".safetensors") and SAFETENSORS_AVAILABLE:
94
+ state_dict = load_file(checkpoint_path)
95
+ else:
96
+ state_dict = torch.load(checkpoint_path, map_location=device)
97
+
98
+ # Try loading directly first
99
+ try:
100
+ model.load_state_dict(state_dict)
101
+ print(f"✅ PyTorch Model loaded successfully!")
102
+ except Exception as e:
103
+ # Keys don't match - apply remapping for architecture compatibility
104
+ print(f"⚠️ Direct load failed. Attempting key remapping...")
105
+ from collections import OrderedDict
106
+ new_state_dict = OrderedDict()
107
+ for k, v in state_dict.items():
108
+ if k.startswith('rgb_branch.features.'):
109
+ new_k = k.replace('rgb_branch.features.', 'rgb_branch.net.features.')
110
+ new_state_dict[new_k] = v
111
+ elif k.startswith('rgb_branch.avgpool.'):
112
+ new_k = k.replace('rgb_branch.avgpool.', 'rgb_branch.net.avgpool.')
113
+ new_state_dict[new_k] = v
114
+ else:
115
+ new_state_dict[k] = v
116
+
117
+ model.load_state_dict(new_state_dict, strict=False)
118
+ print(f"✅ PyTorch Model loaded successfully (with key remapping)!")
119
+
120
+ except Exception as e:
121
+ print(f"❌ Error loading PyTorch checkpoint: {e}")
122
+ model = None
123
+
124
+ # 2. Load ONNX Model (Removed)
125
+ # System optimized for PyTorch Pipeline (Threaded Preprocessing)
126
+ video_model_onnx = None
127
+
128
+ transform = get_transform()
129
+ return model, transform
130
+
131
+ def allowed_file(filename):
132
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
133
+
134
+ def predict_image(image_path):
135
+ """Make prediction on a single image"""
136
+ if model is None:
137
+ return None, "Error: Model not loaded. Check backend logs for 'best_model.safetensors' error."
138
+
139
+ try:
140
+ # Read and preprocess image
141
+ image = cv2.imread(image_path)
142
+ if image is None:
143
+ return None, "Error: Could not read image"
144
+
145
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
146
+ augmented = transform(image=image)
147
+ image_tensor = augmented['image'].unsqueeze(0).to(device)
148
+
149
+
150
+ # 0. Metadata & Watermark Checks
151
+ meta_result = metadata_checker.check_metadata(image_path)
152
+ water_result = watermark_checker.check_watermarks(image_path)
153
+
154
+ # Make prediction
155
+ logits = model(image_tensor)
156
+ prob = torch.sigmoid(logits).item()
157
+
158
+ # Generate Heatmap
159
+ heatmap = model.get_heatmap(image_tensor)
160
+
161
+ # Process Heatmap for Visualization
162
+ # Resize to original image size
163
+ heatmap = cv2.resize(heatmap, (image.shape[1], image.shape[0]))
164
+ heatmap = np.uint8(255 * heatmap)
165
+ heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
166
+
167
+ # Superimpose
168
+ # Heatmap is BGR (from cv2), Image is RGB. Convert Image to BGR.
169
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
170
+ superimposed_img = heatmap * 0.4 + image_bgr * 0.6
171
+ superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)
172
+
173
+ # Encode to Base64
174
+ _, buffer = cv2.imencode('.jpg', superimposed_img)
175
+ heatmap_b64 = base64.b64encode(buffer).decode('utf-8')
176
+
177
+ is_fake = prob > 0.5
178
+
179
+ # Override if metadata confirms fake
180
+ if meta_result['detected'] or water_result['detected']:
181
+ is_fake = True
182
+ # If visual model was unsure (e.g. 0.4), bump it up?
183
+ # Or just rely on the 'prediction' label.
184
+ # Let's trust the metadata 100%
185
+ prob = max(prob, 0.99)
186
+
187
+ # Hidden Check: Explicitly flag known generator filenames as FAKE without frontend badging
188
+ filename_lower = os.path.basename(image_path).lower()
189
+ if "chatgpt" in filename_lower or "gemini" in filename_lower:
190
+ is_fake = True
191
+ prob = max(prob, 0.998) # Extremely high confidence
192
+ # Intentionally NOT adding to meta_result or water_result to keep it hidden from badges
193
+ # as requested by user ("dont shiw this in fornetend")
194
+
195
+ label = "FAKE" if is_fake else "REAL"
196
+ confidence = prob if is_fake else 1 - prob
197
+
198
+ return {
199
+ 'prediction': label,
200
+ 'confidence': float(confidence),
201
+ 'fake_probability': float(prob),
202
+ 'real_probability': float(1 - prob),
203
+ 'heatmap': heatmap_b64,
204
+ 'metadata_check': meta_result,
205
+ 'watermark_check': water_result
206
+ }, None
207
+ except Exception as e:
208
+ return None, str(e)
209
+
210
+
211
+ @app.route('/')
212
+ def index():
213
+ """Serve the frontend"""
214
+ # Use absolute path to avoid CWD issues
215
+ frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend'))
216
+ return send_from_directory(frontend_dir, 'index.html')
217
+
218
+ @app.route('/history_uploads/<path:filename>')
219
+ def serve_history_image(filename):
220
+ """Serve history images and videos with Range support"""
221
+ file_path = os.path.join(HISTORY_FOLDER, filename)
222
+ if not os.path.exists(file_path):
223
+ return jsonify({'error': 'File not found'}), 404
224
+
225
+ # Handle Video Range Requests
226
+ if filename.lower().endswith(('.mp4', '.mov', '.avi', '.webm')):
227
+ file_size = os.path.getsize(file_path)
228
+ range_header = request.headers.get('Range', None)
229
+
230
+ if not range_header:
231
+ # No Range header, serve normally but with video headers
232
+ response = make_response(send_from_directory(HISTORY_FOLDER, filename))
233
+ response.headers['Content-Type'] = 'video/mp4'
234
+ response.headers['Accept-Ranges'] = 'bytes'
235
+ return response
236
+
237
+ # Parse Range Header
238
+ byte1, byte2 = 0, None
239
+ m = re.search(r'bytes=(\d+)-(\d*)', range_header)
240
+ if m:
241
+ g = m.groups()
242
+ byte1 = int(g[0])
243
+ if g[1]:
244
+ byte2 = int(g[1])
245
+
246
+ length = file_size - byte1
247
+ if byte2 is not None:
248
+ length = byte2 + 1 - byte1
249
+
250
+ # Read partial content
251
+ with open(file_path, 'rb') as f:
252
+ f.seek(byte1)
253
+ data = f.read(length)
254
+
255
+ response = Response(
256
+ data,
257
+ 206,
258
+ mimetype='video/mp4',
259
+ direct_passthrough=True
260
+ )
261
+
262
+ # Determine content range
263
+ content_range_end = byte2 if byte2 is not None else file_size - 1
264
+
265
+ response.headers.add('Content-Range', f'bytes {byte1}-{content_range_end}/{file_size}')
266
+ response.headers.add('Accept-Ranges', 'bytes')
267
+ response.headers.add('Content-Length', str(length))
268
+ response.headers.add('Access-Control-Allow-Origin', '*')
269
+ return response
270
+
271
+ # Default for images
272
+ response = send_from_directory(HISTORY_FOLDER, filename)
273
+ return response
274
+
275
+ def reencode_video(input_path):
276
+ """Re-encode video to H.264/AAC with faststart using ffmpeg"""
277
+ try:
278
+ output_path = input_path + "_temp.mp4"
279
+ print(f"🔄 Re-encoding video: {input_path}")
280
+
281
+ # FFmpeg command
282
+ # -y: overwrite output
283
+ # -c:v libx264: use H.264 video codec
284
+ # -preset fast: encode speed
285
+ # -profile:v high: high profile for better compatibility
286
+ # -level 4.0: compatibility level
287
+ # -pix_fmt yuv420p: ensure wide player compatibility (essential for QuickTime/Safari)
288
+ # -c:a aac: use AAC audio codec
289
+ # -movflags +faststart: move metadata to front for streaming
290
+ cmd = [
291
+ 'ffmpeg', '-y',
292
+ '-i', input_path,
293
+ '-c:v', 'libx264',
294
+ '-preset', 'fast',
295
+ '-pix_fmt', 'yuv420p',
296
+ '-c:a', 'aac',
297
+ '-movflags', '+faststart',
298
+ output_path
299
+ ]
300
+
301
+ # Run ffmpeg
302
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
303
+
304
+ if result.returncode != 0:
305
+ print(f"❌ FFmpeg re-encoding failed: {result.stderr.decode()}")
306
+ return input_path # Fallback to original
307
+
308
+ print(f"✅ Video re-encoded successfully!")
309
+
310
+ # Replace original
311
+ os.remove(input_path)
312
+ os.rename(output_path, input_path)
313
+ return input_path
314
+
315
+ except Exception as e:
316
+ print(f"❌ Error during re-encoding: {e}")
317
+ return input_path
318
+
319
+ @app.route('/api/health', methods=['GET'])
320
+ def health_check():
321
+ """Health check endpoint with detailed model status"""
322
+ model_status = "ready" if model is not None else "initializing"
323
+
324
+ return jsonify({
325
+ 'status': 'healthy',
326
+ 'model_status': model_status,
327
+ 'model_loaded': model is not None,
328
+ 'device': str(device)
329
+ })
330
+
331
+ @app.route('/api/predict', methods=['POST'])
332
+ def predict():
333
+ """Handle image upload and prediction"""
334
+ try:
335
+ # Check if file is present
336
+ if 'file' not in request.files:
337
+ return jsonify({'error': 'No file provided'}), 400
338
+
339
+ file = request.files['file']
340
+
341
+ if file.filename == '':
342
+ return jsonify({'error': 'No file selected'}), 400
343
+
344
+ if not allowed_file(file.filename):
345
+ return jsonify({'error': 'Invalid file type. Allowed types: png, jpg, jpeg, webp'}), 400
346
+
347
+ # Save file
348
+ filename = secure_filename(file.filename)
349
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
350
+ file.save(filepath)
351
+
352
+ # Make prediction
353
+ result, error = predict_image(filepath)
354
+
355
+ if error:
356
+ return jsonify({'error': error}), 500
357
+
358
+ # Save to History
359
+ import shutil
360
+ history_filename = f"scan_{int(datetime.datetime.now().timestamp())}_{filename}"
361
+ history_path = os.path.join(HISTORY_FOLDER, history_filename)
362
+
363
+ # Copy original file to history folder
364
+ # We need to read the file again or just copy if we haven't deleted it?
365
+ # We read via cv2, the file is still at filepath.
366
+ shutil.copy(filepath, history_path)
367
+
368
+ # Relative path for frontend
369
+ relative_path = f"history_uploads/{history_filename}"
370
+
371
+ scan_id = database.add_scan(
372
+ filename=filename,
373
+ prediction=result['prediction'],
374
+ confidence=result['confidence'],
375
+ fake_prob=result['fake_probability'],
376
+ real_prob=result['real_probability'],
377
+ image_path=relative_path,
378
+ session_id=request.headers.get('X-Session-ID')
379
+ )
380
+
381
+ # Clean up uploaded file
382
+ try:
383
+ os.remove(filepath)
384
+ except:
385
+ pass
386
+
387
+ # Add scan_id to result for frontend tracking
388
+ result['scan_id'] = scan_id
389
+
390
+ return jsonify(result)
391
+
392
+ except Exception as e:
393
+ return jsonify({'error': str(e)}), 500
394
+
395
+ @app.route('/api/predict_video', methods=['POST'])
396
+ def predict_video():
397
+ """Handle video upload and prediction"""
398
+ try:
399
+ if 'file' not in request.files:
400
+ return jsonify({'error': 'No file provided'}), 400
401
+
402
+ file = request.files['file']
403
+
404
+ if file.filename == '':
405
+ return jsonify({'error': 'No file selected'}), 400
406
+
407
+ if not allowed_file(file.filename):
408
+ return jsonify({'error': 'Invalid file type'}), 400
409
+
410
+ # Save file
411
+ filename = secure_filename(file.filename)
412
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
413
+ file.save(filepath)
414
+
415
+ # Re-encode video for proper web playback
416
+ filepath = reencode_video(filepath)
417
+
418
+ # Process Video
419
+ # Prioritize Optimized ONNX Model
420
+ active_model = video_model_onnx if video_model_onnx is not None else model
421
+
422
+ if active_model is None:
423
+ return jsonify({'error': 'Model not loaded'}), 500
424
+
425
+ result = video_inference.process_video(filepath, active_model, transform, device, frames_per_second=10)
426
+
427
+ if "error" in result:
428
+ return jsonify(result), 500
429
+
430
+ # Save to History (Using the first frame or a placeholder icon for now?)
431
+ # For video, we might want to save the video file itself to history_uploads
432
+ # or just a thumbnail. Let's save the video for now.
433
+ import shutil
434
+ history_filename = f"scan_{int(datetime.datetime.now().timestamp())}_{filename}"
435
+ history_path = os.path.join(HISTORY_FOLDER, history_filename)
436
+ shutil.copy(filepath, history_path)
437
+
438
+ relative_path = f"history_uploads/{history_filename}"
439
+
440
+ # Add to database
441
+ # Note: The database 'add_scan' might expect image-specific fields.
442
+ # We'll re-use 'fake_prob' as 'avg_fake_prob'
443
+ scan_id = database.add_scan(
444
+ filename=filename,
445
+ prediction=result['prediction'],
446
+ confidence=result['confidence'],
447
+ fake_prob=result['avg_fake_prob'],
448
+ real_prob=1 - result['avg_fake_prob'],
449
+ image_path=relative_path,
450
+ session_id=request.headers.get('X-Session-ID')
451
+ )
452
+
453
+ # Clean up
454
+ try:
455
+ os.remove(filepath)
456
+ except:
457
+ pass
458
+
459
+ # Add video URL for frontend playback
460
+ result['video_url'] = relative_path
461
+ result['scan_id'] = scan_id
462
+
463
+ return jsonify(result)
464
+
465
+ except Exception as e:
466
+ print(f"Video Error: {e}")
467
+ return jsonify({'error': str(e)}), 500
468
+
469
+
470
+ @app.route('/api/history', methods=['GET'])
471
+ def get_history():
472
+ """Get all past scans"""
473
+ session_id = request.headers.get('X-Session-ID')
474
+ history = database.get_history(session_id)
475
+ return jsonify(history)
476
+
477
+ @app.route('/api/history/<int:scan_id>', methods=['PATCH'])
478
+ def update_history_item(scan_id):
479
+ """Update a specific scan's metadata (notes, tags)"""
480
+ data = request.json
481
+ if not data:
482
+ return jsonify({'error': 'No data provided'}), 400
483
+
484
+ if database.update_scan(scan_id, data):
485
+ return jsonify({'message': 'Scan updated successfully'})
486
+ return jsonify({'error': 'Failed to update scan'}), 500
487
+
488
+ @app.route('/api/history/<int:scan_id>', methods=['DELETE'])
489
+ def delete_scan(scan_id):
490
+ """Delete a specific scan"""
491
+ session_id = request.headers.get('X-Session-ID')
492
+ if database.delete_scan(scan_id, session_id):
493
+ return jsonify({'message': 'Scan deleted'})
494
+ return jsonify({'error': 'Failed to delete scan'}), 500
495
+
496
+ @app.route('/api/history', methods=['DELETE'])
497
+ def clear_history():
498
+ """Clear all history"""
499
+ session_id = request.headers.get('X-Session-ID')
500
+ if database.clear_history(session_id):
501
+ return jsonify({'message': 'History cleared'})
502
+ return jsonify({'error': 'Failed to clear history'}), 500
503
+
504
+ @app.route('/api/feedback', methods=['POST'])
505
+ def submit_feedback():
506
+ """Submit user feedback on a prediction"""
507
+ try:
508
+ data = request.json
509
+ if not data:
510
+ return jsonify({'error': 'No data provided'}), 400
511
+
512
+ scan_id = data.get('scan_id')
513
+ is_correct = data.get('is_correct')
514
+ predicted_label = data.get('predicted_label')
515
+
516
+ if scan_id is None or is_correct is None or not predicted_label:
517
+ return jsonify({'error': 'Missing required fields'}), 400
518
+
519
+ # Get scan details from history
520
+ history = database.get_history()
521
+ scan = next((s for s in history if s['id'] == scan_id), None)
522
+
523
+ if not scan:
524
+ return jsonify({'error': 'Scan not found'}), 404
525
+
526
+ actual_label = None
527
+ feedback_image_path = None
528
+
529
+ # If prediction is incorrect, determine actual label and copy image
530
+ if not is_correct:
531
+ # Actual label is opposite of prediction
532
+ actual_label = 'REAL' if predicted_label == 'FAKE' else 'FAKE'
533
+
534
+ # Copy image to feedback folder for retraining
535
+ if scan.get('image_path'):
536
+ try:
537
+ import shutil
538
+ source_path = os.path.join(os.path.dirname(__file__), '..', 'frontend', scan['image_path'])
539
+ feedback_filename = f"feedback_{scan_id}_{scan['filename']}"
540
+ feedback_dest = os.path.join(FEEDBACK_FOLDER, feedback_filename)
541
+
542
+ if os.path.exists(source_path):
543
+ shutil.copy(source_path, feedback_dest)
544
+ feedback_image_path = feedback_filename
545
+ print(f"✅ Copied feedback image to: {feedback_dest}")
546
+ else:
547
+ print(f"⚠️ Source image not found: {source_path}")
548
+ except Exception as e:
549
+ print(f"❌ Error copying feedback image: {e}")
550
+
551
+ # Record feedback in database
552
+ success = database.add_feedback(
553
+ scan_id=scan_id,
554
+ is_correct=is_correct,
555
+ predicted_label=predicted_label,
556
+ actual_label=actual_label,
557
+ image_path=feedback_image_path,
558
+ confidence=scan.get('confidence')
559
+ )
560
+
561
+ if success:
562
+ feedback_type = 'correct' if is_correct else 'incorrect'
563
+ return jsonify({
564
+ 'message': f'Feedback recorded successfully',
565
+ 'feedback': feedback_type,
566
+ 'actual_label': actual_label
567
+ })
568
+ else:
569
+ return jsonify({'error': 'Failed to record feedback'}), 500
570
+
571
+ except Exception as e:
572
+ print(f"Feedback error: {e}")
573
+ return jsonify({'error': str(e)}), 500
574
+
575
+ @app.route('/api/feedback/stats', methods=['GET'])
576
+ def get_feedback_stats():
577
+ """Get feedback statistics"""
578
+ stats = database.get_feedback_stats()
579
+ return jsonify(stats)
580
+
581
+ @app.route('/api/model-info', methods=['GET'])
582
+ def model_info():
583
+ """Return model information"""
584
+ return jsonify({
585
+ 'model_name': 'DeepGuard: Advanced Deepfake Detector',
586
+ 'architecture': 'Hybrid CNN-ViT',
587
+ 'components': {
588
+ 'RGB Analysis': Config.USE_RGB,
589
+ 'Frequency Domain': Config.USE_FREQ,
590
+ 'Patch-based Detection': Config.USE_PATCH,
591
+ 'Vision Transformer': Config.USE_VIT
592
+ },
593
+ 'image_size': Config.IMAGE_SIZE,
594
+ 'device': str(device),
595
+ 'threshold': 0.5
596
+ })
597
+
598
+ if __name__ == '__main__':
599
+ print("=" * 60)
600
+ print("🚀 DeepGuard - Deepfake Detection System")
601
+ print("=" * 60)
602
+
603
+ # Load model
604
+ load_model()
605
+
606
+ print("=" * 60)
607
+ print("=" * 60)
608
+ # Check if running on Hugging Face Spaces
609
+ if os.environ.get("SPACE_ID"):
610
+ port = 7860
611
+ print(f"🪐 Detected Hugging Face Space. Forcing port {port}")
612
+ else:
613
+ port = int(os.environ.get("PORT", 7860))
614
+
615
+ print(f"🌐 Starting server on http://0.0.0.0:{port}")
616
+ print("=" * 60)
617
+
618
+ app.run(debug=False, host='0.0.0.0', port=port)
backend/checkers/__init__.py ADDED
File without changes
backend/checkers/metadata_checker.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import exifread
4
+ try:
5
+ import c2pa
6
+ except ImportError:
7
+ c2pa = None
8
+
9
+ def check_metadata(filepath):
10
+ """
11
+ Checks for Content Credentials (C2PA) and specific AI-generation metadata in Exif/XMP.
12
+ Returns a dictionary with detection status and details.
13
+ """
14
+ result = {
15
+ "detected": False,
16
+ "method": None,
17
+ "source": None,
18
+ "details": {}
19
+ }
20
+
21
+ # 1. Check C2PA / Content Credentials
22
+ if c2pa:
23
+ try:
24
+ # Correct API usage for c2pa-python
25
+ reader = c2pa.Reader(filepath)
26
+ manifest_json = reader.json()
27
+
28
+ # Robust check: Convert to string and search for keywords
29
+ # This avoids dependency on exact JSON structure which might vary
30
+ json_str = manifest_json.lower()
31
+
32
+ if "dall-e" in json_str:
33
+ result["detected"] = True
34
+ result["method"] = "C2PA"
35
+ result["source"] = "DALL-E"
36
+ return result
37
+ if "adobe firefly" in json_str:
38
+ result["detected"] = True
39
+ result["method"] = "C2PA"
40
+ result["source"] = "Adobe Firefly"
41
+ return result
42
+ if "bing image creator" in json_str:
43
+ result["detected"] = True
44
+ result["method"] = "C2PA"
45
+ result["source"] = "Bing Image Creator"
46
+ return result
47
+ if "my-tool" in json_str: # Example placeholder
48
+ pass
49
+
50
+ # General check for "artificial" or "created" actions if no specific tool found
51
+ if 'c2pa.actions' in json_str and 'artificial' in json_str:
52
+ result["detected"] = True
53
+ result["method"] = "C2PA"
54
+ result["source"] = "AI Generated (C2PA)"
55
+ return result
56
+
57
+ except Exception as e:
58
+ # Expected if no C2PA manifest exists
59
+ # print(f"C2PA Check Info: {e}")
60
+ pass
61
+
62
+ # 2. Check Exif/XMP via ExifRead
63
+ try:
64
+ with open(filepath, 'rb') as f:
65
+ tags = exifread.process_file(f)
66
+
67
+ # Common AI signatures in Exif/XMP/IPTC
68
+ software_tags = [str(tags.get('Image Software', '')), str(tags.get('0th Software', ''))]
69
+ description_tags = [str(tags.get('Image ImageDescription', '')), str(tags.get('EXIF UserComment', ''))]
70
+
71
+ # DALL-E 3 often leaves signature in ImageDescription or Software
72
+ for tag in software_tags + description_tags:
73
+ tag_lower = tag.lower()
74
+ if "dall-e" in tag_lower:
75
+ result["detected"] = True
76
+ result["method"] = "EXIF"
77
+ result["source"] = "DALL-E"
78
+ return result
79
+ if "adobe firefly" in tag_lower:
80
+ result["detected"] = True
81
+ result["method"] = "EXIF"
82
+ result["source"] = "Adobe Firefly"
83
+ return result
84
+ if "bing image creator" in tag_lower:
85
+ result["detected"] = True
86
+ result["method"] = "EXIF"
87
+ result["source"] = "Bing Image Creator"
88
+ return result
89
+ if "stable diffusion" in tag_lower:
90
+ result["detected"] = True
91
+ result["method"] = "EXIF"
92
+ result["source"] = "Stable Diffusion"
93
+ return result
94
+
95
+ # Generic check for other known AI tools based on common signatures
96
+ for tool in ["midjourney", "runway", "leonardo", "nightcafe", "canva"]:
97
+ if tool in tag_lower:
98
+ result["detected"] = True
99
+ result["method"] = "EXIF"
100
+ result["source"] = tool.title() # Capitalize first letter
101
+ return result
102
+
103
+ except Exception as e:
104
+ print(f"Exif Check Error: {e}")
105
+
106
+ # 3. Check PNG Text Chunks (often used by Leonardo, NightCafe, Stable Diffusion)
107
+ # ExifRead doesn't always catch purely textual PNG chunks "parameters" or "Software"
108
+ try:
109
+ from PIL import Image
110
+ img = Image.open(filepath)
111
+ img.load() # Load to access info
112
+
113
+ info = img.info or {}
114
+
115
+ # Combine all string values for search
116
+ search_space = " ".join([str(v).lower() for k, v in info.items()])
117
+
118
+ if "stable diffusion" in search_space:
119
+ result["detected"] = True
120
+ result["method"] = "PNG Metadata"
121
+ result["source"] = "Stable Diffusion"
122
+ return result
123
+
124
+ if "midjourney" in search_space:
125
+ result["detected"] = True
126
+ result["method"] = "PNG Metadata"
127
+ result["source"] = "Midjourney"
128
+ return result
129
+
130
+ if "leonardo" in search_space:
131
+ result["detected"] = True
132
+ result["method"] = "PNG Metadata"
133
+ result["source"] = "Leonardo AI"
134
+ return result
135
+
136
+ if "nightcafe" in search_space:
137
+ result["detected"] = True
138
+ result["method"] = "PNG Metadata"
139
+ result["source"] = "NightCafe"
140
+ return result
141
+
142
+ if "runway" in search_space:
143
+ result["detected"] = True
144
+ result["method"] = "PNG Metadata"
145
+ result["source"] = "Runway Gen-2"
146
+ return result
147
+
148
+ except Exception as e:
149
+ # print(f"PNG Check Error: {e}")
150
+ pass
151
+
152
+
153
+ return result
backend/checkers/watermark_checker.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ try:
3
+ from imwatermark import WatermarkDecoder
4
+ except ImportError:
5
+ WatermarkDecoder = None
6
+
7
+ def check_watermarks(filepath):
8
+ """
9
+ Checks for invisible watermarks (specifically Stable Diffusion's 'sd_private').
10
+ Returns a dictionary with detection status.
11
+ """
12
+ result = {
13
+ "detected": False,
14
+ "method": None,
15
+ "source": None
16
+ }
17
+
18
+ if not WatermarkDecoder:
19
+ return result
20
+
21
+ try:
22
+ # Standard Stable Diffusion watermark is 48 bits, detecting 'bytes'
23
+ decoder = WatermarkDecoder('bytes', 32) # Standard length for some, but SD often uses 48 bits?
24
+ # Actually, the 'invisible-watermark' library default for SD
25
+ # typically uses method='dwtDct' combined with a specific decoder.
26
+
27
+ # Let's try the standard approach for Stable Diffusion detection
28
+ # The library usually has a specific 'bytes' decoder for it.
29
+
30
+ bgr_image = None
31
+ import cv2
32
+ bgr_image = cv2.imread(filepath)
33
+ if bgr_image is None:
34
+ return result
35
+
36
+ decoder = WatermarkDecoder('bytes', 136) # Try generic length or specific
37
+ watermark = decoder.decode(bgr_image, 'dwtDct')
38
+
39
+ # Stable Diffusion's watermark often decodes to explicit bytes.
40
+ # However, a more robust way often used is checking for the specific signature
41
+ # that the library 'invisible-watermark' looks for.
42
+
43
+ # Simplifying: If we decode *something* valid/structured, it might be watermarked.
44
+ # But for 'sd_private', we verify specifically.
45
+
46
+ # Note: A simpler check using the library's built-in script logic:
47
+ # It usually converts "Stability AI" string to bits?
48
+
49
+ # If we successfully decode the known string "Stability" or derivatives.
50
+ decoded_text = watermark.decode('utf-8', errors='ignore')
51
+
52
+ if "Stability" in decoded_text or "sd_private" in decoded_text :
53
+ result["detected"] = True
54
+ result["method"] = "Invisible Watermark"
55
+ result["source"] = "Stable Diffusion"
56
+
57
+ except Exception as e:
58
+ # print(f"Watermark Check Error: {e}")
59
+ pass
60
+
61
+ return result
backend/database.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import datetime
3
+ import os
4
+
5
+ DB_NAME = os.path.join(os.path.dirname(__file__), 'database.db')
6
+
7
+ def get_db_connection():
8
+ try:
9
+ conn = sqlite3.connect(DB_NAME)
10
+ conn.row_factory = sqlite3.Row
11
+ return conn
12
+ except sqlite3.Error as e:
13
+ print(f"Database error: {e}")
14
+ return None
15
+
16
+ def init_db():
17
+ conn = get_db_connection()
18
+ if conn:
19
+ try:
20
+ conn.execute('''
21
+ CREATE TABLE IF NOT EXISTS history (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ filename TEXT NOT NULL,
24
+ prediction TEXT NOT NULL,
25
+ confidence REAL NOT NULL,
26
+ fake_probability REAL NOT NULL,
27
+ real_probability REAL NOT NULL,
28
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
29
+ )
30
+ ''')
31
+ conn.commit()
32
+ print("✅ Database initialized successfully.")
33
+ except sqlite3.Error as e:
34
+ print(f"Error initializing database: {e}")
35
+
36
+ # Migration: Add image_path, notes, tags if not exists
37
+ try:
38
+ conn.execute('ALTER TABLE history ADD COLUMN image_path TEXT')
39
+ print("✅ Added image_path column.")
40
+ except sqlite3.Error:
41
+ pass # Column likely exists
42
+
43
+ try:
44
+ conn.execute('ALTER TABLE history ADD COLUMN notes TEXT')
45
+ print("✅ Added notes column.")
46
+ except sqlite3.Error:
47
+ pass
48
+
49
+ try:
50
+ conn.execute('ALTER TABLE history ADD COLUMN tags TEXT')
51
+ print("✅ Added tags column.")
52
+ except sqlite3.Error:
53
+ pass
54
+
55
+ try:
56
+ conn.execute('ALTER TABLE history ADD COLUMN session_id TEXT')
57
+ print("✅ Added session_id column.")
58
+ except sqlite3.Error:
59
+ pass
60
+
61
+ # Create feedback table for user feedback on predictions
62
+ try:
63
+ conn.execute('''
64
+ CREATE TABLE IF NOT EXISTS feedback (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ scan_id INTEGER NOT NULL,
67
+ user_feedback TEXT NOT NULL,
68
+ predicted_label TEXT NOT NULL,
69
+ actual_label TEXT,
70
+ image_path TEXT,
71
+ confidence REAL,
72
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
73
+ FOREIGN KEY (scan_id) REFERENCES history(id)
74
+ )
75
+ ''')
76
+ conn.commit()
77
+ print("✅ Feedback table initialized successfully.")
78
+ except sqlite3.Error as e:
79
+ print(f"Error initializing feedback table: {e}")
80
+
81
+ finally:
82
+ conn.close()
83
+
84
+ def add_scan(filename, prediction, confidence, fake_prob, real_prob, image_path="", session_id=None):
85
+ conn = get_db_connection()
86
+ if conn:
87
+ try:
88
+ cursor = conn.execute('''
89
+ INSERT INTO history (filename, prediction, confidence, fake_probability, real_probability, image_path, session_id)
90
+ VALUES (?, ?, ?, ?, ?, ?, ?)
91
+ ''', (filename, prediction, confidence, fake_prob, real_prob, image_path, session_id))
92
+ conn.commit()
93
+ scan_id = cursor.lastrowid
94
+ return scan_id
95
+ except sqlite3.Error as e:
96
+ print(f"Error adding scan: {e}")
97
+ return None
98
+ finally:
99
+ conn.close()
100
+ return None
101
+
102
+ def get_history(session_id=None):
103
+ conn = get_db_connection()
104
+ if conn:
105
+ try:
106
+ query = 'SELECT * FROM history'
107
+ params = []
108
+ if session_id:
109
+ query += ' WHERE session_id = ? OR session_id IS NULL' # Allow seeing public/legacy items if desired, or strictly session specific
110
+ # Strict session isolation:
111
+ query = 'SELECT * FROM history WHERE session_id = ?'
112
+ params = [session_id]
113
+ else:
114
+ # If no session_id provided (legacy behavior), maybe show all or none?
115
+ # Let's show only items with NULL session_id to avoid leaking user data
116
+ query = 'SELECT * FROM history WHERE session_id IS NULL'
117
+
118
+ query += ' ORDER BY timestamp DESC'
119
+ cursor = conn.execute(query, params)
120
+ history = [dict(row) for row in cursor.fetchall()]
121
+ return history
122
+ except sqlite3.Error as e:
123
+ print(f"Error retrieving history: {e}")
124
+ return []
125
+ finally:
126
+ conn.close()
127
+ return []
128
+
129
+ def clear_history(session_id=None):
130
+ conn = get_db_connection()
131
+ if conn:
132
+ try:
133
+ if session_id:
134
+ conn.execute('DELETE FROM history WHERE session_id = ?', (session_id,))
135
+ else:
136
+ conn.execute('DELETE FROM history WHERE session_id IS NULL')
137
+ conn.commit()
138
+ return True
139
+ except sqlite3.Error as e:
140
+ print(f"Error clearing history: {e}")
141
+ return False
142
+ finally:
143
+ conn.close()
144
+ return False
145
+
146
+ def delete_scan(scan_id, session_id=None):
147
+ conn = get_db_connection()
148
+ if conn:
149
+ try:
150
+ if session_id:
151
+ conn.execute('DELETE FROM history WHERE id = ? AND session_id = ?', (scan_id, session_id))
152
+ else:
153
+ conn.execute('DELETE FROM history WHERE id = ? AND session_id IS NULL', (scan_id,))
154
+ conn.commit()
155
+ return True
156
+ except sqlite3.Error as e:
157
+ print(f"Error deleting scan: {e}")
158
+ return False
159
+ finally:
160
+ conn.close()
161
+ return False
162
+
163
+ def update_scan(scan_id, data):
164
+ conn = get_db_connection()
165
+ if conn:
166
+ try:
167
+ fields = []
168
+ values = []
169
+ if 'notes' in data:
170
+ fields.append("notes = ?")
171
+ values.append(data['notes'])
172
+ if 'tags' in data:
173
+ fields.append("tags = ?")
174
+ values.append(data['tags'])
175
+
176
+ if not fields:
177
+ return True
178
+
179
+ values.append(scan_id)
180
+ query = f"UPDATE history SET {', '.join(fields)} WHERE id = ?"
181
+ conn.execute(query, tuple(values))
182
+ conn.commit()
183
+ return True
184
+ except sqlite3.Error as e:
185
+ print(f"Error updating scan: {e}")
186
+ return False
187
+ finally:
188
+ conn.close()
189
+ return False
190
+
191
+ def add_feedback(scan_id, is_correct, predicted_label, actual_label=None, image_path=None, confidence=None):
192
+ """Record user feedback on a prediction"""
193
+ conn = get_db_connection()
194
+ if conn:
195
+ try:
196
+ user_feedback = 'correct' if is_correct else 'incorrect'
197
+ conn.execute('''
198
+ INSERT INTO feedback (scan_id, user_feedback, predicted_label, actual_label, image_path, confidence)
199
+ VALUES (?, ?, ?, ?, ?, ?)
200
+ ''', (scan_id, user_feedback, predicted_label, actual_label, image_path, confidence))
201
+ conn.commit()
202
+ return True
203
+ except sqlite3.Error as e:
204
+ print(f"Error adding feedback: {e}")
205
+ return False
206
+ finally:
207
+ conn.close()
208
+ return False
209
+
210
+ def get_incorrect_predictions():
211
+ """Get all incorrect predictions for model retraining"""
212
+ conn = get_db_connection()
213
+ if conn:
214
+ try:
215
+ cursor = conn.execute('''
216
+ SELECT f.*, h.filename
217
+ FROM feedback f
218
+ LEFT JOIN history h ON f.scan_id = h.id
219
+ WHERE f.user_feedback = 'incorrect'
220
+ ORDER BY f.timestamp DESC
221
+ ''')
222
+ incorrect = [dict(row) for row in cursor.fetchall()]
223
+ return incorrect
224
+ except sqlite3.Error as e:
225
+ print(f"Error retrieving incorrect predictions: {e}")
226
+ return []
227
+ finally:
228
+ conn.close()
229
+ return []
230
+
231
+ def get_feedback_stats():
232
+ """Get statistics on user feedback"""
233
+ conn = get_db_connection()
234
+ if conn:
235
+ try:
236
+ cursor = conn.execute('''
237
+ SELECT
238
+ COUNT(*) as total_feedback,
239
+ SUM(CASE WHEN user_feedback = 'correct' THEN 1 ELSE 0 END) as correct_count,
240
+ SUM(CASE WHEN user_feedback = 'incorrect' THEN 1 ELSE 0 END) as incorrect_count
241
+ FROM feedback
242
+ ''')
243
+ stats = dict(cursor.fetchone())
244
+ return stats
245
+ except sqlite3.Error as e:
246
+ print(f"Error retrieving feedback stats: {e}")
247
+ return {'total_feedback': 0, 'correct_count': 0, 'incorrect_count': 0}
248
+ finally:
249
+ conn.close()
250
+ return {'total_feedback': 0, 'correct_count': 0, 'incorrect_count': 0}
251
+
252
+ # Initialize DB on module load
253
+ init_db()
backend/requirements_web.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-cors==4.0.0
3
+ torch
4
+ torchvision
5
+ opencv-python
6
+ albumentations
7
+ Pillow
8
+ numpy
9
+ safetensors
10
+ c2pa-python
11
+ invisible-watermark==0.2.0
12
+ ExifRead
frontend/accuracy_icon.png ADDED

Git LFS Details

  • SHA256: a449f73e4fe4d0d395966158801f57a7ea75053b50d2762f657c9b97272db482
  • Pointer size: 131 Bytes
  • Size of remote file: 467 kB
frontend/analysis.html ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>Analysis Dashboard - DeepGuard</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
12
+ rel="stylesheet">
13
+ <link rel="stylesheet" href="variables.css">
14
+ <link rel="stylesheet" href="style.css">
15
+ <link rel="stylesheet" href="animations.css">
16
+ <link rel="stylesheet" href="pwa.css">
17
+ <link rel="stylesheet" href="responsive-additions.css">
18
+ <link rel="stylesheet" href="responsive-pages.css">
19
+
20
+ <!-- PWA Manifest -->
21
+ <link rel="manifest" href="manifest.json">
22
+ <meta name="theme-color" content="#E3F514">
23
+ <meta name="apple-mobile-web-app-capable" content="yes">
24
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
25
+ <meta name="apple-mobile-web-app-title" content="DeepGuard">
26
+ <link rel="apple-touch-icon" href="logo.ico">
27
+ </head>
28
+
29
+ <body class="analysis-page">
30
+ <div class="mesh-background"></div>
31
+ <div id="particles-js"></div>
32
+
33
+ <!-- Navigation -->
34
+ <nav class="navbar">
35
+ <div class="container">
36
+ <div class="nav-content">
37
+ <a href="index.html" class="logo">
38
+ <img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
39
+ <span class="logo-text">Deep<span class="gradient-text">Guard</span></span>
40
+ </a>
41
+
42
+ <!-- Hamburger Menu Button (Mobile) -->
43
+ <button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
44
+ <span></span>
45
+ <span></span>
46
+ <span></span>
47
+ </button>
48
+
49
+ <!-- Navigation Menu -->
50
+ <div class="nav-menu-wrapper">
51
+ <ul class="nav-menu">
52
+ <li><a href="index.html">Home</a></li>
53
+ <li><a href="analysis.html" class="active">Analysis</a></li>
54
+ <li><a href="history.html">History</a></li>
55
+ </ul>
56
+ <a href="index.html" class="btn-secondary-nav">← Back to Home</a>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </nav>
61
+
62
+ <main class="analysis-container">
63
+ <div class="analysis-grid">
64
+ <!-- Left Side: Upload & Preview -->
65
+ <div class="upload-section">
66
+ <div class="section-header-small">
67
+ <h2>Media Input</h2>
68
+ <p>Upload image for AI analysis</p>
69
+ </div>
70
+
71
+ <div class="upload-area animate-pulse-glow" id="uploadArea">
72
+ <div class="upload-icon animate-float">
73
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor"
74
+ stroke-width="2">
75
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
76
+ <polyline points="17 8 12 3 7 8" />
77
+ <line x1="12" y1="3" x2="12" y2="15" />
78
+ </svg>
79
+ </div>
80
+ <h3 class="upload-title">Drop Images Here</h3>
81
+ <p class="upload-description">Supports JPG, PNG, WEBP, MP4, AVI, MOV · Max 100MB</p>
82
+ <p class="upload-description" style="margin-top: 8px; font-size: 13px; opacity: 0.8;">📋 Or press
83
+ Ctrl/Cmd+V to paste from clipboard</p>
84
+ <p class="file-count-badge" id="fileCountBadge" style="display: none;"></p>
85
+ <input type="file" id="fileInput" accept="image/*,video/*" multiple hidden>
86
+ <button class="btn-primary" onclick="document.getElementById('fileInput').click()"
87
+ aria-label="Select files to upload">Select
88
+ Files</button>
89
+ <!-- Scanner Line Effect -->
90
+ <div class="scanner-line"></div>
91
+ </div>
92
+
93
+ <!-- File Queue Preview -->
94
+ <div class="file-queue-container" id="fileQueueContainer" style="display: none;">
95
+ <div class="queue-header">
96
+ <div class="queue-title-section">
97
+ <h3 class="queue-title">
98
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
99
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
100
+ aria-hidden="true">
101
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
102
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
103
+ </svg>
104
+ Upload Queue
105
+ </h3>
106
+ <span class="queue-count-badge" id="queueCount"
107
+ aria-label="Number of files in queue">0</span>
108
+ </div>
109
+ <div class="queue-actions">
110
+ <button class="btn-secondary-small" onclick="document.getElementById('fileInput').click()"
111
+ title="Add more files" aria-label="Add more files to queue">
112
+ + Add More
113
+ </button>
114
+ <button class="btn-secondary-small" onclick="clearQueue()" title="Clear all files"
115
+ aria-label="Clear all files from queue">
116
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
117
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
118
+ aria-hidden="true">
119
+ <polyline points="3 6 5 6 21 6"></polyline>
120
+ <path
121
+ d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
122
+ </path>
123
+ <line x1="10" y1="11" x2="10" y2="17"></line>
124
+ <line x1="14" y1="11" x2="14" y2="17"></line>
125
+ </svg>
126
+ Clear
127
+ </button>
128
+ </div>
129
+ </div>
130
+ <div class="file-queue" id="fileQueue"></div>
131
+ <div class="queue-footer">
132
+ <button class="btn-secondary-outline" onclick="clearQueue()" style="flex: 1;"
133
+ aria-label="Back to upload area">
134
+ ← Back to Upload
135
+ </button>
136
+ <button class="btn-primary" onclick="processUploadQueue()" style="flex: 2;" id="startUploadBtn"
137
+ aria-label="Start analysis processing">
138
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
139
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
140
+ aria-hidden="true">
141
+ <path
142
+ d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z">
143
+ </path>
144
+ <path
145
+ d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z">
146
+ </path>
147
+ <path d="M9 12H4s.55-3.03 2-5c1.62-2.2 5-3 5-3"></path>
148
+ <path d="M12 15v5s3.03-.55 5-2c2.2-1.62 3-5 3-5"></path>
149
+ </svg>
150
+ Start Analysis
151
+ </button>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="preview-area" id="previewArea" style="display: none;">
156
+ <img id="previewImage" src="" alt="Preview">
157
+ <img id="heatmapOverlay" src="" alt="Heatmap"
158
+ style="display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.5s ease;">
159
+
160
+ <div class="heatmap-toggle-container" id="heatmapToggle" style="display: none;">
161
+ <span class="toggle-label">Heatmap</span>
162
+ <label class="switch">
163
+ <input type="checkbox" id="heatmapSwitch">
164
+ <span class="slider round"></span>
165
+ </label>
166
+ </div>
167
+
168
+ <button class="btn-close-preview" onclick="resetAnalysis()" aria-label="Close preview"
169
+ title="Close Preview">×</button>
170
+ </div>
171
+ <!-- Multi-Stage Processing Overlay -->
172
+ <div id="processingOverlay" class="processing-overlay-enhanced" style="display: none;">
173
+ <div class="processing-content-enhanced">
174
+ <!-- Model Status Badge -->
175
+ <div class="model-status-badge" id="modelStatusBadge" style="display: none;">
176
+ <span class="status-dot"></span>
177
+ <span class="status-text">Model Status: <strong
178
+ id="modelStatusText">Checking...</strong></span>
179
+ </div>
180
+
181
+ <!-- Spinner -->
182
+ <div class="neural-spinner">
183
+ <div class="spinner-ring"></div>
184
+ <div class="spinner-ring"></div>
185
+ <div class="spinner-ring"></div>
186
+ </div>
187
+
188
+ <!-- Main Status Title -->
189
+ <h2 class="processing-title-enhanced" id="processingMainTitle">Analyzing Media</h2>
190
+
191
+ <!-- Step Progress Indicators -->
192
+ <div class="progress-steps" id="progressSteps">
193
+ <div class="progress-step" data-step="upload">
194
+ <div class="step-icon">📤</div>
195
+ <div class="step-label">Uploading</div>
196
+ </div>
197
+ <div class="progress-step" data-step="connect">
198
+ <div class="step-icon">🔌</div>
199
+ <div class="step-label">Connecting</div>
200
+ </div>
201
+ <div class="progress-step" data-step="warmup">
202
+ <div class="step-icon">⏳</div>
203
+ <div class="step-label">Warming Up</div>
204
+ </div>
205
+ <div class="progress-step" data-step="analyze">
206
+ <div class="step-icon">🔍</div>
207
+ <div class="step-label">Analyzing</div>
208
+ </div>
209
+ <div class="progress-step" data-step="generate">
210
+ <div class="step-icon">✨</div>
211
+ <div class="step-label">Finalizing</div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Status Message -->
216
+ <p class="processing-message" id="processingMessage">
217
+ Please don't refresh • Your media is safe and never stored
218
+ </p>
219
+
220
+ <!-- Warm-Up Alert (Hidden by default) -->
221
+ <div class="warmup-alert" id="warmupAlert" style="display: none;">
222
+ <div class="warmup-icon">⏳</div>
223
+ <div class="warmup-content">
224
+ <div class="warmup-progress-wrapper" style="width: 100%;">
225
+ <div
226
+ style="display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: #fff;">
227
+ <span>Model Waking Up</span>
228
+ <span id="warmupPercent">0%</span>
229
+ </div>
230
+ <div
231
+ style="width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; position: relative;">
232
+ <div id="warmupProgressFill"
233
+ style="width: 0%; height: 100%; background: #E3F514; border-radius: 10px; transition: width 0.3s linear; box-shadow: 0 0 10px rgba(227, 245, 20, 0.5);">
234
+ </div>
235
+ </div>
236
+ <p class="warmup-reassurance"
237
+ style="margin-top: 10px; font-size: 12px; color: rgba(255,255,255,0.6);">✓
238
+ Starting cloud instance (first run only)</p>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <!-- Right Side: Results & Metrics -->
247
+ <div class="results-section" id="resultsSection">
248
+ <div class="empty-state">
249
+ <div class="empty-icon animate-float" aria-hidden="true"
250
+ style="opacity: 0.6; color: var(--accent-yellow);">
251
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor"
252
+ stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
253
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
254
+ </svg>
255
+ </div>
256
+ <h3 style="font-family: var(--font-display); font-size: 1.5rem; margin-bottom: 8px;">Ready to
257
+ Analyze</h3>
258
+ <p style="max-width: 280px; margin: 0 auto; line-height: 1.6;">Upload media to start the DeepGuard
259
+ detection pipeline</p>
260
+ </div>
261
+
262
+ <div class="analysis-results" style="display: none;">
263
+ <div class="verdict-card" id="verdictCard">
264
+ <span class="verdict-label">DETECTION RESULT</span>
265
+ <h1 class="verdict-title" id="verdictTitle">--</h1>
266
+ <div id="detectionBadges" class="detection-badges"></div>
267
+ <div class="confidence-meter">
268
+ <div class="meter-bar">
269
+ <div class="meter-fill" id="confidenceBar"></div>
270
+ </div>
271
+ <span class="meter-value" id="confidenceValue">0%</span>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Feedback Section -->
276
+ <div class="feedback-section" id="feedbackSection" style="display: none;">
277
+ <div class="feedback-header">
278
+ <h4>Was this prediction correct?</h4>
279
+ <p>Your feedback helps improve the model</p>
280
+ </div>
281
+ <div class="feedback-buttons">
282
+ <button class="btn-feedback btn-feedback-correct" id="btnFeedbackCorrect"
283
+ onclick="submitFeedback(true)">
284
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
285
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
286
+ <polyline points="20 6 9 17 4 12"></polyline>
287
+ </svg>
288
+ Prediction is Correct
289
+ </button>
290
+ <button class="btn-feedback btn-feedback-wrong" id="btnFeedbackWrong"
291
+ onclick="submitFeedback(false)">
292
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
293
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
294
+ <line x1="18" y1="6" x2="6" y2="18"></line>
295
+ <line x1="6" y1="6" x2="18" y2="18"></line>
296
+ </svg>
297
+ Prediction is Wrong
298
+ </button>
299
+ </div>
300
+ <div class="feedback-message" id="feedbackMessage" style="display: none;"></div>
301
+ </div>
302
+
303
+ </div>
304
+
305
+ <div class="metrics-grid">
306
+ <!-- Chart Container -->
307
+ <div class="metric-card"
308
+ style="grid-column: span 2; display: flex; justify-content: center; align-items: center; padding: 20px;">
309
+ <canvas id="probabilityChart" style="max-height: 150px;"></canvas>
310
+ </div>
311
+
312
+ <div class="metric-card" style="position: relative; overflow: hidden;">
313
+ <div
314
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
315
+ </div>
316
+ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
317
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
318
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
319
+ <circle cx="12" cy="12" r="10"></circle>
320
+ <polyline points="12 6 12 12 16 14"></polyline>
321
+ </svg>
322
+ <span class="metric-label" style="margin-bottom: 0;">Scan Time</span>
323
+ </div>
324
+ <span class="metric-value" id="scanTimeDisplay">--</span>
325
+ </div>
326
+ <div class="metric-card" style="position: relative; overflow: hidden;">
327
+ <div
328
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
329
+ </div>
330
+ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
331
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
332
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
333
+ <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
334
+ <path d="M2 17l10 5 10-5"></path>
335
+ <path d="M2 12l10 5 10-5"></path>
336
+ </svg>
337
+ <span class="metric-label" style="margin-bottom: 0;">Model Version</span>
338
+ </div>
339
+ <span class="metric-value">Mark V</span>
340
+ </div>
341
+ </div>
342
+
343
+ <div class="analysis-details"
344
+ style="background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 20px; padding: 24px; position: relative; overflow: hidden;">
345
+ <div
346
+ style="position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--accent-yellow), transparent);">
347
+ </div>
348
+ <h4
349
+ style="display: flex; align-items: center; gap: 10px; font-family: var(--font-display); margin-bottom: 12px;">
350
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
351
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
352
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
353
+ <polyline points="14 2 14 8 20 8"></polyline>
354
+ <line x1="16" y1="13" x2="8" y2="13"></line>
355
+ <line x1="16" y1="17" x2="8" y2="17"></line>
356
+ <polyline points="10 9 9 9 8 9"></polyline>
357
+ </svg>
358
+ Forensic Analysis
359
+ </h4>
360
+ <p id="analysisText" style="color: var(--text-secondary); line-height: 1.7;">Waiting for analysis...
361
+ </p>
362
+
363
+ <button class="btn-primary" id="downloadReportBtn"
364
+ style="margin-top: 20px; width: 100%; display: none; position: relative; overflow: hidden;"
365
+ onclick="generatePDFReport()">
366
+ 📄 Download Forensic Report
367
+ </button>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Statistics Overview -->
374
+ <div class="container" style="max-width: 1200px; margin-top: 60px;" id="statisticsSection">
375
+ <div class="section-header">
376
+ <h2 class="section-title">Analysis <span class="gradient-text">Statistics</span></h2>
377
+ <p class="section-subtitle">Overview of your detection history</p>
378
+ </div>
379
+ <div class="statistics-grid">
380
+ <div class="stat-card"
381
+ style="--accent-color: var(--accent-yellow); animation: fadeInUp 0.5s ease backwards; animation-delay: 0.1s;">
382
+ <div
383
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
384
+ </div>
385
+ <div class="stat-icon"
386
+ style="background: rgba(227, 245, 20, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(227, 245, 20, 0.2);">
387
+ 📊</div>
388
+ <div class="stat-value" id="totalScans">0</div>
389
+ <div class="stat-label">Total Analyzed</div>
390
+ </div>
391
+ <div class="stat-card"
392
+ style="--accent-color: #ff6b6b; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.2s;">
393
+ <div
394
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #ff6b6b, transparent); border-radius: 3px 3px 0 0;">
395
+ </div>
396
+ <div class="stat-icon"
397
+ style="background: rgba(255, 107, 107, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(255, 107, 107, 0.2);">
398
+ ⚠️</div>
399
+ <div class="stat-value" id="fakeCount">0</div>
400
+ <div class="stat-label">Fake Detected</div>
401
+ </div>
402
+ <div class="stat-card"
403
+ style="--accent-color: #10b981; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.3s;">
404
+ <div
405
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #10b981, transparent); border-radius: 3px 3px 0 0;">
406
+ </div>
407
+ <div class="stat-icon"
408
+ style="background: rgba(16, 185, 129, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(16, 185, 129, 0.2);">
409
+ ✓</div>
410
+ <div class="stat-value" id="realCount">0</div>
411
+ <div class="stat-label">Real Images</div>
412
+ </div>
413
+ <div class="stat-card"
414
+ style="--accent-color: #3b82f6; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.4s;">
415
+ <div
416
+ style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #3b82f6, transparent); border-radius: 3px 3px 0 0;">
417
+ </div>
418
+ <div class="stat-icon"
419
+ style="background: rgba(59, 130, 246, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(59, 130, 246, 0.2);">
420
+ 🎯</div>
421
+ <div class="stat-value" id="avgConfidence">0%</div>
422
+ <div class="stat-label">Avg Confidence</div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+
427
+ <!-- Recent Analyses -->
428
+ <div class="container" style="max-width: 1200px; margin-top: 40px; padding-bottom: 60px;" id="recentSection">
429
+ <div class="section-header">
430
+ <h2 class="section-title">Recent <span class="gradient-text">Analyses</span></h2>
431
+ <a href="history.html" class="btn-secondary" style="font-size: 14px;">View All History →</a>
432
+ </div>
433
+ <div class="recent-grid" id="recentGrid">
434
+ <!-- Recent items will be populated here -->
435
+ </div>
436
+ </div>
437
+ </main>
438
+
439
+ <!-- Footer -->
440
+ <footer class="footer" style="position: relative; z-index: 1;">
441
+ <div class="container">
442
+ <div class="footer-content-minimal">
443
+ <div class="footer-brand">
444
+ <div class="logo">
445
+ <img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
446
+ <span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
447
+ </div>
448
+ </div>
449
+ <p class="footer-tagline-premium">Protecting digital truth with AI</p>
450
+ <div class="footer-links-premium">
451
+ <a href="https://harshasnade-deepfake-detection.hf.space" target="_blank"
452
+ class="footer-link-premium-item">
453
+ <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face">
454
+ <span>Models</span>
455
+ </a>
456
+ <a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model" target="_blank"
457
+ class="footer-link-premium-item">
458
+ <svg viewBox="0 0 24 24" fill="currentColor">
459
+ <path
460
+ d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
461
+ </svg>
462
+ <span>GitHub</span>
463
+ </a>
464
+ </div>
465
+ <div class="footer-copyright-premium">
466
+ <p>&copy; 2024 <span style="color: var(--accent-yellow); font-weight: 700;">DeepGuard</span></p>
467
+ </div>
468
+ </div>
469
+ </div>
470
+ <div class="footer-glow-enhanced"></div>
471
+ <div class="footer-glow"></div>
472
+ </footer>
473
+
474
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
475
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
476
+ <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
477
+ <!-- Toast Container -->
478
+ <div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
479
+
480
+ <script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
481
+ <script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
482
+ <script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
483
+ <script src="motion.js"></script>
484
+ <script src="script.js"></script>
485
+ <script src="mobile.js"></script>
486
+ <script src="pwa.js"></script>
487
+
488
+
489
+ </body>
490
+
491
+ </html>
frontend/analytics_icon.png ADDED

Git LFS Details

  • SHA256: 6e86ab83e494ae3853d92ceaa94b151d7726dc430abd9e2b329db94ca466dcf6
  • Pointer size: 131 Bytes
  • Size of remote file: 449 kB
frontend/animations.css ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== PREMIUM ANIMATIONS ==================== */
2
+
3
+ /* SMOOTH FADE IN UP */
4
+ @keyframes fadeInUp {
5
+ from {
6
+ opacity: 0;
7
+ transform: translate3d(0, 40px, 0);
8
+ }
9
+
10
+ to {
11
+ opacity: 1;
12
+ transform: translate3d(0, 0, 0);
13
+ }
14
+ }
15
+
16
+ .animate-fade-up {
17
+ animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
18
+ opacity: 0;
19
+ /* Init hidden */
20
+ }
21
+
22
+ /* STAGGER DELAYS (utility classes) */
23
+ .delay-100 {
24
+ animation-delay: 0.1s;
25
+ }
26
+
27
+ .delay-200 {
28
+ animation-delay: 0.2s;
29
+ }
30
+
31
+ .delay-300 {
32
+ animation-delay: 0.3s;
33
+ }
34
+
35
+ .delay-400 {
36
+ animation-delay: 0.4s;
37
+ }
38
+
39
+ .delay-500 {
40
+ animation-delay: 0.5s;
41
+ }
42
+
43
+ /* PULSING GLOW (for upload area) */
44
+ @keyframes pulseGlow {
45
+ 0% {
46
+ box-shadow: 0 0 0 0 rgba(227, 245, 20, 0.1);
47
+ border-color: rgba(255, 255, 255, 0.1);
48
+ }
49
+
50
+ 50% {
51
+ box-shadow: 0 0 30px 0 rgba(227, 245, 20, 0.2);
52
+ border-color: rgba(227, 245, 20, 0.5);
53
+ }
54
+
55
+ 100% {
56
+ box-shadow: 0 0 0 0 rgba(227, 245, 20, 0.1);
57
+ border-color: rgba(255, 255, 255, 0.1);
58
+ }
59
+ }
60
+
61
+ .animate-pulse-glow {
62
+ animation: pulseGlow 3s infinite;
63
+ will-change: box-shadow, border-color;
64
+ }
65
+
66
+ /* SHIMMER BORDER (for premium feel) */
67
+ @keyframes borderShimmer {
68
+ 0% {
69
+ background-position: 0% 50%;
70
+ }
71
+
72
+ 100% {
73
+ background-position: 200% 50%;
74
+ }
75
+ }
76
+
77
+ /* FLOATING ELEMENT */
78
+ @keyframes floatY {
79
+
80
+ 0%,
81
+ 100% {
82
+ transform: translateY(0);
83
+ }
84
+
85
+ 50% {
86
+ transform: translateY(-10px);
87
+ }
88
+ }
89
+
90
+ .animate-float {
91
+ animation: floatY 6s ease-in-out infinite;
92
+ will-change: transform;
93
+ /* Hint for GPU promotion */
94
+ }
95
+
96
+ /* SCANNER LINE (High Performance - transform based) */
97
+ @keyframes scanLine {
98
+ 0% {
99
+ transform: translateY(0%);
100
+ opacity: 0;
101
+ }
102
+
103
+ 10% {
104
+ opacity: 1;
105
+ }
106
+
107
+ 90% {
108
+ opacity: 1;
109
+ }
110
+
111
+ 100% {
112
+ transform: translateY(100%);
113
+ opacity: 0;
114
+ }
115
+ }
116
+
117
+ .scanner-line {
118
+ position: absolute;
119
+ top: 0;
120
+ /* Positioned at top, animated via transform */
121
+ left: 0;
122
+ right: 0;
123
+ width: 100%;
124
+ height: 2px;
125
+ background: var(--accent-yellow);
126
+ box-shadow: 0 0 10px var(--accent-yellow);
127
+ animation: scanLine 2s linear infinite;
128
+ z-index: 10;
129
+ pointer-events: none;
130
+ will-change: transform, opacity;
131
+ /* Hint for GPU */
132
+ }
133
+
134
+ /* ==================== ENHANCED PROCESSING OVERLAY ==================== */
135
+
136
+ /* Neural Spinner Animation */
137
+ @keyframes neuralPulse {
138
+
139
+ 0%,
140
+ 100% {
141
+ transform: scale(1);
142
+ opacity: 0.8;
143
+ }
144
+
145
+ 50% {
146
+ transform: scale(1.1);
147
+ opacity: 1;
148
+ }
149
+ }
150
+
151
+ @keyframes spinRing {
152
+ 0% {
153
+ transform: rotate(0deg);
154
+ }
155
+
156
+ 100% {
157
+ transform: rotate(360deg);
158
+ }
159
+ }
160
+
161
+ .neural-spinner {
162
+ position: relative;
163
+ width: 80px;
164
+ height: 80px;
165
+ margin: 0 auto 30px;
166
+ }
167
+
168
+ .neural-spinner .spinner-ring {
169
+ position: absolute;
170
+ width: 100%;
171
+ height: 100%;
172
+ border: 3px solid transparent;
173
+ border-top-color: var(--accent-yellow);
174
+ border-radius: 50%;
175
+ animation: spinRing 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
176
+ }
177
+
178
+ .neural-spinner .spinner-ring:nth-child(2) {
179
+ width: 70%;
180
+ height: 70%;
181
+ top: 15%;
182
+ left: 15%;
183
+ border-top-color: rgba(227, 245, 20, 0.6);
184
+ animation-duration: 2s;
185
+ animation-direction: reverse;
186
+ }
187
+
188
+ .neural-spinner .spinner-ring:nth-child(3) {
189
+ width: 50%;
190
+ height: 50%;
191
+ top: 25%;
192
+ left: 25%;
193
+ border-top-color: rgba(227, 245, 20, 0.3);
194
+ animation-duration: 2.5s;
195
+ }
196
+
197
+ /* Progress Steps Animation */
198
+ @keyframes stepPulse {
199
+
200
+ 0%,
201
+ 100% {
202
+ transform: scale(1);
203
+ opacity: 0.5;
204
+ }
205
+
206
+ 50% {
207
+ transform: scale(1.15);
208
+ opacity: 1;
209
+ }
210
+ }
211
+
212
+ @keyframes stepGlow {
213
+
214
+ 0%,
215
+ 100% {
216
+ box-shadow: 0 0 10px rgba(227, 245, 20, 0.3);
217
+ }
218
+
219
+ 50% {
220
+ box-shadow: 0 0 20px rgba(227, 245, 20, 0.6);
221
+ }
222
+ }
223
+
224
+ .progress-steps {
225
+ display: flex;
226
+ justify-content: center;
227
+ gap: 20px;
228
+ margin: 30px 0;
229
+ flex-wrap: wrap;
230
+ }
231
+
232
+ .progress-step {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: center;
236
+ gap: 8px;
237
+ opacity: 0.4;
238
+ transition: all 0.4s ease;
239
+ }
240
+
241
+ .progress-step .step-icon {
242
+ width: 50px;
243
+ height: 50px;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ font-size: 24px;
248
+ background: rgba(255, 255, 255, 0.05);
249
+ border: 2px solid rgba(255, 255, 255, 0.1);
250
+ border-radius: 50%;
251
+ transition: all 0.4s ease;
252
+ }
253
+
254
+ .progress-step .step-label {
255
+ font-size: 12px;
256
+ font-weight: 500;
257
+ color: rgba(255, 255, 255, 0.6);
258
+ text-transform: uppercase;
259
+ letter-spacing: 0.5px;
260
+ }
261
+
262
+ .progress-step.active {
263
+ opacity: 1;
264
+ }
265
+
266
+ .progress-step.active .step-icon {
267
+ background: rgba(227, 245, 20, 0.1);
268
+ border-color: var(--accent-yellow);
269
+ animation: stepPulse 2s ease-in-out infinite, stepGlow 2s ease-in-out infinite;
270
+ }
271
+
272
+ .progress-step.active .step-label {
273
+ color: var(--accent-yellow);
274
+ }
275
+
276
+ .progress-step.completed {
277
+ opacity: 0.7;
278
+ }
279
+
280
+ .progress-step.completed .step-icon {
281
+ background: rgba(16, 185, 129, 0.1);
282
+ border-color: #10B981;
283
+ }
284
+
285
+ .progress-step.completed .step-label {
286
+ color: #10B981;
287
+ }
288
+
289
+ /* Processing Overlay Enhanced */
290
+ .processing-overlay-enhanced {
291
+ position: fixed;
292
+ top: 0;
293
+ left: 0;
294
+ right: 0;
295
+ bottom: 0;
296
+ background: rgba(0, 0, 0, 0.95);
297
+ backdrop-filter: blur(10px);
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ z-index: 10000;
302
+ padding: 20px;
303
+ }
304
+
305
+ .processing-content-enhanced {
306
+ max-width: 600px;
307
+ width: 100%;
308
+ text-align: center;
309
+ animation: fadeInUp 0.5s ease-out;
310
+ }
311
+
312
+ .model-status-badge {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ gap: 8px;
316
+ padding: 8px 16px;
317
+ background: rgba(255, 255, 255, 0.05);
318
+ border: 1px solid rgba(255, 255, 255, 0.1);
319
+ border-radius: 20px;
320
+ margin-bottom: 20px;
321
+ font-size: 13px;
322
+ }
323
+
324
+ .model-status-badge .status-dot {
325
+ width: 8px;
326
+ height: 8px;
327
+ background: var(--accent-yellow);
328
+ border-radius: 50%;
329
+ animation: neuralPulse 2s ease-in-out infinite;
330
+ }
331
+
332
+ .model-status-badge .status-text {
333
+ color: rgba(255, 255, 255, 0.8);
334
+ }
335
+
336
+ .processing-title-enhanced {
337
+ font-size: 28px;
338
+ font-weight: 700;
339
+ color: #fff;
340
+ margin-bottom: 20px;
341
+ letter-spacing: 0.5px;
342
+ }
343
+
344
+ .processing-message {
345
+ font-size: 14px;
346
+ color: rgba(255, 255, 255, 0.6);
347
+ margin-top: 20px;
348
+ }
349
+
350
+ /* Warm-Up Alert */
351
+ .warmup-alert {
352
+ margin-top: 30px;
353
+ padding: 20px;
354
+ background: rgba(227, 245, 20, 0.05);
355
+ border: 1px solid rgba(227, 245, 20, 0.2);
356
+ border-radius: 12px;
357
+ text-align: left;
358
+ display: flex;
359
+ gap: 15px;
360
+ animation: fadeInUp 0.5s ease-out;
361
+ }
362
+
363
+ .warmup-icon {
364
+ font-size: 32px;
365
+ flex-shrink: 0;
366
+ }
367
+
368
+ .warmup-content h4 {
369
+ margin: 0 0 10px 0;
370
+ font-size: 16px;
371
+ font-weight: 600;
372
+ color: var(--accent-yellow);
373
+ }
374
+
375
+ .warmup-content p {
376
+ margin: 8px 0;
377
+ font-size: 14px;
378
+ color: rgba(255, 255, 255, 0.8);
379
+ line-height: 1.5;
380
+ }
381
+
382
+ .warmup-content .warmup-reason {
383
+ font-size: 13px;
384
+ color: rgba(255, 255, 255, 0.6);
385
+ font-style: italic;
386
+ }
387
+
388
+ .warmup-content .warmup-reassurance {
389
+ font-size: 13px;
390
+ color: #10B981;
391
+ font-weight: 500;
392
+ }
393
+
394
+ /* ==================== ACCESSIBILITY: REDUCED MOTION ==================== */
395
+ @media (prefers-reduced-motion: reduce) {
396
+
397
+ *,
398
+ *::before,
399
+ *::after {
400
+ animation-duration: 0.01ms !important;
401
+ animation-iteration-count: 1 !important;
402
+ transition-duration: 0.01ms !important;
403
+ }
404
+
405
+ .animate-fade-up,
406
+ .animate-float,
407
+ .animate-pulse-glow {
408
+ animation: none !important;
409
+ }
410
+ }
411
+
412
+ /* Reduced animations on mobile for performance */
413
+ @media (max-width: 768px) {
414
+ .animate-float {
415
+ animation-duration: 8s;
416
+ }
417
+
418
+ .animate-pulse-glow {
419
+ animation-duration: 4s;
420
+ }
421
+
422
+ .progress-steps {
423
+ gap: 12px;
424
+ }
425
+
426
+ .progress-step .step-icon {
427
+ width: 40px;
428
+ height: 40px;
429
+ font-size: 20px;
430
+ }
431
+
432
+ .progress-step .step-label {
433
+ font-size: 10px;
434
+ }
435
+
436
+ .warmup-alert {
437
+ flex-direction: column;
438
+ text-align: center;
439
+ }
440
+ }
frontend/assets/demo_part1.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:82193473a0bb0ffb28ec8cdb4543f550ac31acffe94724a48993971ee0c1da7c
3
+ size 31037318
frontend/assets/demo_part2.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2babb244db96caa7b0633c7650139e964471d1556e6fcd89300a211c74faf00
3
+ size 18119552
frontend/assets/displacement.png ADDED

Git LFS Details

  • SHA256: 71d43d18477fdcc44e2671abfbabb3d2c4c1fe442cb83973f7a4e4bb5d1c3bcb
  • Pointer size: 131 Bytes
  • Size of remote file: 778 kB
frontend/assets/extension_demo.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d44b36bc39b1d4fcee78bac3c59d927c9ffef78434488249ba67cf95c0faeea
3
+ size 36407751
frontend/assets/gemini_reveal.png ADDED

Git LFS Details

  • SHA256: fc6e30c66d84e07c1c456917322af07003894d7d3b8e55213840048ab3faf310
  • Pointer size: 132 Bytes
  • Size of remote file: 5.54 MB
frontend/comparison_real.png ADDED

Git LFS Details

  • SHA256: f0e32b279f26c3fae34d4352d982d0f240d6bb6054d942329af16274d83f8abd
  • Pointer size: 131 Bytes
  • Size of remote file: 749 kB
frontend/config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ const CONFIG = {
2
+ // API Base URL - Automatically selects between Localhost and Production
3
+ API_BASE_URL: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
4
+ ? 'http://localhost:7860'
5
+ : 'https://harshasnade-deepfake-detection.hf.space'
6
+ };
frontend/deep_learning_icon.png ADDED

Git LFS Details

  • SHA256: 669df775517eae7afc7a15640b01b6eae0222e1b191afddf9b1f6f262d1e0a6c
  • Pointer size: 131 Bytes
  • Size of remote file: 552 kB
frontend/extension.css ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Extension Section Styles */
2
+ .extension-section {
3
+ padding: 100px 0;
4
+ position: relative;
5
+ background: linear-gradient(180deg, #000 0%, #0a0a0a 100%);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .extension-container {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 60px;
13
+ position: relative;
14
+ z-index: 2;
15
+ }
16
+
17
+ .extension-content {
18
+ flex: 1;
19
+ text-align: left;
20
+ }
21
+
22
+ .extension-badge {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ gap: 8px;
26
+ padding: 8px 16px;
27
+ background: rgba(227, 245, 20, 0.1);
28
+ border: 1px solid rgba(227, 245, 20, 0.2);
29
+ border-radius: 100px;
30
+ color: var(--accent-yellow);
31
+ font-size: 14px;
32
+ margin-bottom: 24px;
33
+ }
34
+
35
+ .extension-title {
36
+ font-size: 48px;
37
+ line-height: 1.1;
38
+ margin-bottom: 20px;
39
+ font-family: 'Space Grotesk', sans-serif;
40
+ }
41
+
42
+ .extension-description {
43
+ font-size: 18px;
44
+ color: #999;
45
+ margin-bottom: 32px;
46
+ line-height: 1.6;
47
+ }
48
+
49
+ .chrome-btn {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: 12px;
53
+ background: #fff;
54
+ color: #000;
55
+ padding: 16px 32px;
56
+ border-radius: 12px;
57
+ font-weight: 700;
58
+ font-size: 18px;
59
+ transition: all 0.3s ease;
60
+ text-decoration: none;
61
+ }
62
+
63
+ .chrome-btn:hover {
64
+ transform: translateY(-2px);
65
+ box-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);
66
+ }
67
+
68
+ .chrome-icon {
69
+ width: 24px;
70
+ height: 24px;
71
+ }
72
+
73
+ .extension-visual {
74
+ flex: 1.2;
75
+ }
76
+
77
+ /* Reusing window styles but ensuring specific context */
78
+ .extension-visual .video-window-container {
79
+ box-shadow: -30px 30px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
80
+ }
81
+
82
+ @media (max-width: 968px) {
83
+ .extension-container {
84
+ flex-direction: column-reverse;
85
+ /* Video on top on mobile? Or text on top? usually text on top. column-reverse puts visual first if html is visual last. */
86
+ flex-direction: column;
87
+ text-align: center;
88
+ }
89
+
90
+ .extension-content {
91
+ text-align: center;
92
+ }
93
+ }
frontend/favicon.ico ADDED

Git LFS Details

  • SHA256: 8d65e65acd366679f3818bcba75ee7aa31d41639be9bd7dc1a715ddb45505246
  • Pointer size: 131 Bytes
  • Size of remote file: 780 kB
frontend/hero_reveal.js ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Hero Reveal Effect
3
+ * Adapts the fluid reveal effect for the hero section of the main landing page.
4
+ */
5
+
6
+ class HeroFluidReveal {
7
+ constructor() {
8
+ this.container = document.getElementById('heroRevealContainer');
9
+ this.canvas = document.getElementById('revealCanvas');
10
+
11
+ if (!this.container || !this.canvas) {
12
+ console.warn('HeroFluidReveal: Container or Canvas not found.');
13
+ return;
14
+ }
15
+
16
+ this.ctx = this.canvas.getContext('2d');
17
+
18
+ // Use the new IDs we added to index.html
19
+ this.bgImg = document.getElementById('hero-img-bg');
20
+ this.revealImg = document.getElementById('hero-img-reveal');
21
+
22
+ // Initialize state
23
+ this.width = this.container.offsetWidth;
24
+ this.height = this.container.offsetHeight;
25
+
26
+ // Mouse state (relative to container)
27
+ this.mouse = { x: this.width / 2, y: this.height / 2 };
28
+ this.targetMouse = { x: this.width / 2, y: this.height / 2 };
29
+ this.isMouseOver = false;
30
+
31
+ // Blob state
32
+ this.blob = {
33
+ x: this.width / 2,
34
+ y: this.height / 2,
35
+ vx: 0,
36
+ vy: 0,
37
+ radius: 250 // Slightly smaller for hero section if needed, or keep 300
38
+ };
39
+
40
+ // Configuration
41
+ this.numPoints = 20;
42
+ this.points = [];
43
+ this.init();
44
+ }
45
+
46
+ init() {
47
+ // Point class definition (same as before)
48
+ this.Point = class {
49
+ constructor(angle, radius) {
50
+ this.angle = angle;
51
+ this.baseRadius = radius;
52
+ this.radius = radius;
53
+ this.x = 0;
54
+ this.y = 0;
55
+ this.noiseOffset = Math.random() * 1000;
56
+ this.speed = 0.002 + Math.random() * 0.003;
57
+ }
58
+
59
+ update(centerX, centerY, velocityX, velocityY, time) {
60
+ const noise = Math.sin(time * this.speed + this.noiseOffset) * 20;
61
+ const dirX = Math.cos(this.angle);
62
+ const dirY = Math.sin(this.angle);
63
+ const dot = dirX * velocityX + dirY * velocityY;
64
+ const stretch = dot * 1.5;
65
+ const currentRadius = this.baseRadius + noise - stretch;
66
+ this.x = centerX + Math.cos(this.angle) * currentRadius;
67
+ this.y = centerY + Math.sin(this.angle) * currentRadius;
68
+ }
69
+ };
70
+
71
+ this.resize();
72
+ window.addEventListener('resize', () => this.resize());
73
+
74
+ // Listen to window mouse events to avoid z-index blocking by hero content
75
+ window.addEventListener('mousemove', (e) => this.onMouseMove(e));
76
+
77
+ // Optional: We can still use container bounds to "pause" or hide if needed,
78
+ // but for a background effect, continuous tracking is usually better.
79
+ // Removed container-specific enter/leave to prevent stuttering at edges of children.
80
+
81
+ // Initialize points
82
+ for (let i = 0; i < this.numPoints; i++) {
83
+ const angle = (i / this.numPoints) * Math.PI * 2;
84
+ this.points.push(new this.Point(angle, this.blob.radius));
85
+ }
86
+
87
+ // Start loop
88
+ requestAnimationFrame((t) => this.render(t));
89
+ }
90
+
91
+ resize() {
92
+ this.width = this.container.offsetWidth;
93
+ this.height = this.container.offsetHeight;
94
+ this.canvas.width = this.width;
95
+ this.canvas.height = this.height;
96
+ }
97
+
98
+ onMouseMove(e) {
99
+ // Calculate mouse position relative to container
100
+ const rect = this.container.getBoundingClientRect();
101
+ this.targetMouse.x = e.clientX - rect.left;
102
+ this.targetMouse.y = e.clientY - rect.top;
103
+ }
104
+
105
+ updateBlob() {
106
+ const dx = this.targetMouse.x - this.blob.x;
107
+ const dy = this.targetMouse.y - this.blob.y;
108
+
109
+ // Ease
110
+ const ease = 0.25;
111
+ this.blob.vx += dx * ease;
112
+ this.blob.vy += dy * ease;
113
+
114
+ // Friction
115
+ this.blob.vx *= 0.75;
116
+ this.blob.vy *= 0.75;
117
+
118
+ this.blob.x += this.blob.vx;
119
+ this.blob.y += this.blob.vy;
120
+
121
+ const velX = (this.targetMouse.x - this.blob.x) * 0.1;
122
+ const velY = (this.targetMouse.y - this.blob.y) * 0.1;
123
+
124
+ return { velX, velY };
125
+ }
126
+
127
+ drawBlobPath(time, velX, velY) {
128
+ this.ctx.beginPath();
129
+ this.points.forEach(p => p.update(this.blob.x, this.blob.y, velX, velY, time));
130
+
131
+ const p0 = this.points[0];
132
+ const pLast = this.points[this.points.length - 1];
133
+ const midX = (p0.x + pLast.x) / 2;
134
+ const midY = (p0.y + pLast.y) / 2;
135
+
136
+ this.ctx.moveTo(midX, midY);
137
+
138
+ for (let i = 0; i < this.points.length; i++) {
139
+ const p = this.points[i];
140
+ const nextP = this.points[(i + 1) % this.points.length];
141
+ const nextMidX = (p.x + nextP.x) / 2;
142
+ const nextMidY = (p.y + nextP.y) / 2;
143
+ this.ctx.quadraticCurveTo(p.x, p.y, nextMidX, nextMidY);
144
+ }
145
+ this.ctx.closePath();
146
+ }
147
+
148
+ drawImageCover(img) {
149
+ const imgRatio = img.width / img.height;
150
+ const canvasRatio = this.width / this.height;
151
+ let drawW, drawH, curX, curY;
152
+
153
+ if (imgRatio > canvasRatio) {
154
+ drawH = this.height;
155
+ drawW = drawH * imgRatio;
156
+ curX = (this.width - drawW) / 2;
157
+ curY = 0;
158
+ } else {
159
+ drawW = this.width;
160
+ drawH = drawW / imgRatio;
161
+ curX = 0;
162
+ curY = (this.height - drawH) / 2;
163
+ }
164
+
165
+ this.ctx.drawImage(img, curX, curY, drawW, drawH);
166
+ }
167
+
168
+ render(time) {
169
+ this.ctx.clearRect(0, 0, this.width, this.height);
170
+
171
+ // IMPORTANT: We do NOT draw the background image on the canvas.
172
+ // The background image is an <img> tag in HTML (id="hero-img-bg").
173
+ // The canvas sits ON TOP of it.
174
+ // The canvas draws the "Reveal" image ONLY inside the blob.
175
+
176
+ // 1. Update Physics
177
+ const velocity = this.updateBlob();
178
+
179
+ // 2. Create Mask and Draw Reveal
180
+ this.ctx.save();
181
+ this.drawBlobPath(time, velocity.velX, velocity.velY);
182
+ this.ctx.clip();
183
+
184
+ // Apply theme-based filter
185
+ const theme = document.documentElement.getAttribute('data-theme');
186
+ if (theme === 'light') {
187
+ // Rotates Yellow (~60deg) to Blue (~240deg) -> +180deg
188
+ this.ctx.filter = 'hue-rotate(190deg) brightness(1.1) saturate(1.2)';
189
+ } else {
190
+ this.ctx.filter = 'none';
191
+ }
192
+
193
+ if (this.revealImg && this.revealImg.complete) {
194
+ this.drawImageCover(this.revealImg);
195
+ }
196
+
197
+ // Optional: Add a subtle border/glow to the reveal edge
198
+ // this.ctx.lineWidth = 2;
199
+ // this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
200
+ // this.ctx.stroke();
201
+
202
+ this.ctx.restore();
203
+
204
+ requestAnimationFrame((t) => this.render(t));
205
+ }
206
+ }
207
+
208
+ // Initialize when DOM is ready
209
+ document.addEventListener('DOMContentLoaded', () => {
210
+ // Wait slightly to ensure images start loading?
211
+ // Actually window.onload is safer for images but DOMContentLoaded is faster for UI.
212
+ // The class checks .complete so it handles loading.
213
+ new HeroFluidReveal();
214
+ });
frontend/history.css ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== PREMIUM HISTORY STYLES ==================== */
2
+
3
+ /* --- Grid System --- */
4
+ .history-grid {
5
+ display: grid;
6
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
7
+ gap: 24px;
8
+ padding-bottom: 40px;
9
+ }
10
+
11
+ /* --- Card Styling (Glass + Neon) --- */
12
+ .history-card,
13
+ .grid-card {
14
+ background: rgba(20, 20, 20, 0.6);
15
+ backdrop-filter: blur(12px);
16
+ -webkit-backdrop-filter: blur(12px);
17
+ border: 1px solid rgba(255, 255, 255, 0.08);
18
+ border-radius: 24px;
19
+ padding: 20px;
20
+ display: flex;
21
+ flex-direction: column;
22
+ transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
23
+ position: relative;
24
+ overflow: hidden;
25
+ animation: fadeInUp 0.6s ease backwards;
26
+ }
27
+
28
+ .history-card:hover,
29
+ .grid-card:hover {
30
+ transform: translateY(-8px);
31
+ border-color: rgba(227, 245, 20, 0.3);
32
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5), 0 0 20px rgba(227, 245, 20, 0.05);
33
+ background: rgba(255, 255, 255, 0.04);
34
+ }
35
+
36
+ /* Selection State */
37
+ .grid-card.selected {
38
+ border-color: var(--accent-yellow);
39
+ box-shadow: 0 0 0 2px rgba(227, 245, 20, 0.2);
40
+ background: rgba(227, 245, 20, 0.05);
41
+ }
42
+
43
+ /* --- Preview Image --- */
44
+ .grid-preview {
45
+ width: 100%;
46
+ aspect-ratio: 16/9;
47
+ object-fit: cover;
48
+ border-radius: 16px;
49
+ margin-bottom: 16px;
50
+ background: #000;
51
+ border: 1px solid rgba(255, 255, 255, 0.05);
52
+ }
53
+
54
+ /* --- Badges (Neon Glow) --- */
55
+ .table-badge,
56
+ .history-badge,
57
+ .recent-badge {
58
+ padding: 6px 12px;
59
+ border-radius: 100px;
60
+ font-weight: 700;
61
+ font-size: 11px;
62
+ text-transform: uppercase;
63
+ letter-spacing: 1px;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ backdrop-filter: blur(4px);
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .fake,
72
+ .badge-fake,
73
+ .verdict-fake {
74
+ background: rgba(227, 245, 20, 0.1);
75
+ color: #E3F514;
76
+ border: 1px solid rgba(227, 245, 20, 0.3);
77
+ box-shadow: 0 0 15px rgba(227, 245, 20, 0.15);
78
+ }
79
+
80
+ .real,
81
+ .badge-real,
82
+ .verdict-real {
83
+ background: rgba(16, 185, 129, 0.1);
84
+ color: #10B981;
85
+ border: 1px solid rgba(16, 185, 129, 0.3);
86
+ box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
87
+ }
88
+
89
+ /* --- Controls Bar (Refined) --- */
90
+ .history-controls {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: var(--gap-sm, 16px);
94
+ margin: var(--gap-md, 30px) 0 var(--gap-sm, 24px) 0;
95
+ padding: var(--card-padding, 20px);
96
+ background: rgba(10, 10, 10, 0.6);
97
+ backdrop-filter: blur(16px);
98
+ border: 1px solid rgba(255, 255, 255, 0.08);
99
+ border-radius: 20px;
100
+ align-items: center;
101
+ }
102
+
103
+ .search-container {
104
+ flex: 2;
105
+ min-width: 250px;
106
+ }
107
+
108
+ .search-input,
109
+ .filter-select {
110
+ width: 100%;
111
+ background: rgba(255, 255, 255, 0.03);
112
+ border: 1px solid rgba(255, 255, 255, 0.1);
113
+ border-radius: 12px;
114
+ padding: 12px 18px;
115
+ color: #fff;
116
+ font-size: 14px;
117
+ transition: all 0.3s ease;
118
+ font-family: var(--font-primary);
119
+ }
120
+
121
+ .search-input:focus,
122
+ .filter-select:focus {
123
+ outline: none;
124
+ border-color: var(--accent-yellow);
125
+ background: rgba(255, 255, 255, 0.06);
126
+ box-shadow: 0 0 15px rgba(227, 245, 20, 0.1);
127
+ }
128
+
129
+ .filter-controls {
130
+ display: flex;
131
+ gap: 12px;
132
+ flex: 3;
133
+ }
134
+
135
+ .btn-export,
136
+ .btn-clear-all {
137
+ padding: 12px 20px;
138
+ border-radius: 12px;
139
+ font-weight: 600;
140
+ font-size: 13px;
141
+ cursor: pointer;
142
+ transition: all 0.3s ease;
143
+ border: 1px solid transparent;
144
+ }
145
+
146
+ .btn-export {
147
+ background: rgba(255, 255, 255, 0.05);
148
+ color: #fff;
149
+ border-color: rgba(255, 255, 255, 0.1);
150
+ }
151
+
152
+ .btn-export:hover {
153
+ background: rgba(227, 245, 20, 0.1);
154
+ color: var(--accent-yellow);
155
+ border-color: var(--accent-yellow);
156
+ }
157
+
158
+ .btn-clear-all {
159
+ background: rgba(255, 59, 48, 0.05);
160
+ color: #ff3b30;
161
+ border-color: rgba(255, 59, 48, 0.2);
162
+ }
163
+
164
+ .btn-clear-all:hover {
165
+ background: rgba(255, 59, 48, 0.15);
166
+ border-color: #ff3b30;
167
+ box-shadow: 0 0 15px rgba(255, 59, 48, 0.1);
168
+ }
169
+
170
+ /* --- Filter Chips --- */
171
+ .filter-chips {
172
+ display: flex;
173
+ gap: 8px;
174
+ width: 100%;
175
+ margin-top: 4px;
176
+ padding-top: 16px;
177
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
178
+ }
179
+
180
+ .chip {
181
+ padding: 8px 16px;
182
+ border-radius: 100px;
183
+ background: rgba(255, 255, 255, 0.03);
184
+ border: 1px solid rgba(255, 255, 255, 0.08);
185
+ color: #888;
186
+ font-size: 13px;
187
+ font-weight: 500;
188
+ cursor: pointer;
189
+ transition: all 0.3s ease;
190
+ }
191
+
192
+ .chip:hover {
193
+ background: rgba(255, 255, 255, 0.08);
194
+ color: #fff;
195
+ }
196
+
197
+ .chip.active {
198
+ background: var(--accent-yellow);
199
+ color: #000;
200
+ border-color: var(--accent-yellow);
201
+ font-weight: 600;
202
+ box-shadow: 0 0 15px rgba(227, 245, 20, 0.3);
203
+ }
204
+
205
+ /* --- History Table (Glass) --- */
206
+ .history-table-container {
207
+ background: rgba(10, 10, 10, 0.4);
208
+ backdrop-filter: blur(12px);
209
+ border: 1px solid rgba(255, 255, 255, 0.05);
210
+ border-radius: 24px;
211
+ overflow: hidden;
212
+ margin-bottom: 60px;
213
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
214
+ }
215
+
216
+ .history-table {
217
+ width: 100%;
218
+ border-collapse: separate;
219
+ border-spacing: 0;
220
+ }
221
+
222
+ .history-table th {
223
+ background: rgba(255, 255, 255, 0.02);
224
+ padding: 20px 24px;
225
+ text-align: left;
226
+ color: var(--text-secondary);
227
+ font-weight: 600;
228
+ font-size: 12px;
229
+ text-transform: uppercase;
230
+ letter-spacing: 1.5px;
231
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
232
+ }
233
+
234
+ .history-table td {
235
+ padding: 20px 24px;
236
+ color: #fff;
237
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
238
+ vertical-align: middle;
239
+ transition: background 0.2s;
240
+ }
241
+
242
+ .history-table tbody tr {
243
+ transition: all 0.2s;
244
+ }
245
+
246
+ .history-table tbody tr:hover {
247
+ background: rgba(255, 255, 255, 0.03);
248
+ transform: scale(1.005);
249
+ /* Subtle scale interaction */
250
+ }
251
+
252
+ /* Table Preview */
253
+ .table-preview-img {
254
+ width: 64px;
255
+ height: 64px;
256
+ object-fit: cover;
257
+ border-radius: 12px;
258
+ border: 1px solid rgba(255, 255, 255, 0.1);
259
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
260
+ }
261
+
262
+ .table-filename {
263
+ font-weight: 500;
264
+ color: #fff;
265
+ font-family: var(--font-display);
266
+ letter-spacing: 0.5px;
267
+ }
268
+
269
+ .table-date {
270
+ color: var(--text-secondary);
271
+ font-size: 13px;
272
+ font-variant-numeric: tabular-nums;
273
+ }
274
+
275
+ /* Actions in Table */
276
+ .table-actions {
277
+ display: flex;
278
+ gap: 8px;
279
+ opacity: 0.6;
280
+ transition: opacity 0.3s;
281
+ }
282
+
283
+ .history-table tbody tr:hover .table-actions {
284
+ opacity: 1;
285
+ }
286
+
287
+ .btn-table-action {
288
+ background: rgba(255, 255, 255, 0.05);
289
+ border: 1px solid rgba(255, 255, 255, 0.1);
290
+ color: #fff;
291
+ width: 36px;
292
+ height: 36px;
293
+ padding: 0;
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+ border-radius: 10px;
298
+ transition: all 0.2s;
299
+ }
300
+
301
+ .btn-table-action:hover {
302
+ background: var(--accent-yellow);
303
+ color: #000;
304
+ border-color: var(--accent-yellow);
305
+ transform: translateY(-2px);
306
+ }
307
+
308
+ .btn-table-delete:hover {
309
+ background: #ff3b30;
310
+ color: #fff;
311
+ border-color: #ff3b30;
312
+ }
313
+
314
+ /* --- Empty State --- */
315
+ .empty-state {
316
+ padding: 80px 20px;
317
+ }
318
+
319
+ .empty-icon {
320
+ font-size: 56px;
321
+ margin-bottom: 24px;
322
+ opacity: 0.8;
323
+ filter: drop-shadow(0 0 20px rgba(227, 245, 20, 0.3));
324
+ }
325
+
326
+ .empty-state h3 {
327
+ font-size: 24px;
328
+ font-family: var(--font-display);
329
+ margin-bottom: 12px;
330
+ }
331
+
332
+ /* --- Pagination (Refined) --- */
333
+ .pagination {
334
+ display: flex;
335
+ justify-content: center;
336
+ align-items: center;
337
+ gap: 20px;
338
+ padding: 30px 0;
339
+ }
340
+
341
+ .btn-page {
342
+ width: 44px;
343
+ height: 44px;
344
+ border-radius: 12px;
345
+ background: rgba(255, 255, 255, 0.05);
346
+ border: 1px solid rgba(255, 255, 255, 0.1);
347
+ color: #fff;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ cursor: pointer;
352
+ transition: all 0.2s;
353
+ }
354
+
355
+ .btn-page:hover:not(:disabled) {
356
+ background: var(--accent-yellow);
357
+ color: #000;
358
+ border-color: var(--accent-yellow);
359
+ }
360
+
361
+ .btn-page:disabled {
362
+ opacity: 0.3;
363
+ cursor: not-allowed;
364
+ }
365
+
366
+ .page-info {
367
+ font-variant-numeric: tabular-nums;
368
+ color: var(--text-secondary);
369
+ font-weight: 500;
370
+ }
371
+
372
+
373
+
374
+ /* ==================== GRID VIEW STYLES ==================== */
375
+ .history-grid-container {
376
+ display: grid;
377
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
378
+ gap: 20px;
379
+ margin-bottom: 40px;
380
+ }
381
+
382
+ .grid-card {
383
+ background: rgba(17, 17, 17, 0.6);
384
+ backdrop-filter: blur(10px);
385
+ border: 1px solid rgba(255, 255, 255, 0.05);
386
+ border-radius: 20px;
387
+ overflow: hidden;
388
+ transition: all 0.4s ease;
389
+ position: relative;
390
+ }
391
+
392
+ .grid-card:hover {
393
+ transform: translateY(-5px);
394
+ border-color: var(--accent-yellow);
395
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
396
+ }
397
+
398
+ .grid-preview {
399
+ width: 100%;
400
+ aspect-ratio: 16/9;
401
+ object-fit: cover;
402
+ cursor: pointer;
403
+ }
404
+
405
+ .grid-content {
406
+ padding: 16px;
407
+ }
408
+
409
+ .grid-header {
410
+ display: flex;
411
+ justify-content: space-between;
412
+ align-items: center;
413
+ margin-bottom: 12px;
414
+ }
415
+
416
+ .grid-title {
417
+ font-weight: 600;
418
+ font-size: 14px;
419
+ white-space: nowrap;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ margin-bottom: 4px;
423
+ }
424
+
425
+ .grid-date {
426
+ font-size: 12px;
427
+ color: #666;
428
+ }
429
+
430
+ /* ==================== BULK SELECTION ==================== */
431
+ .batch-actions-bar {
432
+ position: fixed;
433
+ bottom: 30px;
434
+ left: 50%;
435
+ transform: translateX(-50%) translateY(100px);
436
+ background: var(--accent-yellow);
437
+ color: #000;
438
+ padding: 12px 24px;
439
+ border-radius: 50px;
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 20px;
443
+ box-shadow: 0 10px 40px rgba(227, 245, 20, 0.3);
444
+ z-index: 1000;
445
+ transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
446
+ }
447
+
448
+ .batch-actions-bar.active {
449
+ transform: translateX(-50%) translateY(0);
450
+ }
451
+
452
+ .selection-info {
453
+ font-weight: 700;
454
+ font-size: 14px;
455
+ }
456
+
457
+ .btn-batch {
458
+ background: rgba(0, 0, 0, 0.1);
459
+ border: 1px solid rgba(0, 0, 0, 0.1);
460
+ padding: 6px 16px;
461
+ border-radius: 20px;
462
+ font-weight: 600;
463
+ font-size: 12px;
464
+ cursor: pointer;
465
+ transition: all 0.2s;
466
+ }
467
+
468
+ .btn-batch:hover {
469
+ background: rgba(0, 0, 0, 0.2);
470
+ }
471
+
472
+ /* ==================== PAGINATION ==================== */
473
+ .pagination {
474
+ display: flex;
475
+ justify-content: center;
476
+ align-items: center;
477
+ gap: 15px;
478
+ margin-top: 20px;
479
+ padding-bottom: 60px;
480
+ }
481
+
482
+ .btn-page {
483
+ background: rgba(255, 255, 255, 0.03);
484
+ border: 1px solid rgba(255, 255, 255, 0.1);
485
+ color: #fff;
486
+ width: 36px;
487
+ height: 36px;
488
+ border-radius: 10px;
489
+ display: flex;
490
+ align-items: center;
491
+ justify-content: center;
492
+ cursor: pointer;
493
+ transition: all 0.3s;
494
+ }
495
+
496
+ .btn-page:disabled {
497
+ opacity: 0.3;
498
+ cursor: not-allowed;
499
+ }
500
+
501
+ .btn-page.active {
502
+ background: var(--accent-yellow);
503
+ color: #000;
504
+ border-color: var(--accent-yellow);
505
+ }
506
+
507
+ .page-info {
508
+ color: #888;
509
+ font-size: 14px;
510
+ }
511
+
512
+ /* ==================== PREVIEW MODAL ==================== */
513
+ .modal-overlay {
514
+ position: fixed;
515
+ top: 0;
516
+ left: 0;
517
+ width: 100%;
518
+ height: 100%;
519
+ background: rgba(0, 0, 0, 0.8);
520
+ backdrop-filter: blur(8px);
521
+ z-index: 2000;
522
+ display: none;
523
+ align-items: center;
524
+ justify-content: center;
525
+ padding: 20px;
526
+ }
527
+
528
+ .modal-container {
529
+ background: #111;
530
+ border: 1px solid rgba(255, 255, 255, 0.1);
531
+ border-radius: 24px;
532
+ width: 100%;
533
+ max-width: 900px;
534
+ max-height: 90vh;
535
+ overflow-y: auto;
536
+ position: relative;
537
+ animation: modalSlideUp 0.4s ease;
538
+ }
539
+
540
+ @keyframes modalSlideUp {
541
+ from {
542
+ opacity: 0;
543
+ transform: translateY(30px);
544
+ }
545
+
546
+ to {
547
+ opacity: 1;
548
+ transform: translateY(0);
549
+ }
550
+ }
551
+
552
+ .modal-close {
553
+ position: absolute;
554
+ top: 20px;
555
+ right: 20px;
556
+ background: rgba(255, 255, 255, 0.05);
557
+ border: none;
558
+ color: #fff;
559
+ width: 36px;
560
+ height: 36px;
561
+ border-radius: 50%;
562
+ cursor: pointer;
563
+ font-size: 20px;
564
+ z-index: 10;
565
+ }
566
+
567
+ .modal-body {
568
+ display: grid;
569
+ grid-template-columns: 1fr 1fr;
570
+ gap: 30px;
571
+ padding: 40px;
572
+ }
573
+
574
+ .modal-media {
575
+ width: 100%;
576
+ border-radius: 16px;
577
+ border: 1px solid rgba(255, 255, 255, 0.1);
578
+ }
579
+
580
+ .modal-details {
581
+ display: flex;
582
+ flex-direction: column;
583
+ gap: 20px;
584
+ }
585
+
586
+ .modal-title {
587
+ font-size: 24px;
588
+ font-weight: 700;
589
+ color: #fff;
590
+ }
591
+
592
+ .notes-section {
593
+ margin-top: 10px;
594
+ }
595
+
596
+ .notes-area {
597
+ width: 100%;
598
+ background: rgba(255, 255, 255, 0.03);
599
+ border: 1px solid rgba(255, 255, 255, 0.1);
600
+ border-radius: 12px;
601
+ padding: 12px;
602
+ color: #fff;
603
+ font-size: 14px;
604
+ min-height: 100px;
605
+ resize: vertical;
606
+ }
607
+
608
+ .notes-area:focus {
609
+ outline: none;
610
+ border-color: var(--accent-yellow);
611
+ }
612
+
613
+ .btn-save-notes {
614
+ margin-top: 10px;
615
+ background: var(--accent-yellow);
616
+ color: #000;
617
+ border: none;
618
+ padding: 8px 16px;
619
+ border-radius: 8px;
620
+ font-weight: 600;
621
+ cursor: pointer;
622
+ }
623
+
624
+ @media (max-width: 768px) {
625
+ .modal-body {
626
+ grid-template-columns: 1fr;
627
+ padding: 20px;
628
+ }
629
+ }
frontend/history.html ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>Scan History - DeepGuard</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
12
+ rel="stylesheet">
13
+ <link rel="stylesheet" href="variables.css">
14
+ <link rel="stylesheet" href="style.css">
15
+ <link rel="stylesheet" href="history.css">
16
+ <link rel="stylesheet" href="animations.css">
17
+ <link rel="stylesheet" href="pwa.css">
18
+ <link rel="stylesheet" href="responsive-additions.css">
19
+ <link rel="stylesheet" href="responsive-pages.css">
20
+
21
+ <!-- PWA Manifest -->
22
+ <link rel="manifest" href="manifest.json">
23
+ <meta name="theme-color" content="#E3F514">
24
+ <meta name="apple-mobile-web-app-capable" content="yes">
25
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
26
+ <meta name="apple-mobile-web-app-title" content="DeepGuard">
27
+ <link rel="apple-touch-icon" href="logo.ico">
28
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
29
+ </head>
30
+
31
+ <body class="analysis-page">
32
+ <div class="mesh-background"></div>
33
+ <div id="particles-js"></div>
34
+
35
+ <!-- Navigation -->
36
+ <nav class="navbar">
37
+ <div class="container">
38
+ <div class="nav-content">
39
+ <a href="index.html" class="logo">
40
+ <img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
41
+ <span class="logo-text">Deep<span class="gradient-text">Guard</span></span>
42
+ </a>
43
+
44
+ <!-- Hamburger Menu Button (Mobile) -->
45
+ <button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
46
+ <span></span>
47
+ <span></span>
48
+ <span></span>
49
+ </button>
50
+
51
+ <!-- Navigation Menu -->
52
+ <div class="nav-menu-wrapper">
53
+ <ul class="nav-menu">
54
+ <li><a href="index.html">Home</a></li>
55
+ <li><a href="analysis.html">Analysis</a></li>
56
+ <li><a href="history.html" class="active">History</a></li>
57
+ </ul>
58
+ <a href="index.html" class="btn-secondary-nav">← Back to Home</a>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </nav>
63
+
64
+ <main class="analysis-container">
65
+ <div class="container" style="max-width: 1200px; padding-top: 40px;">
66
+ <div class="section-header">
67
+ <h2 class="section-title">Scan <span class="gradient-text">History</span></h2>
68
+ <p class="section-subtitle">View and manage your past detection results</p>
69
+ </div>
70
+
71
+ <!-- Controls Bar -->
72
+ <div class="history-controls">
73
+ <div class="search-container">
74
+ <input type="text" id="searchInput" class="search-input" placeholder="🔍 Search by filename..."
75
+ oninput="handleSearch()">
76
+ </div>
77
+
78
+ <!-- View Toggle -->
79
+ <div class="view-toggle">
80
+ <button class="btn-toggle active" id="listViewBtn" onclick="toggleView('list')" title="List View">
81
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
82
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
83
+ <line x1="8" y1="6" x2="21" y2="6"></line>
84
+ <line x1="8" y1="12" x2="21" y2="12"></line>
85
+ <line x1="8" y1="18" x2="21" y2="18"></line>
86
+ <line x1="3" y1="6" x2="3.01" y2="6"></line>
87
+ <line x1="3" y1="12" x2="3.01" y2="12"></line>
88
+ <line x1="3" y1="18" x2="3.01" y2="18"></line>
89
+ </svg>
90
+ </button>
91
+ <button class="btn-toggle" id="gridViewBtn" onclick="toggleView('grid')" title="Grid View">
92
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
93
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
94
+ <rect x="3" y="3" width="7" height="7"></rect>
95
+ <rect x="14" y="3" width="7" height="7"></rect>
96
+ <rect x="14" y="14" width="7" height="7"></rect>
97
+ <rect x="3" y="14" width="7" height="7"></rect>
98
+ </svg>
99
+ </button>
100
+ </div>
101
+
102
+ <div class="filter-controls">
103
+ <select id="filterPrediction" class="filter-select" onchange="applyFilters()">
104
+ <option value="all">All Results</option>
105
+ <option value="FAKE">Fake Only</option>
106
+ <option value="REAL">Real Only</option>
107
+ </select>
108
+
109
+ <select id="filterConfidence" class="filter-select" onchange="applyFilters()">
110
+ <option value="all">All Confidence</option>
111
+ <option value="high">High (>80%)</option>
112
+ <option value="medium">Medium (50-80%)</option>
113
+ <option value="low">Low (<50%)< /option>
114
+ </select>
115
+
116
+ <select id="sortBy" class="filter-select" onchange="applyFilters()">
117
+ <option value="date-desc">Latest First</option>
118
+ <option value="date-asc">Oldest First</option>
119
+ <option value="confidence-desc">Confidence ↓</option>
120
+ <option value="confidence-asc">Confidence ↑</option>
121
+ <option value="filename-asc">Filename A-Z</option>
122
+ </select>
123
+ </div>
124
+
125
+ <div class="export-controls">
126
+ <button class="btn-export" onclick="exportHistory('csv')">
127
+ 📄 Export CSV
128
+ </button>
129
+ <button class="btn-export" onclick="exportHistory('json')">
130
+ 📋 Export JSON
131
+ </button>
132
+ <button class="btn-clear-all" onclick="confirmClearAll()">
133
+ 🗑 Clear All
134
+ </button>
135
+ </div>
136
+
137
+ <!-- Quick Filter Chips -->
138
+ <div class="filter-chips">
139
+ <div class="chip active" onclick="setQuickFilter('all', this)">All SCANS</div>
140
+ <div class="chip" onclick="setQuickFilter('FAKE', this)">FAKES Detected</div>
141
+ <div class="chip" onclick="setQuickFilter('REAL', this)">REAL Media</div>
142
+ <div class="chip" onclick="setQuickFilter('high', this)">High Confidence</div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Results Count -->
147
+ <div class="results-count" id="resultsCount">
148
+ Showing <span id="showingCount">0</span> of <span id="totalCount">0</span> results
149
+ </div>
150
+
151
+ <!-- History Table -->
152
+ <div class="history-table-container" id="historyTableContainer">
153
+ <table class="history-table" id="historyTable">
154
+ <thead>
155
+ <tr>
156
+ <th style="width: 40px; cursor: default;"><input type="checkbox" id="selectAllCheckbox"
157
+ onclick="toggleSelectAll(this)"></th>
158
+ <th>Preview</th>
159
+ <th onclick="sortTable('filename')">Filename <span class="sort-indicator"></span></th>
160
+ <th onclick="sortTable('prediction')">Result <span class="sort-indicator"></span></th>
161
+ <th onclick="sortTable('confidence')">Confidence <span class="sort-indicator"></span></th>
162
+ <th onclick="sortTable('timestamp')">Date <span class="sort-indicator"></span></th>
163
+ <th>Actions</th>
164
+ </tr>
165
+ </thead>
166
+ <tbody id="historyTableBody">
167
+ <!-- Rows will be populated here -->
168
+ </tbody>
169
+ </table>
170
+
171
+ <!-- Empty State -->
172
+ <div class="empty-state" id="historyEmptyState" style="display: none;">
173
+ <div class="empty-icon">📂</div>
174
+ <h3>No History Found</h3>
175
+ <p>Your recent analysis results will appear here.</p>
176
+ <a href="analysis.html" class="btn-mini-corner">
177
+ <i class="fas fa-plus"></i> New Analysis
178
+ </a>
179
+ </div>
180
+
181
+ <!-- No Results State -->
182
+ <div class="empty-state" id="noResultsState" style="display: none;">
183
+ <div class="empty-icon">🔍</div>
184
+ <h3>No Results Found</h3>
185
+ <p>Try adjusting your search or filter criteria.</p>
186
+ </div>
187
+
188
+ <!-- Grid View Container -->
189
+ <div id="historyGridContainer" class="history-grid-container" style="display: none;"></div>
190
+
191
+ <!-- Pagination -->
192
+ <div class="pagination" id="paginationControls">
193
+ <button class="btn-page" id="prevPageBtn" onclick="changePage(-1)" disabled>←</button>
194
+ <span class="page-info">Page <span id="currentPage">1</span> of <span
195
+ id="totalPages">1</span></span>
196
+ <button class="btn-page" id="nextPageBtn" onclick="changePage(1)">→</button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Batch Actions Bar -->
202
+ <div id="batchActionsBar" class="batch-actions-bar">
203
+ <span class="selection-info"><span id="selectedCount">0</span> items selected</span>
204
+ <button class="btn-batch" onclick="batchExport('csv')">Export CSV</button>
205
+ <button class="btn-batch" onclick="batchDelete()" style="color: #ff3b30;">Delete Selected</button>
206
+ <button class="btn-batch" onclick="clearSelection()" style="background: transparent;">Cancel</button>
207
+ </div>
208
+
209
+ <!-- Preview Modal -->
210
+ <div id="previewModal" class="modal-overlay" onclick="closeModal(event)">
211
+ <div class="modal-container">
212
+ <button class="modal-close"
213
+ onclick="document.getElementById('previewModal').style.display = 'none'">&times;</button>
214
+ <div class="modal-body" id="modalBody">
215
+ <!-- Populated via JS -->
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </main>
220
+
221
+ <script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
222
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
223
+ <script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
224
+ <script src="motion.js"></script>
225
+ <script src="config.js"></script>
226
+ <script src="script.js"></script>
227
+ <script src="mobile.js"></script>
228
+ <script src="pwa.js"></script>
229
+ <script>
230
+ // Simple script to handle active state or mock history if we wanted to
231
+ document.addEventListener('DOMContentLoaded', () => {
232
+ // We can add mock history here later if requested
233
+ });
234
+ </script>
235
+ </body>
236
+
237
+ </html>
frontend/index.html ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>DeepGuard - AI-Powered Deepfake Detection System</title>
8
+ <meta name="description"
9
+ content="Advanced AI-powered deepfake detection system using cutting-edge machine learning to identify manipulated media with unprecedented accuracy.">
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
14
+ rel="stylesheet">
15
+ <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
16
+ <link rel="stylesheet" href="variables.css">
17
+ <link rel="stylesheet" href="style.css">
18
+ <link rel="stylesheet" href="showcase.css">
19
+ <link rel="stylesheet" href="loader.css">
20
+ <link rel="stylesheet" href="video_player.css">
21
+ <link rel="stylesheet" href="extension.css">
22
+ <link rel="stylesheet" href="scroll_indicator.css">
23
+ <link rel="stylesheet" href="orbit.css">
24
+ <link rel="stylesheet" href="pwa.css">
25
+ <link rel="stylesheet" href="responsive-additions.css">
26
+
27
+ <!-- PWA Manifest -->
28
+ <link rel="manifest" href="manifest.json">
29
+ <meta name="theme-color" content="#E3F514">
30
+ <meta name="apple-mobile-web-app-capable" content="yes">
31
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
32
+ <meta name="apple-mobile-web-app-title" content="DeepGuard">
33
+ <link rel="apple-touch-icon" href="logo.ico">
34
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
35
+ </head>
36
+
37
+ <body>
38
+ <!-- Enhanced Loading Screen -->
39
+ <div id="loader-wrapper">
40
+ <!-- Brutalist Counter -->
41
+ <div class="loader-content brutalist">
42
+ <p id="loader-quote">IN A WORLD WHERE SEEING IS NO LONGER BELIEVING,<br>OUR SYSTEM EXISTS TO <span
43
+ class="quote-highlight">PROTECT THE TRUTH</span></p>
44
+
45
+ <!-- Detection Status (Cycling: ANALYZING → REAL? → FAKE?) -->
46
+ <div class="detection-status" id="detectionStatus">
47
+ <span class="status-text">ANALYZING...</span>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="counter-wrapper" style="color: #E3F514; opacity: 1; display: block;">
52
+ <span id="loader-percent">0</span><span class="percent-symbol">%</span>
53
+ </div>
54
+
55
+ <!-- Meta Information -->
56
+ <div class="loader-meta">
57
+ <span>DeepGuard Mark V</span>
58
+ <span id="loaderTimestamp">INITIALIZING SYSTEM</span>
59
+ </div>
60
+ </div>
61
+ <!-- Animated Background -->
62
+ <div id="canvas-container"></div>
63
+ <div class="mesh-background"></div>
64
+ <!-- Scroll Progress Bar -->
65
+ <div class="scroll-progress-container">
66
+ <div class="scroll-progress-bar" id="scrollProgress"></div>
67
+ </div>
68
+ <div id="particles-js"></div>
69
+
70
+ <!-- Noise Overlay -->
71
+ <div class="noise-overlay"></div>
72
+
73
+ <!-- Navigation -->
74
+ <nav class="navbar">
75
+ <div class="container">
76
+ <div class="nav-content">
77
+ <a href="index.html" class="logo">
78
+ <img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
79
+ <span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
80
+ </a>
81
+
82
+ <!-- Hamburger Menu Button (Mobile) -->
83
+ <button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
84
+ <span></span>
85
+ <span></span>
86
+ <span></span>
87
+ </button>
88
+
89
+ <!-- Navigation Menu -->
90
+ <div class="nav-menu-wrapper">
91
+ <ul class="nav-menu">
92
+ <li><a href="index.html" class="active">Home</a></li>
93
+ <li><a href="analysis.html">Analysis</a></li>
94
+ <li><a href="history.html">History</a></li>
95
+ </ul>
96
+ <a href="analysis.html" class="btn-primary">Get Started</a>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </nav>
101
+
102
+ <!-- Hero Section -->
103
+ <section class="hero">
104
+ <div class="hero-background">
105
+ <!-- Lottie Player Script -->
106
+ <script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
107
+
108
+ <div class="gradient-orb orb-1"></div>
109
+ <div class="gradient-orb orb-2"></div>
110
+ <div class="gradient-orb orb-3"></div>
111
+
112
+ <!-- Floating 3D Objects -->
113
+ <div class="floating-3d-object floating-cube" id="floatingCube">
114
+ <div class="cube-face cube-front"></div>
115
+ <div class="cube-face cube-back"></div>
116
+ <div class="cube-face cube-right"></div>
117
+ <div class="cube-face cube-left"></div>
118
+ <div class="cube-face cube-top"></div>
119
+ <div class="cube-face cube-bottom"></div>
120
+ </div>
121
+
122
+ <div class="floating-3d-object floating-pyramid" id="floatingPyramid">
123
+ <div class="pyramid-face pyramid-front"></div>
124
+ <div class="pyramid-face pyramid-back"></div>
125
+ <div class="pyramid-face pyramid-left"></div>
126
+ <div class="pyramid-face pyramid-right"></div>
127
+ <div class="pyramid-face pyramid-base"></div>
128
+ </div>
129
+
130
+ <!-- Fluid Hover Reveal Images -->
131
+ <div class="hero-reveal-container" id="heroRevealContainer">
132
+ <!-- Background image removed as per user request -->
133
+
134
+ <img src="assets/gemini_reveal.png" alt="AI Detection Foreground" class="reveal-image reveal-top"
135
+ id="hero-img-reveal" style="display: none;"> <!-- Hidden, drawn by canvas -->
136
+ <canvas id="revealCanvas" class="reveal-canvas"></canvas>
137
+ </div>
138
+ </div>
139
+ <div class="container">
140
+ <div class="hero-content">
141
+ <div class="hero-badge">
142
+ <span class="badge-dot"></span>
143
+ <span>AI-Powered Detection</span>
144
+ </div>
145
+ <h1 class="hero-title" data-aos="fade-up" data-aos-delay="100">
146
+ Protect Reality with
147
+ <br />
148
+ <span class="gradient-text-hero">Advanced AI Detection</span>
149
+ </h1>
150
+ <p class="hero-description" data-aos="fade-up" data-aos-delay="200">
151
+ Welcome to the future of media authentication. Our cutting-edge deepfake detection system leverages
152
+ state-of-the-art AI to identify manipulated content with unprecedented accuracy.
153
+ </p>
154
+ <div class="hero-actions" data-aos="fade-up" data-aos-delay="300">
155
+ <a href="analysis.html" class="btn-hero-primary">Try Detection Now</a>
156
+ <a href="#live-demo" class="btn-hero-secondary">
157
+ <span class="play-icon">▶</span>
158
+ Watch Demo
159
+ </a>
160
+ <a href="#extension" class="btn-hero-white">
161
+ <img src="https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg"
162
+ alt="Chrome" width="20" height="20">
163
+ Get Extension
164
+ </a>
165
+ </div>
166
+ <div class="hero-stats" data-aos="zoom-in" data-aos-delay="400" data-speed="0.1">
167
+ <div class="stat-item">
168
+ <div class="stat-value">97%</div>
169
+ <div class="stat-label">Accuracy Rate</div>
170
+ </div>
171
+ <div class="stat-divider"></div>
172
+ <div class="stat-item">
173
+ <div class="stat-value">1.3M</div>
174
+ <div class="stat-label">Dataset Trained On</div>
175
+ </div>
176
+ <div class="stat-divider"></div>
177
+ <div class="stat-item">
178
+ <div class="stat-value">&lt; 2s</div>
179
+ <div class="stat-label">Detection Time</div>
180
+ </div>
181
+ </div>
182
+
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Scroll Indicator (Moved out of hero-content to adhere to bottom of viewport) -->
188
+ <div class="scroll-indicator" data-speed="-0.2">
189
+ <div class="mouse">
190
+ <div class="wheel"></div>
191
+ </div>
192
+ <div class="arrow-scroll"></div>
193
+ </div>
194
+ </section>
195
+
196
+ <!-- Section Divider (Wave) -->
197
+ <div class="section-divider">
198
+ <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" preserveAspectRatio="none">
199
+ <path
200
+ d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z"
201
+ class="shape-fill"></path>
202
+ </svg>
203
+ </div>
204
+
205
+ <!-- Live Demo Section -->
206
+ <section id="live-demo" class="video-demo-section" style="padding: 60px 0; position: relative; z-index: 5;">
207
+ <div class="container">
208
+ <div class="section-header">
209
+ <h2 class="section-title">See It In <span class="gradient-text">Action</span></h2>
210
+ <p class="section-subtitle">Watch DeepGuard analyze deepfakes in real-time</p>
211
+ </div>
212
+
213
+ <div class="video-window-container" data-aos="zoom-in-up">
214
+ <!-- Mac Window Header -->
215
+ <div class="window-header">
216
+ <div class="window-controls">
217
+ <span class="control red"></span>
218
+ <span class="control yellow"></span>
219
+ <span class="control green"></span>
220
+ </div>
221
+ <div class="window-title">DeepGuard Live Analysis - Mark V</div>
222
+ </div>
223
+
224
+ <!-- Video Player -->
225
+ <div class="video-content-wrapper">
226
+ <video id="demoVideoPlayer" class="demo-video" muted playsinline preload="metadata" width="100%">
227
+ <source src="assets/demo_part1.mov" type="video/mp4">
228
+ <source src="assets/demo_part1.mov" type="video/quicktime">
229
+ Your browser does not support the video tag.
230
+ </video>
231
+
232
+ <!-- Play Overlay -->
233
+ <div class="play-overlay" id="playOverlay">
234
+ <div class="play-button">▶</div>
235
+ </div>
236
+
237
+ <!-- Progress Bar (Fake OS look) -->
238
+ <div class="video-progress-bar">
239
+ <div class="progress-fill" id="videoProgress"></div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </section>
245
+
246
+ <!-- Extension Showcase Section -->
247
+ <section id="extension" class="extension-section">
248
+ <div class="container">
249
+ <div class="extension-container">
250
+ <div class="extension-content" data-aos="fade-right">
251
+ <div class="extension-badge">
252
+ <span>●</span> Available for Chrome
253
+ </div>
254
+ <h2 class="extension-title">DeepGuard <span class="gradient-text">Everywhere</span></h2>
255
+ <p class="extension-description">
256
+ Detect deepfakes in real-time while you browse social media. Our browser extension automatically
257
+ scans images on X, Instagram, and Reddit.
258
+ </p>
259
+ <a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model/tree/main/extension"
260
+ class="chrome-btn" target="_blank">
261
+ <img src="https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg"
262
+ alt="Chrome" class="chrome-icon">
263
+ Add to Chrome
264
+ </a>
265
+ </div>
266
+
267
+ <div class="extension-visual" data-aos="fade-left">
268
+ <div class="video-window-container" style="max-width: 100%;">
269
+ <div class="window-header">
270
+ <div class="window-controls">
271
+ <span class="control red"></span>
272
+ <span class="control yellow"></span>
273
+ <span class="control green"></span>
274
+ </div>
275
+ <div class="window-title">DeepGuard Extension Preview</div>
276
+ </div>
277
+ <div class="video-content-wrapper">
278
+ <video class="demo-video" autoplay muted loop playsinline preload="metadata" width="100%">
279
+ <source src="assets/extension_demo.mov" type="video/mp4">
280
+ <source src="assets/extension_demo.mov" type="video/quicktime">
281
+ </video>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ </section>
288
+
289
+
290
+
291
+ <!-- Technology Stack Section -->
292
+ <section id="technology" class="tech-section">
293
+ <div class="container">
294
+ <div class="section-header">
295
+ <h2 class="section-title">Built with <span class="gradient-text">Advanced Technology</span></h2>
296
+ <p class="section-subtitle">Enterprise-grade AI infrastructure powering reliable detection</p>
297
+ </div>
298
+ <div class="tech-grid">
299
+ <div class="tech-card" data-aos="zoom-in" data-aos-delay="100">
300
+ <div class="tech-icon">🧠</div>
301
+ <h3>EfficientNet V2</h3>
302
+ <p>Spatial Feature Extraction</p>
303
+ </div>
304
+ <div class="tech-card" data-aos="zoom-in" data-aos-delay="200">
305
+ <div class="tech-icon">🌪️</div>
306
+ <h3>Swin Transformer</h3>
307
+ <p>Global Context Attention</p>
308
+ </div>
309
+ <div class="tech-card" data-aos="zoom-in" data-aos-delay="300">
310
+ <div class="tech-icon">🌊</div>
311
+ <h3>FFT Analysis</h3>
312
+ <p>Frequency Domain Inspection</p>
313
+ </div>
314
+ <div class="tech-card" data-aos="zoom-in" data-aos-delay="400">
315
+ <div class="tech-icon">🐍</div>
316
+ <h3>Python & PyTorch</h3>
317
+ <p>Core AI Framework</p>
318
+ </div>
319
+ <div class="tech-card" data-aos="zoom-in" data-aos-delay="500">
320
+ <div class="tech-icon">🎯</div>
321
+ <h3>Patch Encoder</h3>
322
+ <p>Local Artifact Detection</p>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </section>
327
+
328
+
329
+
330
+
331
+
332
+ <!-- How It Works -->
333
+ <section class="how-it-works">
334
+ <div class="container">
335
+ <div class="section-header">
336
+ <h2 class="section-title">How It <span class="gradient-text">Works</span></h2>
337
+ <p class="section-subtitle">Advanced AI pipeline for accurate deepfake detection</p>
338
+ </div>
339
+ <div class="pipeline">
340
+ <div class="pipeline-step">
341
+ <div class="step-number">01</div>
342
+ <div class="step-icon">📤</div>
343
+ <h3 class="step-title">Upload Media</h3>
344
+ <p class="step-description">Upload your image or video file through our secure platform</p>
345
+ </div>
346
+ <div class="pipeline-arrow">→</div>
347
+ <div class="pipeline-step">
348
+ <div class="step-number">02</div>
349
+ <div class="step-icon">🔍</div>
350
+ <h3 class="step-title">AI Analysis</h3>
351
+ <p class="step-description">Deep learning model analyzes pixel patterns and artifacts</p>
352
+ </div>
353
+ <div class="pipeline-arrow">→</div>
354
+ <div class="pipeline-step">
355
+ <div class="step-number">03</div>
356
+ <div class="step-icon">🧮</div>
357
+ <h3 class="step-title">Multi-Modal Fusion</h3>
358
+ <p class="step-description">Analyzes frequency (FFT), local patches, and global context
359
+ simultaneously</p>
360
+ </div>
361
+ <div class="pipeline-arrow">→</div>
362
+ <div class="pipeline-step">
363
+ <div class="step-number">04</div>
364
+ <div class="step-icon">✨</div>
365
+ <h3 class="step-title">Results</h3>
366
+ <p class="step-description">Receive detailed report with confidence score and analysis</p>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </section>
371
+
372
+ <!-- Live Demo Section -->
373
+ <!-- Demo Section Removed - Moved to top -->
374
+
375
+ <!-- CTA Section -->
376
+ <section class="cta-section">
377
+ <div class="container">
378
+ <div class="cta-content">
379
+ <h2 class="cta-title">Ready to Protect Against Deepfakes?</h2>
380
+ <p class="cta-description">Join thousands of users trusting our AI-powered detection system</p>
381
+ <div class="cta-actions">
382
+ <a href="analysis.html" class="btn-cta-primary">Get Started Free</a>
383
+
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </section>
388
+
389
+ <!-- Footer -->
390
+ <footer class="footer">
391
+ <div class="container">
392
+ <div class="footer-content-minimal">
393
+ <!-- Brand -->
394
+ <div class="footer-brand">
395
+ <div class="logo">
396
+ <img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
397
+ <span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
398
+ </div>
399
+ </div>
400
+
401
+ <!-- Tagline -->
402
+ <p class="footer-tagline-premium">Protecting digital truth with AI</p>
403
+
404
+ <!-- Links -->
405
+ <div class="footer-links-premium">
406
+ <a href="https://harshasnade-deepfake-detection.hf.space" target="_blank"
407
+ class="footer-link-premium-item">
408
+ <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face">
409
+ <span>Models</span>
410
+ </a>
411
+ <a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model" target="_blank"
412
+ class="footer-link-premium-item">
413
+ <svg viewBox="0 0 24 24" fill="currentColor">
414
+ <path
415
+ d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
416
+ </svg>
417
+ <span>GitHub</span>
418
+ </a>
419
+ </div>
420
+
421
+ <!-- Copyright -->
422
+ <div class="footer-copyright-premium">
423
+ <p>&copy; 2024 <span style="color: var(--accent-yellow); font-weight: 700;">DeepGuard</span></p>
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ <!-- Enhanced decorative glows -->
429
+ <div class="footer-glow-enhanced"></div>
430
+ <div class="footer-glow"></div>
431
+ </footer>
432
+
433
+ <script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
434
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
435
+ <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
436
+ <script src="three_bg.js"></script>
437
+ <script src="loader.js"></script>
438
+ <script src="script.js"></script>
439
+ <script src="hero_reveal.js"></script>
440
+ <script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
441
+ <script src="motion.js"></script>
442
+ <script src="orbit_interaction.js"></script>
443
+ <script src="mobile.js"></script>
444
+ <script src="pwa.js"></script>
445
+ </body>
446
+
447
+ </html>
frontend/loader.css ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== BRUTALIST LOADER ==================== */
2
+ #loader-wrapper {
3
+ position: fixed;
4
+ top: 0;
5
+ left: 0;
6
+ width: 100vw;
7
+ height: 100vh;
8
+ background-color: #000000 !important;
9
+ z-index: 2147483647 !important;
10
+ /* Max Z-Index */
11
+ display: flex;
12
+ flex-direction: column;
13
+ justify-content: center;
14
+ align-items: center;
15
+ overflow: hidden;
16
+ color: #ffffff;
17
+ font-family: sans-serif;
18
+ /* Fallback first */
19
+ transition: transform 0.5s ease-in-out;
20
+ }
21
+
22
+ #loader-wrapper.loaded {
23
+ transform: translateY(-100%);
24
+ pointer-events: none;
25
+ }
26
+
27
+ /* Brutalist Content */
28
+ .loader-content {
29
+ text-align: center;
30
+ position: relative;
31
+ z-index: 2147483647;
32
+ width: 100%;
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: center;
37
+ opacity: 1 !important;
38
+ visibility: visible !important;
39
+ }
40
+
41
+ .counter-wrapper {
42
+ font-size: 10vw;
43
+ font-weight: 900;
44
+ font-family: sans-serif;
45
+ line-height: 1;
46
+ color: #E3F514 !important;
47
+ /* Force Yellow */
48
+ position: absolute;
49
+ bottom: 40px;
50
+ right: 40px;
51
+ margin: 0;
52
+ display: block;
53
+ opacity: 1 !important;
54
+ text-align: right;
55
+ }
56
+
57
+ #loader-quote {
58
+ color: #ffffff;
59
+ font-family: 'Space Grotesk', sans-serif;
60
+ font-size: 2.5rem;
61
+ font-weight: 700;
62
+ text-align: center;
63
+ max-width: 90%;
64
+ margin-bottom: 2rem;
65
+ opacity: 1;
66
+ line-height: 1.3;
67
+ text-transform: uppercase;
68
+ letter-spacing: 0.1em;
69
+ z-index: 2147483648;
70
+
71
+ padding: 0;
72
+ border: none;
73
+ box-shadow: none;
74
+ backdrop-filter: none;
75
+ }
76
+
77
+ /* Character States for Monkeytype effect (DeepGuard Themed) */
78
+ .char-waiting {
79
+ color: rgba(255, 255, 255, 0.2);
80
+ transition: color 0.1s ease;
81
+ }
82
+
83
+ .char-typed {
84
+ color: #ffffff;
85
+ text-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
86
+ }
87
+
88
+ .char-current {
89
+ background-color: #E3F514;
90
+ color: #000000;
91
+ border-radius: 0px;
92
+ /* Brutalist sharp edges */
93
+ }
94
+
95
+ .quote-highlight {
96
+ /* Reset glass effect to allow inner spans to control color */
97
+ background: none;
98
+ -webkit-text-fill-color: initial;
99
+ text-fill-color: initial;
100
+ font-weight: inherit;
101
+ letter-spacing: inherit;
102
+ filter: none;
103
+ display: inline;
104
+ }
105
+
106
+ /* Remove underline for pure glass text look */
107
+ .quote-highlight::after {
108
+ display: none;
109
+ }
110
+
111
+ #loader-percent {
112
+ color: #E3F514 !important;
113
+ }
114
+
115
+ .percent-symbol {
116
+ font-size: 4vw;
117
+ vertical-align: super;
118
+ margin-left: 10px;
119
+ opacity: 1 !important;
120
+ color: #E3F514 !important;
121
+ }
122
+
123
+ /* Status Text */
124
+ .detection-status {
125
+ font-family: monospace, sans-serif;
126
+ font-size: 1.5rem;
127
+ letter-spacing: 0.1em;
128
+ text-transform: uppercase;
129
+ font-weight: bold;
130
+ min-height: 2em;
131
+ display: flex;
132
+ justify-content: center;
133
+ align-items: center;
134
+ color: #ffffff !important;
135
+ margin-bottom: 20px;
136
+ }
137
+
138
+ .status-text {
139
+ color: #ffffff !important;
140
+ }
141
+
142
+ /* Meta Information */
143
+ .loader-meta {
144
+ position: absolute;
145
+ bottom: 40px;
146
+ width: 100%;
147
+ text-align: center;
148
+ color: #666;
149
+ font-size: 14px;
150
+ z-index: 2147483647;
151
+ }
152
+
153
+ /* Mobile Responsive */
154
+ @media (max-width: 768px) {
155
+ .counter-wrapper {
156
+ font-size: 15vw;
157
+ bottom: 20px;
158
+ right: 20px;
159
+ }
160
+
161
+ #loader-quote {
162
+ font-size: 1.4rem;
163
+ width: 90%;
164
+ padding: 1.5rem;
165
+ letter-spacing: 0.05em;
166
+ }
167
+ }
frontend/loader.js ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==================== ENHANCED LOADER SYSTEM ====================
2
+ // Countdown Timer and Real/Fake Status Cycling (Minimum 5 seconds)
3
+
4
+ const initLoader = () => {
5
+ console.log("Initializing Loader...");
6
+ const loaderWrapper = document.getElementById('loader-wrapper');
7
+ const loaderPercent = document.getElementById('loader-percent');
8
+ const detectionStatus = document.getElementById('detectionStatus');
9
+ const statusText = detectionStatus?.querySelector('.status-text');
10
+ const loaderTimestamp = document.getElementById('loaderTimestamp');
11
+ const loaderQuote = document.getElementById('loader-quote');
12
+
13
+ // Global flag for backend readiness (default false)
14
+ window.isBackendReady = false;
15
+
16
+ // Expose status update function
17
+ window.updateLoaderStatus = (message) => {
18
+ if (statusText) {
19
+ statusText.textContent = message;
20
+ // Clear animations/colors to show this is a sticky state
21
+ if (detectionStatus) {
22
+ detectionStatus.classList.remove('analyzing', 'real', 'fake');
23
+ detectionStatus.classList.add('analyzing');
24
+ }
25
+ }
26
+ };
27
+
28
+ if (!loaderWrapper) {
29
+ console.error("Loader wrapper not found!");
30
+ return;
31
+ }
32
+
33
+ // Force visibility at start
34
+ loaderWrapper.style.display = 'flex';
35
+ loaderWrapper.style.opacity = '1';
36
+
37
+ // Prevent double initialization
38
+ if (loaderWrapper.dataset.initialized) return;
39
+ loaderWrapper.dataset.initialized = "true";
40
+
41
+ let progress = 0;
42
+ const targetProgress = 100;
43
+ let currentStatus = 0;
44
+ const startTime = Date.now();
45
+ const minimumDuration = 3500; // 3.5 seconds (Optimized for better UX)
46
+
47
+ // Safety Timeout - Force remove loader after 2 minutes (120000ms) to allow for cold starts
48
+ // If backend is completely dead, this ensures user isn't stuck forever.
49
+ setTimeout(() => {
50
+ if (loaderWrapper && loaderWrapper.style.display !== 'none' && !loaderWrapper.classList.contains('loaded')) {
51
+ console.warn("Loader safety timeout triggered - forcing removal.");
52
+ loaderWrapper.classList.add('loaded');
53
+ setTimeout(() => {
54
+ loaderWrapper.style.display = 'none';
55
+ }, 800);
56
+ }
57
+ }, 120000);
58
+
59
+ // Real/Fake Status Messages
60
+ const statusMessages = [
61
+ { text: 'ANALYZING...', class: 'analyzing' },
62
+ { text: 'SCANNING PATTERNS...', class: 'analyzing' },
63
+ { text: 'REAL?', class: 'real' },
64
+ { text: 'CHECKING AUTHENTICITY...', class: 'analyzing' },
65
+ { text: 'FAKE?', class: 'fake' },
66
+ { text: 'VERIFYING DATA...', class: 'analyzing' }
67
+ ];
68
+
69
+ // Update timestamp
70
+ const updateTimestamp = () => {
71
+ const now = new Date();
72
+ const timeStr = now.toLocaleTimeString('en-US', { hour12: false });
73
+ if (loaderTimestamp) {
74
+ loaderTimestamp.textContent = `SYSTEM TIME: ${timeStr}`;
75
+ }
76
+ };
77
+ updateTimestamp();
78
+ const timeInterval = setInterval(updateTimestamp, 1000);
79
+
80
+ // Cycle through status messages
81
+ let statusInterval;
82
+ const cycleStatus = () => {
83
+ // If external system says we are waiting (via explicit message override), stop cycling
84
+ // We'll use a property on the statusText or just check text content if it matches our "Starting..." message?
85
+ // Simpler: Just rely on the animation loop. If we are paused at 99%, we stop cycling.
86
+
87
+ if (!statusText || !detectionStatus) return;
88
+
89
+ // If we are waiting for backend (progress capped at 99), stop cycling status text
90
+ if (progress >= 99 && !window.isBackendReady) {
91
+ return;
92
+ }
93
+
94
+ const status = statusMessages[currentStatus];
95
+ statusText.textContent = status.text;
96
+
97
+ // Remove all status classes
98
+ detectionStatus.classList.remove('analyzing', 'real', 'fake');
99
+ // Add current class
100
+ detectionStatus.classList.add(status.class);
101
+
102
+ currentStatus = (currentStatus + 1) % statusMessages.length;
103
+ };
104
+
105
+ // Start cycling status every 800ms
106
+ cycleStatus();
107
+ statusInterval = setInterval(cycleStatus, 800);
108
+
109
+ // Countdown Timer Animation - Smooth progression from 0 to 100
110
+ let lastFrameTime = startTime;
111
+
112
+ // Pre-process loader quotes for typing effect
113
+ let allChars = [];
114
+ if (loaderQuote && !loaderQuote.dataset.processed) {
115
+ loaderQuote.dataset.processed = "true";
116
+
117
+ const processNode = (node) => {
118
+ if (node.nodeType === Node.TEXT_NODE) {
119
+ const text = node.textContent;
120
+ // Skip empty text nodes that are just whitespace to avoid weird spacing gaps if flex/grid were used,
121
+ // but for standard flow, whitespace is needed. However, large blocks of whitespace can be ignored.
122
+ if (text.trim().length === 0 && text.length > 0) {
123
+ // Keep the whitespace node as is
124
+ return;
125
+ }
126
+
127
+ const fragment = document.createDocumentFragment();
128
+ const map = text.split('');
129
+ map.forEach(char => {
130
+ const span = document.createElement('span');
131
+ span.textContent = char;
132
+ span.className = 'char-waiting'; // Start in waiting state
133
+ fragment.appendChild(span);
134
+ allChars.push(span);
135
+ });
136
+ node.replaceWith(fragment);
137
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
138
+ if (node.tagName !== 'BR') {
139
+ Array.from(node.childNodes).forEach(processNode);
140
+ }
141
+ }
142
+ };
143
+
144
+ Array.from(loaderQuote.childNodes).forEach(processNode);
145
+
146
+ // Reveal the quote container after processing spans
147
+ // Synchronous update to prevent frame flicker
148
+ loaderQuote.style.opacity = '1';
149
+ }
150
+
151
+ function animateLoader() {
152
+ const now = Date.now();
153
+ const elapsed = now - startTime;
154
+
155
+ // Calculate exact progress based on time (0 to 100 over 5 seconds)
156
+ let exactProgress = Math.min((elapsed / minimumDuration) * 100, 100);
157
+
158
+ // BLOCKING: If backend is not ready, cap progress at 99%
159
+ if (!window.isBackendReady && exactProgress >= 99) {
160
+ // TIMEOUT FALLBACK: If we've been waiting too long (> 8 seconds), just let them in.
161
+ if (elapsed > 8000) {
162
+ console.warn("Backend check timed out - proceeding anyway.");
163
+ window.isBackendReady = true;
164
+ } else {
165
+ exactProgress = 99;
166
+ // Update status text if it hasn't been updated yet to show waiting state
167
+ if (statusText && statusText.textContent !== "STARTING SERVER..." && statusText.textContent !== "WAITING FOR BACKEND...") {
168
+ // We rely on script.js calling updateLoaderStatus, but we can also set a default here if stuck
169
+ // But let's let script.js drive the specific message
170
+ }
171
+ }
172
+ }
173
+
174
+ // Update progress value directly (no interpolation to avoid jumps)
175
+ progress = exactProgress;
176
+
177
+ if (loaderPercent) {
178
+ loaderPercent.textContent = Math.floor(progress);
179
+ }
180
+
181
+ if (allChars.length > 0) {
182
+ const totalChars = allChars.length;
183
+ // Calculate how many characters should be lit up based on progress
184
+ // We want all chars lit by 100%
185
+ const charsToLight = Math.floor((progress / 100) * totalChars);
186
+
187
+ allChars.forEach((charSpan, index) => {
188
+ if (index < charsToLight) {
189
+ charSpan.className = 'char-typed';
190
+ } else if (index === charsToLight && index < totalChars) {
191
+ charSpan.className = 'char-current';
192
+ } else {
193
+ charSpan.className = 'char-waiting';
194
+ }
195
+ });
196
+ } else if (loaderQuote) {
197
+ // Fallback if processing failed
198
+ loaderQuote.style.opacity = progress / 100;
199
+ }
200
+
201
+ // Only finish when minimum time has elapsed AND backend is ready
202
+ if (elapsed >= minimumDuration && progress >= 100 && window.isBackendReady === true) {
203
+ // Ensure we show 100%
204
+ if (loaderPercent) {
205
+ loaderPercent.textContent = '100';
206
+ }
207
+
208
+ // Reached 100% - Exit loader
209
+ setTimeout(() => {
210
+ console.log("Loader finished.");
211
+ clearInterval(statusInterval);
212
+ clearInterval(timeInterval);
213
+
214
+ loaderWrapper.classList.add('loaded'); // CSS transform
215
+
216
+ // Remove from DOM after animation
217
+ setTimeout(() => {
218
+ loaderWrapper.style.display = 'none';
219
+ }, 800); // Wait for CSS transition
220
+ }, 500); // Brief pause at 100%
221
+ } else {
222
+ requestAnimationFrame(animateLoader);
223
+ }
224
+ }
225
+
226
+ // Start countdown
227
+ animateLoader();
228
+ };
229
+
230
+ // Robust initialization
231
+ if (document.readyState === 'loading') {
232
+ document.addEventListener('DOMContentLoaded', initLoader);
233
+ } else {
234
+ // DOM already ready, run immediately
235
+ initLoader();
236
+ }
frontend/logo.ico ADDED

Git LFS Details

  • SHA256: 8d65e65acd366679f3818bcba75ee7aa31d41639be9bd7dc1a715ddb45505246
  • Pointer size: 131 Bytes
  • Size of remote file: 780 kB
frontend/logo.svg ADDED
frontend/manifest.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "DeepGuard - AI Deepfake Detection",
3
+ "short_name": "DeepGuard",
4
+ "description": "Advanced AI-powered deepfake detection system using cutting-edge machine learning to identify manipulated media with unprecedented accuracy.",
5
+ "start_url": "/index.html",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#000000",
9
+ "theme_color": "#E3F514",
10
+ "orientation": "portrait-primary",
11
+ "icons": [
12
+ {
13
+ "src": "icon-192.png",
14
+ "sizes": "192x192",
15
+ "type": "image/png"
16
+ },
17
+ {
18
+ "src": "icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png"
21
+ },
22
+ {
23
+ "src": "logo.ico",
24
+ "sizes": "16x16 32x32 48x48 64x64",
25
+ "type": "image/x-icon"
26
+ }
27
+ ],
28
+ "screenshots": [
29
+ {
30
+ "src": "/assets/screenshot-desktop.png",
31
+ "sizes": "1280x720",
32
+ "type": "image/png",
33
+ "form_factor": "wide",
34
+ "label": "DeepGuard Desktop View"
35
+ },
36
+ {
37
+ "src": "/assets/screenshot-mobile.png",
38
+ "sizes": "750x1334",
39
+ "type": "image/png",
40
+ "form_factor": "narrow",
41
+ "label": "DeepGuard Mobile View"
42
+ }
43
+ ],
44
+ "shortcuts": [
45
+ {
46
+ "name": "Analyze Media",
47
+ "short_name": "Analyze",
48
+ "description": "Start analyzing media for deepfakes",
49
+ "url": "/analysis.html",
50
+ "icons": [
51
+ {
52
+ "src": "/icon-192.png",
53
+ "sizes": "192x192",
54
+ "type": "image/png"
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "name": "View History",
60
+ "short_name": "History",
61
+ "description": "View scan history",
62
+ "url": "/history.html",
63
+ "icons": [
64
+ {
65
+ "src": "logo.ico",
66
+ "sizes": "192x192",
67
+ "type": "image/png"
68
+ }
69
+ ]
70
+ }
71
+ ],
72
+ "categories": [
73
+ "productivity",
74
+ "utilities",
75
+ "security"
76
+ ],
77
+ "iarc_rating_id": "e84b072d-71b3-4d3e-86ae-31a8ce4e53b7",
78
+ "prefer_related_applications": false,
79
+ "related_applications": [],
80
+ "share_target": {
81
+ "action": "/analysis.html",
82
+ "method": "GET",
83
+ "enctype": "application/x-www-form-urlencoded",
84
+ "params": {
85
+ "title": "title",
86
+ "text": "text",
87
+ "url": "url"
88
+ }
89
+ }
90
+ }
frontend/mobile.js ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Mobile Navigation & Utilities
3
+ * Handles hamburger menu, touch events, and mobile-specific optimizations
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ // ==================== HAMBURGER MENU ====================
10
+ const hamburger = document.getElementById('hamburger');
11
+ const navMenuWrapper = document.querySelector('.nav-menu-wrapper');
12
+ const body = document.body;
13
+
14
+ if (hamburger && navMenuWrapper) {
15
+ // Toggle menu
16
+ hamburger.addEventListener('click', function () {
17
+ this.classList.toggle('active');
18
+ navMenuWrapper.classList.toggle('active');
19
+ body.classList.toggle('menu-open');
20
+ });
21
+
22
+ // Close menu when clicking on a nav link
23
+ const navLinks = navMenuWrapper.querySelectorAll('.nav-menu a, .btn-primary');
24
+ navLinks.forEach(link => {
25
+ link.addEventListener('click', function () {
26
+ hamburger.classList.remove('active');
27
+ navMenuWrapper.classList.remove('active');
28
+ body.classList.remove('menu-open');
29
+ });
30
+ });
31
+
32
+ // Close menu when clicking outside
33
+ document.addEventListener('click', function (event) {
34
+ const isClickInsideNav = navMenuWrapper.contains(event.target);
35
+ const isClickOnHamburger = hamburger.contains(event.target);
36
+
37
+ if (!isClickInsideNav && !isClickOnHamburger && navMenuWrapper.classList.contains('active')) {
38
+ hamburger.classList.remove('active');
39
+ navMenuWrapper.classList.remove('active');
40
+ body.classList.remove('menu-open');
41
+ }
42
+ });
43
+
44
+ // Close menu on ESC key
45
+ document.addEventListener('keydown', function (event) {
46
+ if (event.key === 'Escape' && navMenuWrapper.classList.contains('active')) {
47
+ hamburger.classList.remove('active');
48
+ navMenuWrapper.classList.remove('active');
49
+ body.classList.remove('menu-open');
50
+ }
51
+ });
52
+ }
53
+
54
+ // ==================== VIEWPORT HEIGHT FIX (iOS) ====================
55
+ // Fix for 100vh on mobile browsers (address bar issue)
56
+ function setViewportHeight() {
57
+ const vh = window.innerHeight * 0.01;
58
+ document.documentElement.style.setProperty('--vh', `${vh}px`);
59
+ }
60
+
61
+ setViewportHeight();
62
+ window.addEventListener('resize', setViewportHeight);
63
+ window.addEventListener('orientationchange', setViewportHeight);
64
+
65
+ // ==================== TOUCH IMPROVEMENTS ====================
66
+ // Add touch-active class for better touch feedback
67
+ document.querySelectorAll('button, a, .tech-card, .showcase-item, .history-card').forEach(element => {
68
+ element.addEventListener('touchstart', function () {
69
+ this.classList.add('touch-active');
70
+ }, { passive: true });
71
+
72
+ element.addEventListener('touchend', function () {
73
+ this.classList.remove('touch-active');
74
+ }, { passive: true });
75
+
76
+ element.addEventListener('touchcancel', function () {
77
+ this.classList.remove('touch-active');
78
+ }, { passive: true });
79
+ });
80
+
81
+ // ==================== PREVENT ZOOM ON INPUT FOCUS ====================
82
+ // Already handled in CSS with font-size: 16px, but adding for completeness
83
+ const inputs = document.querySelectorAll('input, textarea, select');
84
+ inputs.forEach(input => {
85
+ input.addEventListener('focus', function () {
86
+ const viewport = document.querySelector('meta[name=viewport]');
87
+ if (viewport) {
88
+ viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0';
89
+ }
90
+ });
91
+
92
+ input.addEventListener('blur', function () {
93
+ const viewport = document.querySelector('meta[name=viewport]');
94
+ if (viewport) {
95
+ viewport.content = 'width=device-width, initial-scale=1.0';
96
+ }
97
+ });
98
+ });
99
+
100
+ // ==================== MOBILE DETECTION ====================
101
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
102
+ const isTablet = /(iPad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test(navigator.userAgent.toLowerCase());
103
+
104
+ if (isMobile) {
105
+ document.body.classList.add('is-mobile');
106
+ }
107
+ if (isTablet) {
108
+ document.body.classList.add('is-tablet');
109
+ }
110
+
111
+ // ==================== SMOOTH SCROLL POLYFILL ====================
112
+ // For browsers that don't support smooth scrolling
113
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
114
+ anchor.addEventListener('click', function (e) {
115
+ const target = document.querySelector(this.getAttribute('href'));
116
+ if (target) {
117
+ e.preventDefault();
118
+ target.scrollIntoView({
119
+ behavior: 'smooth',
120
+ block: 'start'
121
+ });
122
+ }
123
+ });
124
+ });
125
+
126
+ // ==================== DEBOUNCED RESIZE HANDLER ====================
127
+ let resizeTimer;
128
+ window.addEventListener('resize', function () {
129
+ clearTimeout(resizeTimer);
130
+ resizeTimer = setTimeout(function () {
131
+ // Trigger custom event that other scripts can listen to
132
+ window.dispatchEvent(new CustomEvent('debouncedResize'));
133
+ }, 250);
134
+ });
135
+
136
+ // ==================== LAZY LOAD OPTIMIZATION ====================
137
+ // Only load images when they're about to enter the viewport
138
+ if ('IntersectionObserver' in window) {
139
+ const imageObserver = new IntersectionObserver((entries, observer) => {
140
+ entries.forEach(entry => {
141
+ if (entry.isIntersecting) {
142
+ const img = entry.target;
143
+ if (img.dataset.src) {
144
+ img.src = img.dataset.src;
145
+ img.removeAttribute('data-src');
146
+ observer.unobserve(img);
147
+ }
148
+ }
149
+ });
150
+ }, {
151
+ rootMargin: '50px'
152
+ });
153
+
154
+ document.querySelectorAll('img[data-src]').forEach(img => {
155
+ imageObserver.observe(img);
156
+ });
157
+ }
158
+
159
+ // ==================== PREVENT OVERSCROLL (iOS) ====================
160
+ // Prevent rubber-band scrolling on iOS
161
+ let scrollStartY = 0;
162
+
163
+ document.addEventListener('touchstart', function (e) {
164
+ scrollStartY = e.touches[0].pageY;
165
+ }, { passive: true });
166
+
167
+ document.addEventListener('touchmove', function (e) {
168
+ const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
169
+ const scrollHeight = document.documentElement.scrollHeight;
170
+ const clientHeight = document.documentElement.clientHeight;
171
+ const scrollY = e.touches[0].pageY;
172
+
173
+ // Prevent overscroll at top
174
+ if (scrollTop === 0 && scrollY > scrollStartY) {
175
+ e.preventDefault();
176
+ }
177
+
178
+ // Prevent overscroll at bottom
179
+ if (scrollTop + clientHeight >= scrollHeight && scrollY < scrollStartY) {
180
+ e.preventDefault();
181
+ }
182
+ }, { passive: false });
183
+
184
+ // ==================== PERFORMANCE OPTIMIZATION ====================
185
+ // Reduce animations on low-end devices
186
+ if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) {
187
+ document.body.classList.add('reduce-motion');
188
+ }
189
+
190
+ // Detect slow connection
191
+ if ('connection' in navigator) {
192
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
193
+ if (connection && (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g')) {
194
+ document.body.classList.add('slow-connection');
195
+ // Disable heavy animations
196
+ document.querySelectorAll('.floating-3d-object').forEach(el => {
197
+ el.style.display = 'none';
198
+ });
199
+ }
200
+ }
201
+
202
+ // ==================== HORIZONTAL SCROLL INDICATOR ====================
203
+ // Add scroll indicator for tables on mobile
204
+ const scrollableElements = document.querySelectorAll('.history-table-container, .pipeline');
205
+ scrollableElements.forEach(element => {
206
+ if (element.scrollWidth > element.clientWidth) {
207
+ element.classList.add('has-horizontal-scroll');
208
+
209
+ // Remove indicator after first scroll
210
+ element.addEventListener('scroll', function () {
211
+ this.classList.remove('has-horizontal-scroll');
212
+ }, { once: true });
213
+ }
214
+ });
215
+
216
+ // ==================== STATUS BAR COLOR (PWA) ====================
217
+ // Set theme color for mobile browsers
218
+ const metaThemeColor = document.querySelector('meta[name=theme-color]');
219
+ if (!metaThemeColor) {
220
+ const meta = document.createElement('meta');
221
+ meta.name = 'theme-color';
222
+ meta.content = '#000000';
223
+ document.head.appendChild(meta);
224
+ }
225
+
226
+ console.log('🚀 Mobile optimizations loaded');
227
+ })();
frontend/motion.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * DeepGuard Motion Design System
3
+ * Implements: Lenis Smooth Scroll, Magnetic Buttons, Spotlight Cards, Text Reveals
4
+ */
5
+
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ const lenis = initSmoothScroll();
8
+ initMagneticButtons();
9
+ initSpotlightCards();
10
+ initTextReveals();
11
+ if (lenis) {
12
+ initParallax(lenis);
13
+ }
14
+ });
15
+
16
+ /* ==================== 1. SMOOTH SCROLL (LENIS) ==================== */
17
+ function initSmoothScroll() {
18
+ // Check if Lenis is loaded
19
+ if (typeof Lenis === 'undefined') {
20
+ console.warn('Lenis not loaded. Skipping smooth scroll.');
21
+ return null;
22
+ }
23
+
24
+ const lenis = new Lenis({
25
+ duration: 1.2,
26
+ easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
27
+ direction: 'vertical',
28
+ gestureDirection: 'vertical',
29
+ smooth: true,
30
+ mouseMultiplier: 1,
31
+ smoothTouch: false,
32
+ touchMultiplier: 2,
33
+ });
34
+
35
+ function raf(time) {
36
+ lenis.raf(time);
37
+ requestAnimationFrame(raf);
38
+ }
39
+
40
+ requestAnimationFrame(raf);
41
+
42
+ return lenis;
43
+ }
44
+
45
+ /* ==================== 2. MAGNETIC BUTTONS ==================== */
46
+ function initMagneticButtons() {
47
+ const buttons = document.querySelectorAll('.btn-primary, .btn-hero-primary');
48
+
49
+ buttons.forEach(btn => {
50
+ btn.addEventListener('mousemove', (e) => {
51
+ const rect = btn.getBoundingClientRect();
52
+ const x = e.clientX - rect.left;
53
+ const y = e.clientY - rect.top;
54
+
55
+ // Calculate distance from center
56
+ const centerX = rect.width / 2;
57
+ const centerY = rect.height / 2;
58
+
59
+ const deltaX = (x - centerX) * 0.3; // Strength of pull
60
+ const deltaY = (y - centerY) * 0.3;
61
+
62
+ btn.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
63
+ });
64
+
65
+ btn.addEventListener('mouseleave', () => {
66
+ btn.style.transform = 'translate(0px, 0px)';
67
+ });
68
+ });
69
+ }
70
+
71
+ /* ==================== 3. SPOTLIGHT CARDS ==================== */
72
+ function initSpotlightCards() {
73
+ const cards = document.querySelectorAll('.feature-card, .showcase-item, .tech-card');
74
+
75
+ cards.forEach(card => {
76
+ card.addEventListener('mousemove', (e) => {
77
+ const rect = card.getBoundingClientRect();
78
+ const x = e.clientX - rect.left;
79
+ const y = e.clientY - rect.top;
80
+
81
+ card.style.setProperty('--mouse-x', `${x}px`);
82
+ card.style.setProperty('--mouse-y', `${y}px`);
83
+ });
84
+ });
85
+ }
86
+
87
+ /* ==================== 4. TEXT REVEALS ==================== */
88
+ function initTextReveals() {
89
+ // Targets: Hero title, Section titles
90
+ const targets = document.querySelectorAll('.hero-title, .section-title');
91
+
92
+ const observer = new IntersectionObserver((entries) => {
93
+ entries.forEach(entry => {
94
+ if (entry.isIntersecting) {
95
+ entry.target.classList.add('in-view');
96
+ observer.unobserve(entry.target); // Only animate once
97
+ }
98
+ });
99
+ }, {
100
+ threshold: 0.2
101
+ });
102
+
103
+ targets.forEach(target => {
104
+ observer.observe(target);
105
+ });
106
+ }
107
+
108
+ /* ==================== 5. PARALLAX EFFECTS ==================== */
109
+ function initParallax(lenis) {
110
+ const parallaxItems = document.querySelectorAll('[data-speed]');
111
+
112
+ if (parallaxItems.length === 0) return;
113
+
114
+ lenis.on('scroll', ({ scroll }) => {
115
+ parallaxItems.forEach(item => {
116
+ const speed = parseFloat(item.dataset.speed) || 0;
117
+ // Apply standard translation
118
+ // Note: This overrides other transforms, so use on dedicated wrappers or elements without other transforms
119
+ item.style.transform = `translateY(${scroll * speed}px)`;
120
+ });
121
+ });
122
+ }
frontend/offline.html ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>Offline - DeepGuard</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="stylesheet" href="responsive-additions.css">
10
+ <style>
11
+ .offline-container {
12
+ min-height: 100vh;
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ justify-content: center;
17
+ text-align: center;
18
+ padding: 40px 20px;
19
+ background: var(--primary-bg);
20
+ }
21
+
22
+ .offline-icon {
23
+ font-size: 120px;
24
+ margin-bottom: 30px;
25
+ opacity: 0.3;
26
+ }
27
+
28
+ .offline-title {
29
+ font-size: 48px;
30
+ font-weight: 800;
31
+ color: var(--accent-yellow);
32
+ margin-bottom: 20px;
33
+ font-family: var(--font-display);
34
+ }
35
+
36
+ .offline-message {
37
+ font-size: 20px;
38
+ color: var(--text-secondary);
39
+ max-width: 500px;
40
+ margin: 0 auto 40px;
41
+ line-height: 1.6;
42
+ }
43
+
44
+ .offline-actions {
45
+ display: flex;
46
+ gap: 16px;
47
+ flex-wrap: wrap;
48
+ justify-content: center;
49
+ }
50
+
51
+ .btn-retry {
52
+ padding: 16px 32px;
53
+ background: var(--accent-yellow);
54
+ color: #000;
55
+ border: none;
56
+ border-radius: 12px;
57
+ font-size: 16px;
58
+ font-weight: 600;
59
+ cursor: pointer;
60
+ transition: all 0.3s ease;
61
+ }
62
+
63
+ .btn-retry:hover {
64
+ transform: translateY(-2px);
65
+ box-shadow: 0 10px 30px rgba(227, 245, 20, 0.3);
66
+ }
67
+
68
+ .btn-home {
69
+ padding: 16px 32px;
70
+ background: transparent;
71
+ color: var(--accent-yellow);
72
+ border: 2px solid var(--accent-yellow);
73
+ border-radius: 12px;
74
+ font-size: 16px;
75
+ font-weight: 600;
76
+ cursor: pointer;
77
+ transition: all 0.3s ease;
78
+ text-decoration: none;
79
+ display: inline-block;
80
+ }
81
+
82
+ .btn-home:hover {
83
+ background: rgba(227, 245, 20, 0.1);
84
+ }
85
+
86
+ .offline-tips {
87
+ margin-top: 60px;
88
+ padding: 30px;
89
+ background: rgba(255, 255, 255, 0.03);
90
+ border: 1px solid rgba(255, 255, 255, 0.1);
91
+ border-radius: 16px;
92
+ max-width: 600px;
93
+ }
94
+
95
+ .offline-tips h3 {
96
+ color: var(--accent-yellow);
97
+ margin-bottom: 15px;
98
+ font-size: 18px;
99
+ }
100
+
101
+ .offline-tips ul {
102
+ list-style: none;
103
+ padding: 0;
104
+ text-align: left;
105
+ color: var(--text-secondary);
106
+ }
107
+
108
+ .offline-tips li {
109
+ padding: 8px 0;
110
+ padding-left: 30px;
111
+ position: relative;
112
+ }
113
+
114
+ .offline-tips li::before {
115
+ content: '•';
116
+ color: var(--accent-yellow);
117
+ position: absolute;
118
+ left: 0;
119
+ font-size: 24px;
120
+ }
121
+ </style>
122
+ </head>
123
+
124
+ <body>
125
+ <div class="offline-container">
126
+ <div class="offline-icon">📡</div>
127
+ <h1 class="offline-title">You're Offline</h1>
128
+ <p class="offline-message">
129
+ It looks like you've lost your internet connection. Some features may not be available until you're back
130
+ online.
131
+ </p>
132
+
133
+ <div class="offline-actions">
134
+ <button class="btn-retry" onclick="location.reload()">
135
+ 🔄 Try Again
136
+ </button>
137
+ <a href="/index.html" class="btn-home">
138
+ 🏠 Go Home
139
+ </a>
140
+ </div>
141
+
142
+ <div class="offline-tips">
143
+ <h3>While You're Offline:</h3>
144
+ <ul>
145
+ <li>You can still browse previously loaded pages</li>
146
+ <li>Cached content remains available</li>
147
+ <li>Analysis history is accessible</li>
148
+ <li>New analyses require an internet connection</li>
149
+ </ul>
150
+ </div>
151
+ </div>
152
+
153
+ <script>
154
+ // Auto-retry when online
155
+ window.addEventListener('online', () => {
156
+ console.log('Connection restored');
157
+ setTimeout(() => {
158
+ location.reload();
159
+ }, 1000);
160
+ });
161
+
162
+ // Check connection status
163
+ if (navigator.onLine) {
164
+ console.log('Online - attempting to reload');
165
+ location.reload();
166
+ }
167
+ </script>
168
+ </body>
169
+
170
+ </html>
frontend/orbit.css ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 3D Orbit - Stabilized & High Fidelity */
2
+ .orbit-section {
3
+ padding: 100px 0;
4
+ position: relative;
5
+ overflow: hidden;
6
+ background: var(--primary-bg);
7
+ }
8
+
9
+ .orbit-container {
10
+ position: relative;
11
+ height: 750px;
12
+ width: 100%;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ perspective: 1500px;
17
+ }
18
+
19
+ /* 2D Overlay Elements - High Priority */
20
+ .orbit-info-card {
21
+ position: absolute;
22
+ top: 50%;
23
+ left: 50%;
24
+ transform: translate(-50%, -50%) scale(0.9);
25
+ width: 440px;
26
+ padding: 40px;
27
+ background: rgba(5, 5, 5, 0.98);
28
+ backdrop-filter: blur(30px);
29
+ border: 2px solid var(--accent-yellow);
30
+ box-shadow: 0 0 100px rgba(0, 0, 0, 0.95);
31
+ border-radius: 35px;
32
+ text-align: center;
33
+ opacity: 0;
34
+ visibility: hidden;
35
+ transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1),
36
+ opacity 0.4s ease,
37
+ visibility 0.4s;
38
+ z-index: 2000;
39
+ /* Absolute Top */
40
+ pointer-events: auto;
41
+ backface-visibility: hidden;
42
+ }
43
+
44
+ .orbit-info-card.active {
45
+ opacity: 1;
46
+ visibility: visible;
47
+ transform: translate(-50%, -50%) scale(1.0);
48
+ }
49
+
50
+ .orbit-info-title {
51
+ font-size: 26px;
52
+ font-weight: 800;
53
+ color: var(--accent-yellow);
54
+ margin-bottom: 12px;
55
+ text-shadow: 0 0 15px rgba(227, 245, 20, 0.3);
56
+ }
57
+
58
+ .orbit-info-desc {
59
+ color: var(--text-secondary);
60
+ line-height: 1.8;
61
+ font-size: 16px;
62
+ margin-bottom: 25px;
63
+ }
64
+
65
+ .orbit-tag {
66
+ font-size: 11px;
67
+ padding: 6px 14px;
68
+ background: rgba(227, 245, 20, 0.1);
69
+ border: 1px solid rgba(227, 245, 20, 0.3);
70
+ color: var(--accent-yellow);
71
+ border-radius: 25px;
72
+ font-weight: 700;
73
+ margin: 0 5px;
74
+ }
75
+
76
+ /* 3D Stage Space */
77
+ .orbit-stage {
78
+ position: absolute;
79
+ width: 1000px;
80
+ height: 1000px;
81
+ transform-style: preserve-3d;
82
+ transform: rotateX(65deg);
83
+ /* The fixed 3D slant */
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ pointer-events: none;
88
+ }
89
+
90
+ .orbit-hub {
91
+ position: absolute;
92
+ width: 300px;
93
+ height: 300px;
94
+ transform: rotateX(-65deg);
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ z-index: 5;
99
+ }
100
+
101
+ .orbit-hub-inner {
102
+ width: 140px;
103
+ height: 140px;
104
+ background: #000;
105
+ border: 3px solid var(--accent-yellow);
106
+ border-radius: 50%;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ font-size: 55px;
111
+ box-shadow: 0 0 50px rgba(227, 245, 20, 0.3);
112
+ animation: hub-pulse-v3 5s infinite ease-in-out;
113
+ }
114
+
115
+ @keyframes hub-pulse-v3 {
116
+
117
+ 0%,
118
+ 100% {
119
+ transform: scale(1);
120
+ box-shadow: 0 0 50px rgba(227, 245, 20, 0.3);
121
+ }
122
+
123
+ 50% {
124
+ transform: scale(1.1);
125
+ box-shadow: 0 0 80px rgba(227, 245, 20, 0.5);
126
+ }
127
+ }
128
+
129
+ /* Spinning Plane */
130
+ .orbit-ring {
131
+ position: absolute;
132
+ width: 750px;
133
+ height: 750px;
134
+ transform-style: preserve-3d;
135
+ animation: orbit-main-v4 45s linear infinite;
136
+ pointer-events: none;
137
+ }
138
+
139
+ /* Smooth Pause */
140
+ .orbit-ring.paused,
141
+ .orbit-ring.paused .node-content {
142
+ animation-play-state: paused;
143
+ }
144
+
145
+ @keyframes orbit-main-v4 {
146
+ from {
147
+ transform: rotateZ(0deg);
148
+ }
149
+
150
+ to {
151
+ transform: rotateZ(360deg);
152
+ }
153
+ }
154
+
155
+ /* Nodes */
156
+ .orbit-node {
157
+ position: absolute;
158
+ width: 120px;
159
+ height: 120px;
160
+ background: #000;
161
+ border: 2px solid rgba(255, 255, 255, 0.15);
162
+ border-radius: 50%;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ cursor: pointer;
167
+ pointer-events: auto;
168
+ z-index: 100;
169
+ /* Use transitions carefully to avoid glitching during ring rotation */
170
+ transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
171
+ backface-visibility: hidden;
172
+ }
173
+
174
+ .orbit-node:hover {
175
+ border-color: var(--accent-yellow);
176
+ box-shadow: 0 0 40px rgba(227, 245, 20, 0.5);
177
+ transform: scale(1.3);
178
+ }
179
+
180
+ .node-content {
181
+ width: 100%;
182
+ height: 100%;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ /* Billboard */
187
+ transform: rotateX(-65deg);
188
+ animation: counter-v4 45s linear infinite;
189
+ backface-visibility: hidden;
190
+ }
191
+
192
+ @keyframes counter-v4 {
193
+ from {
194
+ transform: rotateX(-65deg) rotateZ(0deg);
195
+ }
196
+
197
+ to {
198
+ transform: rotateX(-65deg) rotateZ(-360deg);
199
+ }
200
+ }
201
+
202
+ .node-icon {
203
+ width: 70px;
204
+ height: 70px;
205
+ object-fit: contain;
206
+ /* Fix for white backgrounds in icons - Blend them out */
207
+ mix-blend-mode: screen;
208
+ filter: drop-shadow(0 0 10px rgba(227, 245, 20, 0.3));
209
+ pointer-events: none;
210
+ }
211
+
212
+ /* Static Positions */
213
+ .node-1 {
214
+ top: 0;
215
+ left: 50%;
216
+ transform: translate(-50%, -50%);
217
+ }
218
+
219
+ .node-2 {
220
+ top: 50%;
221
+ right: 0;
222
+ transform: translate(50%, -50%);
223
+ }
224
+
225
+ .node-3 {
226
+ bottom: 0;
227
+ left: 50%;
228
+ transform: translate(-50%, 50%);
229
+ }
230
+
231
+ .node-4 {
232
+ top: 50%;
233
+ left: 0;
234
+ transform: translate(-50%, -50%);
235
+ }
236
+
237
+ /* Mobile */
238
+ @media (max-width: 900px) {
239
+ .orbit-ring {
240
+ width: 500px;
241
+ height: 500px;
242
+ }
243
+
244
+ .node-1 {
245
+ transform: rotate(0deg) translate(250px);
246
+ }
247
+
248
+ .node-2 {
249
+ transform: rotate(90deg) translate(250px);
250
+ }
251
+
252
+ .node-3 {
253
+ transform: rotate(180deg) translate(250px);
254
+ }
255
+
256
+ .node-4 {
257
+ transform: rotate(270deg) translate(250px);
258
+ }
259
+
260
+ .orbit-info-card {
261
+ width: 340px;
262
+ padding: 25px;
263
+ }
264
+
265
+ .orbit-node {
266
+ width: 90px;
267
+ height: 90px;
268
+ }
269
+
270
+ .node-icon {
271
+ width: 45px;
272
+ height: 45px;
273
+ }
274
+ }
frontend/orbit_interaction.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const featureData = {
2
+ fusion: {
3
+ title: "Hybrid Fusion Architecture",
4
+ desc: "Combines EfficientNetV2 for spatial details and Swin Transformer V2 for global context with frequency domain analysis.",
5
+ tags: ["CNN-ViT", "Multi-Modal", "Spatial-Temporal"]
6
+ },
7
+ realtime: {
8
+ title: "Real-Time Analysis",
9
+ desc: "Lightning-fast detection processing thousands of images per minute with GPU acceleration and optimized pipelines.",
10
+ tags: ["CUDA", "Batch Process", "Low Latency"]
11
+ },
12
+ accuracy: {
13
+ title: "97% Detection Accuracy",
14
+ desc: "Industry-leading precision in detecting AI-generated and manipulated media across various generation methods.",
15
+ tags: ["Verified", "Tested", "SOTA"]
16
+ },
17
+ analytics: {
18
+ title: "Advanced Analytics",
19
+ desc: "Comprehensive reports with confidence scores, heatmaps, and detailed forensic analysis for every scan.",
20
+ tags: ["Reports", "Forensics", "Heatmaps"]
21
+ }
22
+ };
23
+
24
+ function initRobustOrbit() {
25
+ const nodes = document.querySelectorAll('.orbit-node');
26
+ const ring = document.querySelector('.orbit-ring');
27
+ const card = document.getElementById('orbitInfoCard');
28
+ const title = document.getElementById('orbitTitle');
29
+ const desc = document.getElementById('orbitDesc');
30
+ const tagsContainer = document.getElementById('orbitTags');
31
+ const hub = document.querySelector('.orbit-hub-inner');
32
+
33
+ if (!nodes.length || !ring || !card) return;
34
+
35
+ let hoverTimeout;
36
+ let isHoveringNode = false;
37
+ let isHoveringCard = false;
38
+
39
+ const updateState = () => {
40
+ if (isHoveringNode || isHoveringCard) {
41
+ clearTimeout(hoverTimeout);
42
+ card.classList.add('active');
43
+ ring.classList.add('paused');
44
+ if (hub) {
45
+ hub.style.boxShadow = "0 0 70px var(--accent-yellow)";
46
+ hub.textContent = "🔍";
47
+ }
48
+ } else {
49
+ hoverTimeout = setTimeout(() => {
50
+ if (!isHoveringNode && !isHoveringCard) {
51
+ card.classList.remove('active');
52
+ ring.classList.remove('paused');
53
+ if (hub) {
54
+ hub.style.boxShadow = "";
55
+ hub.textContent = "🛡️";
56
+ }
57
+ }
58
+ }, 150);
59
+ }
60
+ };
61
+
62
+ nodes.forEach(node => {
63
+ node.addEventListener('mouseenter', () => {
64
+ isHoveringNode = true;
65
+ const id = node.getAttribute('data-id');
66
+ const data = featureData[id];
67
+ if (data) {
68
+ title.textContent = data.title;
69
+ desc.textContent = data.desc;
70
+ tagsContainer.innerHTML = data.tags.map(t => `<span class="orbit-tag">${t}</span>`).join('');
71
+ }
72
+ updateState();
73
+ });
74
+
75
+ node.addEventListener('mouseleave', () => {
76
+ isHoveringNode = false;
77
+ updateState();
78
+ });
79
+ });
80
+
81
+ // Keeping it frozen when mouse is over the card itself
82
+ card.addEventListener('mouseenter', () => {
83
+ isHoveringCard = true;
84
+ updateState();
85
+ });
86
+
87
+ card.addEventListener('mouseleave', () => {
88
+ isHoveringCard = false;
89
+ updateState();
90
+ });
91
+
92
+ // Stability: Force animation refresh
93
+ requestAnimationFrame(() => {
94
+ ring.style.animation = 'none';
95
+ void ring.offsetWidth;
96
+ ring.style.animation = 'orbit-main-v4 45s linear infinite';
97
+
98
+ document.querySelectorAll('.node-content').forEach(nc => {
99
+ nc.style.animation = 'none';
100
+ void nc.offsetWidth;
101
+ nc.style.animation = 'counter-v4 45s linear infinite';
102
+ });
103
+ });
104
+ }
105
+
106
+ // Multi-method startup
107
+ if (document.readyState === 'loading') {
108
+ document.addEventListener('DOMContentLoaded', initRobustOrbit);
109
+ } else {
110
+ initRobustOrbit();
111
+ }
112
+ window.addEventListener('load', initRobustOrbit);
frontend/pwa.css ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== PWA STYLES ==================== */
2
+
3
+ /* PWA Install Button */
4
+ .pwa-install-button {
5
+ position: fixed;
6
+ bottom: 30px;
7
+ right: 30px;
8
+ z-index: 1000;
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 10px;
12
+ padding: 14px 24px;
13
+ background: var(--accent-yellow);
14
+ color: #000;
15
+ border: none;
16
+ border-radius: 50px;
17
+ font-size: 15px;
18
+ font-weight: 600;
19
+ cursor: pointer;
20
+ box-shadow: 0 10px 40px rgba(227, 245, 20, 0.3);
21
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
22
+ opacity: 0;
23
+ transform: translateY(20px) scale(0.9);
24
+ pointer-events: none;
25
+ }
26
+
27
+ .pwa-install-button.visible {
28
+ opacity: 1;
29
+ transform: translateY(0) scale(1);
30
+ pointer-events: all;
31
+ }
32
+
33
+ .pwa-install-button:hover {
34
+ transform: translateY(-2px) scale(1.05);
35
+ box-shadow: 0 15px 50px rgba(227, 245, 20, 0.4);
36
+ }
37
+
38
+ .pwa-install-button:active {
39
+ transform: translateY(0) scale(0.98);
40
+ }
41
+
42
+ .pwa-install-button svg {
43
+ width: 20px;
44
+ height: 20px;
45
+ }
46
+
47
+ /* PWA Update Notification */
48
+ .pwa-update-notification {
49
+ position: fixed;
50
+ top: 20px;
51
+ left: 50%;
52
+ transform: translateX(-50%) translateY(-120%);
53
+ z-index: 10000;
54
+ background: rgba(0, 0, 0, 0.95);
55
+ backdrop-filter: blur(20px);
56
+ border: 1px solid rgba(227, 245, 20, 0.3);
57
+ border-radius: 16px;
58
+ padding: 0;
59
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
60
+ transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
61
+ max-width: 500px;
62
+ width: 90%;
63
+ }
64
+
65
+ .pwa-update-notification.visible {
66
+ transform: translateX(-50%) translateY(0);
67
+ }
68
+
69
+ .update-content {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 16px;
73
+ padding: 20px;
74
+ position: relative;
75
+ }
76
+
77
+ .update-icon {
78
+ font-size: 40px;
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ .update-text {
83
+ flex: 1;
84
+ }
85
+
86
+ .update-text strong {
87
+ color: var(--accent-yellow);
88
+ font-size: 16px;
89
+ display: block;
90
+ margin-bottom: 4px;
91
+ }
92
+
93
+ .update-text p {
94
+ color: #888;
95
+ font-size: 13px;
96
+ margin: 0;
97
+ }
98
+
99
+ .update-btn {
100
+ padding: 10px 20px;
101
+ background: var(--accent-yellow);
102
+ color: #000;
103
+ border: none;
104
+ border-radius: 8px;
105
+ font-size: 14px;
106
+ font-weight: 600;
107
+ cursor: pointer;
108
+ transition: all 0.2s ease;
109
+ flex-shrink: 0;
110
+ }
111
+
112
+ .update-btn:hover {
113
+ transform: translateY(-2px);
114
+ box-shadow: 0 5px 15px rgba(227, 245, 20, 0.3);
115
+ }
116
+
117
+ .update-dismiss {
118
+ position: absolute;
119
+ top: 10px;
120
+ right: 10px;
121
+ background: transparent;
122
+ border: none;
123
+ color: #666;
124
+ font-size: 18px;
125
+ cursor: pointer;
126
+ width: 30px;
127
+ height: 30px;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ border-radius: 50%;
132
+ transition: all 0.2s ease;
133
+ }
134
+
135
+ .update-dismiss:hover {
136
+ background: rgba(255, 255, 255, 0.1);
137
+ color: #fff;
138
+ }
139
+
140
+ /* iOS Install Prompt */
141
+ .ios-install-prompt {
142
+ position: fixed;
143
+ bottom: 0;
144
+ left: 0;
145
+ right: 0;
146
+ z-index: 10000;
147
+ background: rgba(0, 0, 0, 0.98);
148
+ backdrop-filter: blur(20px);
149
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
150
+ padding: 30px 20px;
151
+ transform: translateY(100%);
152
+ transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
153
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
154
+ }
155
+
156
+ .ios-install-prompt.visible {
157
+ transform: translateY(0);
158
+ }
159
+
160
+ .ios-prompt-content {
161
+ max-width: 500px;
162
+ margin: 0 auto;
163
+ position: relative;
164
+ }
165
+
166
+ .ios-prompt-close {
167
+ position: absolute;
168
+ top: -10px;
169
+ right: 0;
170
+ background: transparent;
171
+ border: none;
172
+ color: #666;
173
+ font-size: 24px;
174
+ cursor: pointer;
175
+ width: 40px;
176
+ height: 40px;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+
182
+ .ios-prompt-icon {
183
+ text-align: center;
184
+ margin-bottom: 15px;
185
+ }
186
+
187
+ .ios-prompt-content h3 {
188
+ color: var(--accent-yellow);
189
+ text-align: center;
190
+ font-size: 20px;
191
+ margin-bottom: 10px;
192
+ }
193
+
194
+ .ios-prompt-content p {
195
+ color: #888;
196
+ text-align: center;
197
+ margin-bottom: 15px;
198
+ }
199
+
200
+ .ios-prompt-content ol {
201
+ color: #fff;
202
+ padding-left: 20px;
203
+ font-size: 14px;
204
+ line-height: 1.8;
205
+ }
206
+
207
+ .ios-prompt-content ol li {
208
+ margin-bottom: 8px;
209
+ }
210
+
211
+ .ios-prompt-content ol strong {
212
+ color: var(--accent-yellow);
213
+ }
214
+
215
+ /* PWA Mode Adjustments */
216
+ body.pwa-mode {
217
+ /* Add any PWA-specific styles */
218
+ }
219
+
220
+ /* iOS PWA Status Bar Spacing */
221
+ body.ios-pwa {
222
+ padding-top: env(safe-area-inset-top);
223
+ padding-bottom: env(safe-area-inset-bottom);
224
+ }
225
+
226
+ body.ios-pwa .navbar {
227
+ padding-top: calc(env(safe-area-inset-top) + 20px);
228
+ }
229
+
230
+ /* Hide install button on mobile when already in PWA mode */
231
+ body.pwa-mode .pwa-install-button {
232
+ display: none;
233
+ }
234
+
235
+ /* Mobile-specific adjustments */
236
+ @media (max-width: 768px) {
237
+ .pwa-install-button {
238
+ bottom: 20px;
239
+ right: 20px;
240
+ padding: 12px 20px;
241
+ font-size: 14px;
242
+ }
243
+
244
+ .pwa-update-notification {
245
+ top: 10px;
246
+ width: 95%;
247
+ }
248
+
249
+ .update-content {
250
+ flex-wrap: wrap;
251
+ padding: 15px;
252
+ }
253
+
254
+ .update-btn {
255
+ width: 100%;
256
+ margin-top: 10px;
257
+ }
258
+ }
259
+
260
+ /* Slideup animation for simple toast */
261
+ @keyframes slideUp {
262
+ from {
263
+ opacity: 0;
264
+ transform: translate(-50%, 20px);
265
+ }
266
+
267
+ to {
268
+ opacity: 1;
269
+ transform: translate(-50%, 0);
270
+ }
271
+ }
frontend/pwa.js ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * PWA Installation and Service Worker Registration
3
+ */
4
+
5
+ (function () {
6
+ 'use strict';
7
+
8
+ // ==================== SERVICE WORKER REGISTRATION ====================
9
+ if ('serviceWorker' in navigator) {
10
+ window.addEventListener('load', () => {
11
+ navigator.serviceWorker.register('/service-worker.js')
12
+ .then((registration) => {
13
+ console.log('✅ Service Worker registered:', registration.scope);
14
+
15
+ // Check for updates
16
+ registration.addEventListener('updatefound', () => {
17
+ const newWorker = registration.installing;
18
+ console.log('🔄 Service Worker update found');
19
+
20
+ newWorker.addEventListener('statechange', () => {
21
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
22
+ // New version available
23
+ showUpdateNotification();
24
+ }
25
+ });
26
+ });
27
+ })
28
+ .catch((error) => {
29
+ console.error('❌ Service Worker registration failed:', error);
30
+ });
31
+ });
32
+ }
33
+
34
+ // ==================== PWA INSTALL PROMPT ====================
35
+ let deferredPrompt;
36
+ let installButton;
37
+
38
+ // Listen for install prompt event
39
+ window.addEventListener('beforeinstallprompt', (e) => {
40
+ console.log('💾 Install prompt triggered');
41
+
42
+ // Prevent Chrome 67 and earlier from automatically showing the prompt
43
+ e.preventDefault();
44
+
45
+ // Store the event for later use
46
+ deferredPrompt = e;
47
+
48
+ // Show install button
49
+ showInstallButton();
50
+ });
51
+
52
+ // Create and show install button
53
+ function showInstallButton() {
54
+ // Check if already installed
55
+ if (window.matchMedia('(display-mode: standalone)').matches) {
56
+ console.log('Already installed as PWA');
57
+ return;
58
+ }
59
+
60
+ // Check if button already exists
61
+ if (document.getElementById('pwa-install-btn')) return;
62
+
63
+ // Create install button
64
+ installButton = document.createElement('button');
65
+ installButton.id = 'pwa-install-btn';
66
+ installButton.className = 'pwa-install-button';
67
+ installButton.innerHTML = `
68
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
70
+ <polyline points="7 10 12 15 17 10"></polyline>
71
+ <line x1="12" y1="15" x2="12" y2="3"></line>
72
+ </svg>
73
+ <span>Install App</span>
74
+ `;
75
+
76
+ installButton.addEventListener('click', handleInstallClick);
77
+
78
+ // Add to page
79
+ document.body.appendChild(installButton);
80
+
81
+ // Fade in animation
82
+ setTimeout(() => {
83
+ installButton.classList.add('visible');
84
+ }, 100);
85
+ }
86
+
87
+ // Handle install button click
88
+ async function handleInstallClick() {
89
+ if (!deferredPrompt) return;
90
+
91
+ // Show install prompt
92
+ deferredPrompt.prompt();
93
+
94
+ // Wait for user choice
95
+ const { outcome } = await deferredPrompt.userChoice;
96
+
97
+ console.log(`User response to install prompt: ${outcome}`);
98
+
99
+ if (outcome === 'accepted') {
100
+ console.log('✅ PWA installed');
101
+ hideInstallButton();
102
+ } else {
103
+ console.log('❌ PWA installation declined');
104
+ }
105
+
106
+ // Clear the deferredPrompt
107
+ deferredPrompt = null;
108
+ }
109
+
110
+ // Hide install button
111
+ function hideInstallButton() {
112
+ if (installButton) {
113
+ installButton.classList.remove('visible');
114
+ setTimeout(() => {
115
+ if (installButton && installButton.parentNode) {
116
+ installButton.remove();
117
+ }
118
+ }, 300);
119
+ }
120
+ }
121
+
122
+ // ==================== DETECT PWA MODE ====================
123
+ window.addEventListener('DOMContentLoaded', () => {
124
+ // Check if running as installed PWA
125
+ const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
126
+ window.navigator.standalone ||
127
+ document.referrer.includes('android-app://');
128
+
129
+ if (isStandalone) {
130
+ console.log('🚀 Running as PWA');
131
+ document.body.classList.add('pwa-mode');
132
+
133
+ // Add iOS status bar spacing
134
+ if (navigator.userAgent.match(/iPhone|iPad|iPod/)) {
135
+ document.body.classList.add('ios-pwa');
136
+ }
137
+ } else {
138
+ console.log('🌐 Running in browser');
139
+ }
140
+ });
141
+
142
+ // ==================== UPDATE NOTIFICATION ====================
143
+ function showUpdateNotification() {
144
+ // Check if notification already exists
145
+ if (document.getElementById('pwa-update-notification')) return;
146
+
147
+ const notification = document.createElement('div');
148
+ notification.id = 'pwa-update-notification';
149
+ notification.className = 'pwa-update-notification';
150
+ notification.innerHTML = `
151
+ <div class="update-content">
152
+ <div class="update-icon">🔄</div>
153
+ <div class="update-text">
154
+ <strong>New version available!</strong>
155
+ <p>Click to update and get the latest features</p>
156
+ </div>
157
+ <button class="update-btn" onclick="window.location.reload()">
158
+ Update Now
159
+ </button>
160
+ <button class="update-dismiss" onclick="this.parentElement.parentElement.remove()">
161
+
162
+ </button>
163
+ </div>
164
+ `;
165
+
166
+ document.body.appendChild(notification);
167
+
168
+ setTimeout(() => {
169
+ notification.classList.add('visible');
170
+ }, 100);
171
+ }
172
+
173
+ // ==================== ONLINE/OFFLINE STATUS ====================
174
+ window.addEventListener('online', () => {
175
+ console.log('🌐 Connection restored');
176
+ showToast('Back online!', 'success');
177
+ });
178
+
179
+ window.addEventListener('offline', () => {
180
+ console.log('📡 Connection lost');
181
+ showToast('You are offline. Some features may be limited.', 'warning');
182
+ });
183
+
184
+ // Helper function for toast (if not already defined)
185
+ function showToast(message, type = 'info') {
186
+ // Use existing toast function if available
187
+ if (window.showToast) {
188
+ window.showToast(message, type);
189
+ return;
190
+ }
191
+
192
+ // Simple fallback toast
193
+ const toast = document.createElement('div');
194
+ toast.className = `simple-toast toast-${type}`;
195
+ toast.textContent = message;
196
+ toast.style.cssText = `
197
+ position: fixed;
198
+ bottom: 20px;
199
+ left: 50%;
200
+ transform: translateX(-50%);
201
+ background: ${type === 'success' ? '#10b981' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
202
+ color: white;
203
+ padding: 12px 24px;
204
+ border-radius: 8px;
205
+ font-size: 14px;
206
+ z-index: 10000;
207
+ animation: slideUp 0.3s ease;
208
+ `;
209
+ document.body.appendChild(toast);
210
+
211
+ setTimeout(() => {
212
+ toast.remove();
213
+ }, 3000);
214
+ }
215
+
216
+ // ==================== iOS ADD TO HOME SCREEN PROMPT ====================
217
+ function showIOSInstallPrompt() {
218
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
219
+ const isInStandaloneMode = ('standalone' in window.navigator) && (window.navigator.standalone);
220
+
221
+ if (isIOS && !isInStandaloneMode) {
222
+ // Check if user has seen this before
223
+ if (localStorage.getItem('ios-install-prompt-dismissed')) {
224
+ return;
225
+ }
226
+
227
+ const prompt = document.createElement('div');
228
+ prompt.className = 'ios-install-prompt';
229
+ prompt.innerHTML = `
230
+ <div class="ios-prompt-content">
231
+ <button class="ios-prompt-close" onclick="this.parentElement.parentElement.remove(); localStorage.setItem('ios-install-prompt-dismissed', 'true');">✕</button>
232
+ <div class="ios-prompt-icon">
233
+ <img src="/logo.ico" alt="DeepGuard" style="width: 60px; height: 60px; border-radius: 12px;">
234
+ </div>
235
+ <h3>Install DeepGuard</h3>
236
+ <p>Install this app on your iPhone:</p>
237
+ <ol>
238
+ <li>Tap the <strong>Share</strong> button <svg width="16" height="20" viewBox="0 0 16 20" fill="#0066cc"><path d="M8 0l8 8h-5v12H5V8H0z"/></svg></li>
239
+ <li>Select <strong>Add to Home Screen</strong></li>
240
+ </ol>
241
+ </div>
242
+ `;
243
+ document.body.appendChild(prompt);
244
+
245
+ setTimeout(() => {
246
+ prompt.classList.add('visible');
247
+ }, 2000);
248
+ }
249
+ }
250
+
251
+ // Show iOS prompt after short delay
252
+ setTimeout(showIOSInstallPrompt, 3000);
253
+
254
+ console.log('📱 PWA features initialized');
255
+ })();
frontend/realtime_analysis_icon.png ADDED

Git LFS Details

  • SHA256: 1d01ebc0c85c5ca972544fbddb9cb4cdc6c4e025668ca30043f2b4ad5c6f8892
  • Pointer size: 131 Bytes
  • Size of remote file: 504 kB
frontend/responsive-additions.css ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== COMPREHENSIVE RESPONSIVE FIXES ==================== */
2
+ /* This file contains ALL responsive fixes for the entire frontend */
3
+ /* Preserves exact visual design on desktop while ensuring perfect mobile adaptation */
4
+
5
+ /* ==================== GLOBAL OVERFLOW PREVENTION ==================== */
6
+ /* ==================== GLOBAL OVERFLOW PREVENTION ==================== */
7
+ html,
8
+ body {
9
+ overflow-x: hidden !important;
10
+ max-width: 100%;
11
+ /* Changed from 100vw to avoid scrollbar width issues */
12
+ width: 100%;
13
+ overscroll-behavior-y: none;
14
+ /* Prevent bounce on mobile if handled by Lenis */
15
+ }
16
+
17
+ /* Ensure all major containers respect viewport */
18
+ .container,
19
+ .analysis-container,
20
+ .hero,
21
+ .section,
22
+ [class*="-container"],
23
+ [class*="-wrapper"] {
24
+ max-width: 100%;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ /* ==================== UNIVERSAL MEDIA CONSTRAINTS ==================== */
29
+ img,
30
+ video,
31
+ canvas,
32
+ iframe {
33
+ max-width: 100%;
34
+ height: auto;
35
+ }
36
+
37
+ /* ==================== LOADER / ANALYZING SCREEN FIXES ==================== */
38
+ @media (max-height: 700px) {
39
+
40
+ /* Fix loader cropping on short screens */
41
+ #loader-wrapper {
42
+ padding: 20px;
43
+ }
44
+
45
+ #loader-quote {
46
+ font-size: clamp(1rem, 4vh, 1.2rem) !important;
47
+ /* Fluid scaling */
48
+ margin-bottom: 1rem;
49
+ max-width: 95%;
50
+ line-height: 1.2;
51
+ }
52
+
53
+ .counter-wrapper {
54
+ font-size: clamp(3rem, 8vh, 5rem) !important;
55
+ /* Stable scaling */
56
+ bottom: 15px;
57
+ right: 15px;
58
+ }
59
+
60
+ .detection-status {
61
+ font-size: 1.2rem !important;
62
+ margin-bottom: 10px;
63
+ }
64
+
65
+ .loader-meta {
66
+ bottom: 15px;
67
+ font-size: 12px;
68
+ }
69
+ }
70
+
71
+ @media (max-width: 480px) {
72
+ #loader-quote {
73
+ font-size: 1rem !important;
74
+ padding: 10px;
75
+ letter-spacing: 0.03em;
76
+ }
77
+
78
+ .counter-wrapper {
79
+ font-size: clamp(4rem, 12vw, 6rem) !important;
80
+ bottom: 10px;
81
+ right: 10px;
82
+ }
83
+
84
+ .percent-symbol {
85
+ font-size: 0.5em;
86
+ /* Generic relative size */
87
+ }
88
+ }
89
+
90
+ /* ==================== ANALYSIS PAGE: PROCESSING OVERLAY ==================== */
91
+ .processing-overlay {
92
+ max-width: 100%;
93
+ max-height: 100%;
94
+ height: 100dvh;
95
+ /* Dynamic viewport height */
96
+ overflow: hidden;
97
+ }
98
+
99
+ @media (max-width: 768px) {
100
+ .processing-overlay {
101
+ padding: 20px;
102
+ }
103
+
104
+ .processing-title {
105
+ font-size: clamp(16px, 4vw, 18px) !important;
106
+ }
107
+
108
+ .processing-status {
109
+ font-size: 13px !important;
110
+ }
111
+
112
+ .processing-time {
113
+ font-size: 12px !important;
114
+ }
115
+ }
116
+
117
+ @media (max-height: 600px) {
118
+ .processing-overlay {
119
+ padding: 10px;
120
+ }
121
+
122
+ .processing-title {
123
+ font-size: 16px !important;
124
+ margin-bottom: 10px;
125
+ }
126
+
127
+ .processing-spinner {
128
+ width: 40px !important;
129
+ height: 40px !important;
130
+ }
131
+ }
132
+
133
+ /* ==================== PREVIEW AREA: IMAGE/VIDEO CONSTRAINTS ==================== */
134
+ .preview-area {
135
+ max-width: 100%;
136
+ /* Use dvh for mobile browser bars */
137
+ max-height: calc(100dvh - 250px);
138
+ overflow: hidden;
139
+ }
140
+
141
+ .preview-area img,
142
+ .preview-area video,
143
+ .preview-area canvas {
144
+ max-width: 100% !important;
145
+ max-height: calc(100dvh - 300px) !important;
146
+ object-fit: contain;
147
+ width: auto;
148
+ height: auto;
149
+ }
150
+
151
+ @media (max-width: 768px) {
152
+ .preview-area {
153
+ max-height: 400px;
154
+ min-height: auto;
155
+ }
156
+
157
+ .preview-area img,
158
+ .preview-area video,
159
+ .preview-area canvas {
160
+ max-height: 350px !important;
161
+ }
162
+ }
163
+
164
+ @media (max-height: 700px) {
165
+ .preview-area {
166
+ max-height: 300px;
167
+ }
168
+
169
+ .preview-area img,
170
+ .preview-area video,
171
+ .preview-area canvas {
172
+ max-height: 250px !important;
173
+ }
174
+ }
175
+
176
+ /* ==================== HEATMAP OVERLAY CONSTRAINTS ==================== */
177
+ .heatmap-overlay,
178
+ .heatmap-container {
179
+ max-width: 100%;
180
+ max-height: calc(100vh - 250px);
181
+ overflow: hidden;
182
+ }
183
+
184
+ .heatmap-overlay img,
185
+ .heatmap-container img {
186
+ max-width: 100% !important;
187
+ max-height: calc(100vh - 300px) !important;
188
+ object-fit: contain;
189
+ }
190
+
191
+ @media (max-width: 768px) {
192
+
193
+ .heatmap-overlay,
194
+ .heatmap-container {
195
+ max-height: 400px;
196
+ }
197
+
198
+ .heatmap-overlay img,
199
+ .heatmap-container img {
200
+ max-height: 350px !important;
201
+ }
202
+ }
203
+
204
+ /* ==================== VIDEO RESULT PAGE ==================== */
205
+ .video-preview-container {
206
+ max-width: 100%;
207
+ max-height: calc(100vh - 200px);
208
+ overflow: hidden;
209
+ }
210
+
211
+ .video-preview-container video {
212
+ max-width: 100% !important;
213
+ max-height: calc(100vh - 250px) !important;
214
+ width: auto;
215
+ height: auto;
216
+ }
217
+
218
+ @media (max-width: 768px) {
219
+ .video-preview-container {
220
+ max-height: 400px;
221
+ }
222
+
223
+ .video-preview-container video {
224
+ max-height: 350px !important;
225
+ }
226
+ }
227
+
228
+ /* ==================== BOTTOM ACTION PANELS / SHEETS ==================== */
229
+ .action-panel,
230
+ .bottom-sheet,
231
+ .queue-footer,
232
+ [class*="action"] {
233
+ position: relative;
234
+ bottom: auto;
235
+ max-width: 100%;
236
+ }
237
+
238
+ @media (max-width: 768px) {
239
+
240
+ .action-panel,
241
+ .bottom-sheet {
242
+ position: sticky;
243
+ bottom: 0;
244
+ left: 0;
245
+ right: 0;
246
+ padding: 12px 16px;
247
+ max-height: 30vh;
248
+ overflow-y: auto;
249
+ }
250
+
251
+ .queue-footer {
252
+ flex-direction: column;
253
+ gap: 10px;
254
+ padding: 12px;
255
+ }
256
+
257
+ .queue-footer button {
258
+ width: 100% !important;
259
+ min-height: 48px;
260
+ }
261
+ }
262
+
263
+ /* ==================== FILE QUEUE MOBILE OPTIMIZATION ==================== */
264
+ @media (max-width: 768px) {
265
+ .file-queue-container {
266
+ max-height: calc(100vh - 400px);
267
+ overflow-y: auto;
268
+ }
269
+
270
+ .file-queue-item {
271
+ padding: 12px;
272
+ gap: 8px;
273
+ }
274
+
275
+ .queue-header {
276
+ flex-direction: column;
277
+ gap: 12px;
278
+ align-items: stretch;
279
+ }
280
+
281
+ .queue-title {
282
+ font-size: 16px;
283
+ }
284
+
285
+ .queue-actions {
286
+ display: flex;
287
+ gap: 8px;
288
+ width: 100%;
289
+ }
290
+
291
+ .queue-actions button {
292
+ flex: 1;
293
+ min-height: 44px;
294
+ font-size: 13px;
295
+ }
296
+ }
297
+
298
+ /* ==================== MODALS & OVERLAYS ==================== */
299
+ .modal-overlay,
300
+ .overlay {
301
+ max-width: 100vw;
302
+ max-height: 100vh;
303
+ overflow-y: auto;
304
+ }
305
+
306
+ .modal-container,
307
+ .modal-content {
308
+ max-width: calc(100vw - 40px);
309
+ max-height: calc(100vh - 40px);
310
+ margin: 20px auto;
311
+ overflow-y: auto;
312
+ }
313
+
314
+ @media (max-width: 768px) {
315
+
316
+ .modal-container,
317
+ .modal-content {
318
+ max-width: calc(100vw - 20px);
319
+ max-height: calc(100vh - 20px);
320
+ margin: 10px;
321
+ border-radius: 12px;
322
+ }
323
+
324
+ .modal-close,
325
+ .btn-close {
326
+ width: 44px;
327
+ height: 44px;
328
+ font-size: 24px;
329
+ }
330
+ }
331
+
332
+ /* ==================== CARDS EXCEEDING VIEWPORT HEIGHT ==================== */
333
+ .card,
334
+ .result-card,
335
+ .analysis-card,
336
+ .verdict-card,
337
+ .metric-card {
338
+ max-height: calc(100vh - 100px);
339
+ overflow-y: auto;
340
+ }
341
+
342
+ @media (max-width: 768px) {
343
+
344
+ .card,
345
+ .result-card,
346
+ .analysis-card {
347
+ max-height: none;
348
+ height: auto;
349
+ }
350
+
351
+ .verdict-card {
352
+ padding: 20px 16px;
353
+ }
354
+ }
355
+
356
+ /* ==================== HISTORY / FILTER PANEL HEIGHT FIXES ==================== */
357
+ .history-controls {
358
+ max-height: calc(100vh - 200px);
359
+ overflow-y: auto;
360
+ }
361
+
362
+ @media (max-width: 768px) {
363
+ .history-controls {
364
+ max-height: none;
365
+ height: auto;
366
+ overflow-y: visible;
367
+ }
368
+
369
+ .filter-controls {
370
+ flex-direction: column;
371
+ gap: 10px;
372
+ }
373
+
374
+ .filter-select {
375
+ width: 100%;
376
+ min-width: 100%;
377
+ }
378
+
379
+ .export-controls {
380
+ flex-direction: column;
381
+ gap: 10px;
382
+ width: 100%;
383
+ }
384
+
385
+ .btn-export,
386
+ .btn-clear-all {
387
+ width: 100%;
388
+ min-height: 48px;
389
+ }
390
+ }
391
+
392
+ /* ==================== HISTORY TABLE CONSTRAINTS ==================== */
393
+ @media (max-width: 768px) {
394
+ .history-table-container {
395
+ max-height: calc(100vh - 350px);
396
+ overflow-x: auto;
397
+ overflow-y: auto;
398
+ -webkit-overflow-scrolling: touch;
399
+ }
400
+ }
401
+
402
+ @media (max-width: 640px) {
403
+ .history-table {
404
+ min-width: 600px;
405
+ }
406
+
407
+ .table-filename {
408
+ max-width: 100px;
409
+ }
410
+ }
411
+
412
+ /* Floating buttons — only apply to actual UI floating buttons, not decorative */
413
+ @media (max-width: 768px) {
414
+
415
+ .floating-button,
416
+ .btn-floating {
417
+ position: fixed;
418
+ bottom: 20px !important;
419
+ right: 20px !important;
420
+ width: 56px;
421
+ height: 56px;
422
+ max-width: calc(100vw - 40px);
423
+ }
424
+ }
425
+
426
+ /* ==================== HERO SECTION SHORT SCREENS ==================== */
427
+ @media (max-height: 700px) {
428
+ .hero {
429
+ min-height: auto;
430
+ padding: 80px 0 40px;
431
+ }
432
+
433
+ .hero-title {
434
+ font-size: 40px;
435
+ margin-bottom: 15px;
436
+ }
437
+
438
+ .hero-description {
439
+ font-size: 15px;
440
+ margin-bottom: 20px;
441
+ }
442
+
443
+ .hero-stats {
444
+ margin-top: 20px;
445
+ gap: 15px;
446
+ }
447
+
448
+ .hero-actions {
449
+ gap: 10px;
450
+ margin-top: 20px;
451
+ }
452
+ }
453
+
454
+ /* ==================== ULTRA-WIDE SCREENS (2560px+) ==================== */
455
+ @media (min-width: 2560px) {
456
+ .container {
457
+ max-width: 1800px;
458
+ }
459
+
460
+ .analysis-container {
461
+ max-width: 1600px;
462
+ }
463
+
464
+ .hero-content {
465
+ max-width: 60%;
466
+ }
467
+ }
468
+
469
+ /* ==================== EXTREME ASPECT RATIOS (21:9, 32:9) ==================== */
470
+ @media (min-aspect-ratio: 21/9) {
471
+ .hero-content {
472
+ max-width: 55%;
473
+ }
474
+
475
+ .analysis-grid {
476
+ gap: 60px;
477
+ }
478
+ }
479
+
480
+ /* ==================== LANDSCAPE MOBILE (SHORT & WIDE) ==================== */
481
+ @media (max-height: 500px) and (orientation: landscape) {
482
+ .navbar {
483
+ padding: 6px 0;
484
+ }
485
+
486
+ .analysis-container {
487
+ padding-top: 70px;
488
+ }
489
+
490
+ .hero {
491
+ min-height: auto;
492
+ padding: 50px 0 30px;
493
+ }
494
+
495
+ .hero-title {
496
+ font-size: 28px;
497
+ }
498
+
499
+ .section-title {
500
+ font-size: 24px;
501
+ }
502
+
503
+ .upload-area {
504
+ min-height: 180px;
505
+ }
506
+
507
+ .preview-area {
508
+ max-height: 250px;
509
+ }
510
+
511
+ /* Force single column even in landscape if screen is too short */
512
+ .analysis-grid {
513
+ grid-template-columns: 1fr;
514
+ }
515
+
516
+ .statistics-grid {
517
+ grid-template-columns: repeat(2, 1fr);
518
+ }
519
+ }
520
+
521
+ /* ==================== VERY TALL MOBILE SCREENS ==================== */
522
+ @media (min-height: 900px) and (max-width: 480px) {
523
+
524
+ /* Optimize for very tall phones */
525
+ .upload-area {
526
+ min-height: 450px;
527
+ }
528
+
529
+ .preview-area {
530
+ max-height: 500px;
531
+ }
532
+
533
+ .section {
534
+ padding: 80px 0;
535
+ }
536
+ }
537
+
538
+ /* ==================== PWA STANDALONE MODE ==================== */
539
+ @media (display-mode: standalone) {
540
+ body {
541
+ overscroll-behavior-y: contain;
542
+ }
543
+
544
+ .navbar {
545
+ padding-bottom: max(12px, env(safe-area-inset-bottom));
546
+ }
547
+
548
+ .footer {
549
+ padding-bottom: max(40px, calc(40px + env(safe-area-inset-bottom)));
550
+ }
551
+
552
+ /* Prevent content from hiding behind home indicator */
553
+ .action-panel,
554
+ .bottom-sheet,
555
+ .queue-footer {
556
+ padding-bottom: max(12px, calc(12px + env(safe-area-inset-bottom)));
557
+ }
558
+ }
559
+
560
+ /* ==================== EMPTY STATES ==================== */
561
+ .empty-state {
562
+ max-width: 100%;
563
+ padding: 40px 20px;
564
+ }
565
+
566
+ @media (max-width: 768px) {
567
+ .empty-state {
568
+ padding: 30px 16px;
569
+ }
570
+
571
+ .empty-icon {
572
+ font-size: 40px;
573
+ }
574
+
575
+ .empty-state h3 {
576
+ font-size: 18px;
577
+ }
578
+
579
+ .empty-state p {
580
+ font-size: 14px;
581
+ }
582
+ }
583
+
584
+ /* ==================== GRID LAYOUTS MOBILE OPTIMIZATION ==================== */
585
+ @media (max-width: 768px) {
586
+
587
+ .tech-grid,
588
+ .showcase-grid,
589
+ .capabilities-grid {
590
+ grid-template-columns: 1fr;
591
+ gap: 16px;
592
+ }
593
+
594
+ .model-stats-grid {
595
+ grid-template-columns: repeat(2, 1fr);
596
+ }
597
+
598
+ .recent-grid {
599
+ grid-template-columns: 1fr;
600
+ }
601
+
602
+ .pipeline {
603
+ flex-direction: column;
604
+ gap: 20px;
605
+ }
606
+
607
+ /* RESPONSIVE: Hide pipeline arrows on mobile since steps stack */
608
+ .pipeline-arrow {
609
+ display: none;
610
+ }
611
+ }
612
+
613
+ /* ==================== TOUCH TARGET SIZES (WCAG AAA) — Mobile Only ==================== */
614
+ @media (max-width: 768px) {
615
+
616
+ button,
617
+ .btn,
618
+ .btn-primary,
619
+ .btn-secondary,
620
+ .btn-hero-primary,
621
+ .btn-hero-secondary,
622
+ .btn-upload,
623
+ .btn-export,
624
+ .btn-toggle,
625
+ .btn-page,
626
+ .btn-batch,
627
+ .control-btn,
628
+ [role="button"] {
629
+ min-height: 44px;
630
+ min-width: 44px;
631
+ }
632
+
633
+ /* Checkboxes and radios need larger hit area */
634
+ [type="checkbox"],
635
+ [type="radio"] {
636
+ min-height: 24px;
637
+ min-width: 24px;
638
+ }
639
+ }
640
+
641
+ /* ==================== PREVENT iOS ZOOM ON INPUT FOCUS ==================== */
642
+ @media (max-width: 768px) {
643
+
644
+ input,
645
+ select,
646
+ textarea {
647
+ font-size: 16px !important;
648
+ }
649
+ }
650
+
651
+ /* ==================== BETTER TAP HIGHLIGHTING ==================== */
652
+ * {
653
+ -webkit-tap-highlight-color: rgba(227, 245, 20, 0.15);
654
+ }
655
+
656
+ /* ==================== SAFE SCROLLING ==================== */
657
+ .scrollable {
658
+ -webkit-overflow-scrolling: touch;
659
+ }
660
+
661
+ /* ==================== ENSURE BUTTONS DONT WRAP TEXT ==================== */
662
+ @media (max-width: 480px) {
663
+
664
+ .btn-primary,
665
+ .btn-secondary,
666
+ .btn-hero-primary,
667
+ .btn-hero-secondary {
668
+ font-size: 14px;
669
+ padding: 12px 20px;
670
+ white-space: nowrap;
671
+ overflow: hidden;
672
+ text-overflow: ellipsis;
673
+ }
674
+ }
675
+
676
+ /* ==================== STATISTICS SECTION MOBILE ==================== */
677
+ @media (max-width: 480px) {
678
+ .statistics-grid {
679
+ grid-template-columns: 1fr !important;
680
+ gap: 12px;
681
+ }
682
+
683
+ .stat-card {
684
+ padding: 16px;
685
+ }
686
+
687
+ .stat-value {
688
+ font-size: 24px;
689
+ }
690
+
691
+ .stat-label {
692
+ font-size: 12px;
693
+ }
694
+ }
695
+
696
+ /* ==================== CONTENT PADDING TOP/BOTTOM FIXES ==================== */
697
+ @media (max-width: 768px) {
698
+ section {
699
+ padding: 50px 0;
700
+ }
701
+
702
+ .analysis-container,
703
+ .history-container {
704
+ padding-top: 100px;
705
+ padding-bottom: 40px;
706
+ }
707
+ }
708
+
709
+ @media (max-width: 480px) {
710
+ section {
711
+ padding: 40px 0;
712
+ }
713
+
714
+ .analysis-container,
715
+ .history-container {
716
+ padding-top: 90px;
717
+ padding-bottom: 30px;
718
+ }
719
+ }
720
+
721
+ /* ==================== VIDEO PLAYER RESPONSIVE ==================== */
722
+ .video-container,
723
+ .video-window-container,
724
+ .video-content-wrapper {
725
+ max-width: 100%;
726
+ overflow: hidden;
727
+ }
728
+
729
+ .video-container video,
730
+ .demo-video {
731
+ max-width: 100%;
732
+ height: auto;
733
+ }
734
+
735
+ @media (max-width: 768px) {
736
+ .img-comp-container {
737
+ height: 300px !important;
738
+ }
739
+ }
740
+
741
+ @media (max-width: 480px) {
742
+ .img-comp-container {
743
+ height: 250px !important;
744
+ }
745
+ }
746
+
747
+ /* ==================== ORBIT/FEATURES SECTION ==================== */
748
+ @media (max-width: 768px) {
749
+ .orbit-container {
750
+ min-height: 400px;
751
+ overflow: hidden;
752
+ }
753
+
754
+ .orbit-stage {
755
+ transform: scale(0.7);
756
+ }
757
+
758
+ .orbit-info-card {
759
+ max-width: 90%;
760
+ padding: 16px;
761
+ }
762
+ }
763
+
764
+ @media (max-width: 480px) {
765
+ .orbit-stage {
766
+ transform: scale(0.5);
767
+ }
768
+ }
769
+
770
+ /* ==================== EXTENSION SECTION ==================== */
771
+ @media (max-width: 768px) {
772
+ .extension-container {
773
+ flex-direction: column;
774
+ }
775
+
776
+ .extension-content,
777
+ .extension-visual {
778
+ width: 100%;
779
+ }
780
+ }
781
+
782
+ /* ==================== OVERFLOW PREVENTION ==================== */
783
+ /* Applied to specific layout containers only — not globally */
784
+ .container,
785
+ .analysis-grid,
786
+ .features-grid,
787
+ .tech-grid,
788
+ .pipeline-grid {
789
+ max-width: 100%;
790
+ overflow-x: hidden;
791
+ }
792
+
793
+ /* ==================== BATCH ACTIONS BAR MOBILE ==================== */
794
+ @media (max-width: 768px) {
795
+ .batch-actions-bar {
796
+ width: 90%;
797
+ max-width: 400px;
798
+ bottom: 20px;
799
+ padding: 12px 16px;
800
+ flex-direction: column;
801
+ gap: 10px;
802
+ }
803
+
804
+ .btn-batch {
805
+ width: 100%;
806
+ min-height: 44px;
807
+ }
808
+ }
809
+
810
+ /* ==================== PAGINATION MOBILE ==================== */
811
+ @media (max-width: 768px) {
812
+ .pagination {
813
+ gap: 8px;
814
+ padding-bottom: 40px;
815
+ }
816
+
817
+ .btn-page {
818
+ width: 40px;
819
+ height: 40px;
820
+ font-size: 14px;
821
+ }
822
+
823
+ .page-info {
824
+ font-size: 13px;
825
+ }
826
+ }
827
+
828
+ /* ==================== FIX ANY FIXED POSITIONING OVERFLOW ==================== */
829
+ @media (max-width: 768px) {
830
+
831
+ [style*="position: fixed"],
832
+ [style*="position:fixed"] {
833
+ max-width: 100vw;
834
+ }
835
+ }
836
+
837
+ /* ==================== LANDSCAPE TABLET ==================== */
838
+ @media (min-width: 769px) and (max-width: 1024px) and (orientation: landscape) {
839
+ .analysis-grid {
840
+ grid-template-columns: 1fr 1fr;
841
+ gap: 30px;
842
+ }
843
+
844
+ .upload-area {
845
+ min-height: 350px;
846
+ }
847
+
848
+ .preview-area {
849
+ max-height: 400px;
850
+ }
851
+ }
852
+
853
+ /* ==================== PORTRAIT TABLET ==================== */
854
+ @media (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) {
855
+ .analysis-grid {
856
+ grid-template-columns: 1fr;
857
+ gap: 30px;
858
+ }
859
+
860
+ .container {
861
+ padding: 0 30px;
862
+ }
863
+ }
864
+
865
+ /* ==================== ENSURE TEXT DOESNT OVERFLOW ==================== */
866
+ h1,
867
+ h2,
868
+ h3,
869
+ h4,
870
+ h5,
871
+ h6,
872
+ p,
873
+ span,
874
+ div {
875
+ word-wrap: break-word;
876
+ overflow-wrap: break-word;
877
+ }
878
+
879
+ /* ==================== FINAL SAFETY NET ==================== */
880
+ @media (max-width: 768px) {
881
+
882
+ /* Ensure absolutely nothing causes horizontal scroll */
883
+ * {
884
+ max-width: 100vw !important;
885
+ }
886
+
887
+ /* Exception for background elements */
888
+ .mesh-background,
889
+ #particles-js,
890
+ #canvas-container,
891
+ .noise-overlay,
892
+ .gradient-orb,
893
+ .floating-3d-object,
894
+ body,
895
+ html {
896
+ max-width: none !important;
897
+ }
898
+ }
frontend/responsive-pages.css ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================================================================================
2
+ RESPONSIVE STYLES FOR NON-LANDING PAGES
3
+ ==================================================================================
4
+ This file contains responsive CSS for: analysis.html, history.html, video_result.html
5
+ DO NOT link this file in index.html - landing page should keep its original design
6
+ ================================================================================== */
7
+
8
+ /* ==================== FLUID TYPOGRAPHY VARIABLES ==================== */
9
+ :root {
10
+ /* Fluid font sizes for inner pages */
11
+ --font-size-page-title: clamp(1.75rem, 4vw + 0.5rem, 3rem);
12
+ --font-size-section-header: clamp(1.25rem, 2.5vw + 0.5rem, 2rem);
13
+ --font-size-card-title: clamp(1rem, 1.5vw + 0.5rem, 1.375rem);
14
+ --font-size-body: clamp(0.875rem, 1vw + 0.5rem, 1.125rem);
15
+ --font-size-small: clamp(0.75rem, 0.5vw + 0.5rem, 0.875rem);
16
+
17
+ /* Fluid spacing */
18
+ --container-padding: clamp(16px, 4vw, 40px);
19
+ --card-padding: clamp(16px, 3vw, 32px);
20
+ --gap-sm: clamp(12px, 2vw, 20px);
21
+ --gap-md: clamp(20px, 3vw, 40px);
22
+ --gap-lg: clamp(30px, 5vw, 60px);
23
+ }
24
+
25
+ /* ==================== ANALYSIS PAGE RESPONSIVE ==================== */
26
+
27
+ /* Analysis grid - stack on tablet and below */
28
+ .analysis-grid {
29
+ gap: var(--gap-md);
30
+ }
31
+
32
+ @media (max-width: 1024px) {
33
+ .analysis-grid {
34
+ grid-template-columns: 1fr !important;
35
+ gap: var(--gap-md);
36
+ }
37
+ }
38
+
39
+ /* Upload section responsive */
40
+ .upload-section {
41
+ gap: var(--gap-sm);
42
+ }
43
+
44
+ .section-header-small h2 {
45
+ font-size: var(--font-size-section-header);
46
+ }
47
+
48
+ /* Upload area fluid sizing */
49
+ .upload-area {
50
+ min-height: clamp(250px, 35vh, 400px);
51
+ padding: var(--card-padding);
52
+ }
53
+
54
+ @media (max-width: 768px) {
55
+ .upload-area {
56
+ min-height: 220px;
57
+ border-radius: 16px;
58
+ }
59
+
60
+ .upload-icon {
61
+ font-size: 48px;
62
+ }
63
+
64
+ .upload-text {
65
+ font-size: 14px;
66
+ }
67
+ }
68
+
69
+ /* Results section responsive */
70
+ .results-section {
71
+ padding: var(--card-padding);
72
+ }
73
+
74
+ @media (max-width: 768px) {
75
+ .results-section {
76
+ border-radius: 16px;
77
+ }
78
+ }
79
+
80
+ /* Statistics grid responsive */
81
+ .statistics-grid {
82
+ gap: var(--gap-sm);
83
+ }
84
+
85
+ @media (max-width: 768px) {
86
+ .statistics-grid {
87
+ grid-template-columns: repeat(2, 1fr) !important;
88
+ }
89
+ }
90
+
91
+ @media (max-width: 480px) {
92
+ .statistics-grid {
93
+ grid-template-columns: 1fr !important;
94
+ }
95
+
96
+ .stat-card {
97
+ padding: 16px;
98
+ }
99
+
100
+ .stat-value {
101
+ font-size: 24px;
102
+ }
103
+ }
104
+
105
+ /* Recent analyses grid responsive */
106
+ .recent-grid {
107
+ gap: var(--gap-sm);
108
+ }
109
+
110
+ @media (max-width: 768px) {
111
+ .recent-grid {
112
+ grid-template-columns: 1fr !important;
113
+ }
114
+ }
115
+
116
+ /* ==================== HISTORY PAGE RESPONSIVE ==================== */
117
+
118
+ /* History controls fluid spacing */
119
+ .history-controls {
120
+ gap: var(--gap-sm);
121
+ padding: var(--card-padding);
122
+ }
123
+
124
+ @media (max-width: 768px) {
125
+ .history-controls {
126
+ flex-direction: column;
127
+ align-items: stretch;
128
+ }
129
+
130
+ .search-container {
131
+ min-width: 100%;
132
+ }
133
+
134
+ .filter-controls {
135
+ flex-direction: column;
136
+ width: 100%;
137
+ }
138
+
139
+ .filter-select {
140
+ width: 100%;
141
+ }
142
+
143
+ .export-controls {
144
+ flex-direction: column;
145
+ width: 100%;
146
+ }
147
+
148
+ .btn-export,
149
+ .btn-clear-all {
150
+ width: 100%;
151
+ min-height: 48px;
152
+ }
153
+ }
154
+
155
+ /* History table horizontal scroll on mobile */
156
+ .history-table-container {
157
+ overflow-x: auto;
158
+ -webkit-overflow-scrolling: touch;
159
+ }
160
+
161
+ @media (max-width: 768px) {
162
+ .history-table {
163
+ min-width: 600px;
164
+ }
165
+ }
166
+
167
+ /* Grid view card sizing */
168
+ @media (max-width: 768px) {
169
+ .history-grid {
170
+ grid-template-columns: 1fr !important;
171
+ gap: var(--gap-sm);
172
+ }
173
+ }
174
+
175
+ /* Batch actions bar mobile */
176
+ @media (max-width: 768px) {
177
+ .batch-actions {
178
+ flex-wrap: wrap;
179
+ gap: 10px;
180
+ padding: 12px;
181
+ }
182
+
183
+ .batch-actions button {
184
+ flex: 1;
185
+ min-width: calc(50% - 10px);
186
+ min-height: 44px;
187
+ }
188
+ }
189
+
190
+ /* ==================== VIDEO RESULT PAGE RESPONSIVE ==================== */
191
+
192
+ /* Video player container responsive */
193
+ .video-preview-container {
194
+ max-width: 100%;
195
+ }
196
+
197
+ @media (max-width: 768px) {
198
+ .video-window-container {
199
+ border-radius: 16px;
200
+ }
201
+
202
+ .window-header {
203
+ padding: 10px 12px;
204
+ }
205
+
206
+ .play-button {
207
+ width: 60px;
208
+ height: 60px;
209
+ font-size: 28px;
210
+ }
211
+ }
212
+
213
+ @media (max-width: 480px) {
214
+ .play-button {
215
+ width: 50px;
216
+ height: 50px;
217
+ font-size: 24px;
218
+ }
219
+ }
220
+
221
+ /* Dashboard grid stack on mobile */
222
+ .dashboard-grid {
223
+ gap: var(--gap-md);
224
+ }
225
+
226
+ @media (max-width: 1024px) {
227
+ .dashboard-grid {
228
+ grid-template-columns: 1fr !important;
229
+ }
230
+ }
231
+
232
+ /* Stats grid compact on mobile */
233
+ @media (max-width: 768px) {
234
+ .video-stats-grid {
235
+ grid-template-columns: repeat(2, 1fr) !important;
236
+ gap: var(--gap-sm);
237
+ }
238
+ }
239
+
240
+ @media (max-width: 480px) {
241
+ .video-stats-grid {
242
+ grid-template-columns: 1fr !important;
243
+ }
244
+ }
245
+
246
+ /* Timeline chart container responsive */
247
+ .timeline-container,
248
+ .chart-container {
249
+ max-width: 100%;
250
+ overflow-x: auto;
251
+ -webkit-overflow-scrolling: touch;
252
+ }
253
+
254
+ /* Frame grid responsive */
255
+ @media (max-width: 768px) {
256
+ .frame-grid {
257
+ grid-template-columns: repeat(2, 1fr) !important;
258
+ gap: var(--gap-sm);
259
+ }
260
+ }
261
+
262
+ @media (max-width: 480px) {
263
+ .frame-grid {
264
+ grid-template-columns: 1fr !important;
265
+ }
266
+ }
267
+
268
+ /* Report section mobile */
269
+ @media (max-width: 768px) {
270
+ .report-section {
271
+ padding: var(--card-padding);
272
+ }
273
+
274
+ .report-header {
275
+ flex-direction: column;
276
+ gap: 12px;
277
+ align-items: flex-start;
278
+ }
279
+
280
+ .btn-download-report {
281
+ width: 100%;
282
+ min-height: 48px;
283
+ }
284
+ }
285
+
286
+ /* ==================== SHARED RESPONSIVE PATTERNS ==================== */
287
+
288
+ /* Container padding */
289
+ .analysis-container,
290
+ .history-container,
291
+ .video-result-container {
292
+ padding-left: var(--container-padding);
293
+ padding-right: var(--container-padding);
294
+ }
295
+
296
+ /* Card styling consistency */
297
+ .result-card,
298
+ .analysis-card,
299
+ .history-card,
300
+ .video-card {
301
+ padding: var(--card-padding);
302
+ }
303
+
304
+ @media (max-width: 768px) {
305
+
306
+ .result-card,
307
+ .analysis-card,
308
+ .history-card,
309
+ .video-card {
310
+ border-radius: 16px;
311
+ }
312
+ }
313
+
314
+ /* Touch targets for interactive elements */
315
+ @media (max-width: 768px) {
316
+
317
+ button,
318
+ .btn,
319
+ .btn-primary,
320
+ .btn-secondary,
321
+ [role="button"] {
322
+ min-height: 44px;
323
+ min-width: 44px;
324
+ }
325
+ }
326
+
327
+ /* Prevent iOS input zoom */
328
+ @media (max-width: 768px) {
329
+
330
+ input,
331
+ select,
332
+ textarea {
333
+ font-size: 16px !important;
334
+ }
335
+ }
336
+
337
+ /* Modal responsive */
338
+ @media (max-width: 768px) {
339
+ .modal-content {
340
+ max-width: calc(100vw - 32px);
341
+ max-height: calc(100vh - 32px);
342
+ margin: 16px;
343
+ border-radius: 16px;
344
+ }
345
+
346
+ .modal-close {
347
+ width: 44px;
348
+ height: 44px;
349
+ }
350
+ }
frontend/script.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/scroll_indicator.css ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==================== SCROLL INDICATOR ==================== */
2
+ .scroll-indicator {
3
+ position: absolute;
4
+ bottom: 30px;
5
+ left: 50%;
6
+ transform: translateX(-50%);
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ gap: 8px;
11
+ z-index: 10;
12
+ opacity: 0.7;
13
+ transition: opacity 0.3s ease;
14
+ cursor: pointer;
15
+ }
16
+
17
+ .scroll-indicator:hover {
18
+ opacity: 1;
19
+ }
20
+
21
+ .mouse {
22
+ width: 26px;
23
+ height: 42px;
24
+ border: 2px solid rgba(255, 255, 255, 0.4);
25
+ border-radius: 20px;
26
+ position: relative;
27
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
28
+ }
29
+
30
+ .wheel {
31
+ width: 4px;
32
+ height: 8px;
33
+ background: var(--accent-yellow, #E3F514);
34
+ border-radius: 2px;
35
+ position: absolute;
36
+ top: 6px;
37
+ left: 50%;
38
+ transform: translateX(-50%);
39
+ animation: scrollWheel 2s ease-in-out infinite;
40
+ }
41
+
42
+ .arrow-scroll {
43
+ width: 10px;
44
+ height: 10px;
45
+ border-right: 2px solid rgba(255, 255, 255, 0.4);
46
+ border-bottom: 2px solid rgba(255, 255, 255, 0.4);
47
+ transform: rotate(45deg);
48
+ animation: scrollArrow 2s ease-in-out infinite;
49
+ animation-delay: 0.2s;
50
+ }
51
+
52
+ @keyframes scrollWheel {
53
+ 0% {
54
+ top: 6px;
55
+ opacity: 1;
56
+ height: 8px;
57
+ }
58
+
59
+ 100% {
60
+ top: 24px;
61
+ opacity: 0;
62
+ height: 4px;
63
+ }
64
+ }
65
+
66
+ @keyframes scrollArrow {
67
+ 0% {
68
+ transform: rotate(45deg) translate(0, 0);
69
+ opacity: 0;
70
+ }
71
+
72
+ 50% {
73
+ opacity: 1;
74
+ }
75
+
76
+ 100% {
77
+ transform: rotate(45deg) translate(2px, 2px);
78
+ opacity: 0;
79
+ }
80
+ }
81
+
82
+ /* Reduced Motion */
83
+ @media (prefers-reduced-motion: reduce) {
84
+
85
+ .wheel,
86
+ .arrow-scroll {
87
+ animation: none;
88
+ }
89
+ }
90
+
91
+ /* Hide on small screens where height is limited */
92
+ @media (max-height: 700px) {
93
+ .scroll-indicator {
94
+ display: none;
95
+ }
96
+ }
frontend/service-worker.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * DeepGuard Service Worker
3
+ * Handles offline functionality, caching, and PWA features
4
+ */
5
+
6
+ const CACHE_VERSION = 'v1.0.0';
7
+ const CACHE_NAME = `deepguard-${CACHE_VERSION}`;
8
+
9
+ // Assets to cache immediately on install
10
+ const STATIC_ASSETS = [
11
+ '/',
12
+ '/index.html',
13
+ '/analysis.html',
14
+ '/history.html',
15
+ '/offline.html',
16
+ '/style.css',
17
+ '/animations.css',
18
+ '/history.css',
19
+ '/loader.css',
20
+ '/orbit.css',
21
+ '/showcase.css',
22
+ '/video_player.css',
23
+ '/extension.css',
24
+ '/scroll_indicator.css',
25
+ '/script.js',
26
+ '/mobile.js',
27
+ '/loader.js',
28
+ '/hero_reveal.js',
29
+ '/motion.js',
30
+ '/orbit_interaction.js',
31
+ '/three_bg.js',
32
+ '/logo.ico',
33
+ '/icon-192.png',
34
+ '/icon-512.png',
35
+ '/manifest.json'
36
+ ];
37
+
38
+ // Assets that can be cached on demand
39
+ const RUNTIME_CACHE = 'deepguard-runtime';
40
+
41
+ // Install event - cache static assets
42
+ self.addEventListener('install', (event) => {
43
+ console.log('[Service Worker] Installing...');
44
+
45
+ event.waitUntil(
46
+ caches.open(CACHE_NAME)
47
+ .then((cache) => {
48
+ console.log('[Service Worker] Caching static assets');
49
+ return cache.addAll(STATIC_ASSETS);
50
+ })
51
+ .then(() => {
52
+ console.log('[Service Worker] Installation complete');
53
+ return self.skipWaiting(); // Activate immediately
54
+ })
55
+ .catch((error) => {
56
+ console.error('[Service Worker] Installation failed:', error);
57
+ })
58
+ );
59
+ });
60
+
61
+ // Activate event - clean up old caches
62
+ self.addEventListener('activate', (event) => {
63
+ console.log('[Service Worker] Activating...');
64
+
65
+ event.waitUntil(
66
+ caches.keys()
67
+ .then((cacheNames) => {
68
+ return Promise.all(
69
+ cacheNames
70
+ .filter((name) => name.startsWith('deepguard-') && name !== CACHE_NAME)
71
+ .map((name) => {
72
+ console.log('[Service Worker] Deleting old cache:', name);
73
+ return caches.delete(name);
74
+ })
75
+ );
76
+ })
77
+ .then(() => {
78
+ console.log('[Service Worker] Activation complete');
79
+ return self.clients.claim(); // Take control immediately
80
+ })
81
+ );
82
+ });
83
+
84
+ // Fetch event - serve from cache, fallback to network
85
+ self.addEventListener('fetch', (event) => {
86
+ const { request } = event;
87
+ const url = new URL(request.url);
88
+
89
+ // Skip cross-origin requests
90
+ if (url.origin !== location.origin) {
91
+ return;
92
+ }
93
+
94
+ // Skip API requests (let them go to network)
95
+ if (url.pathname.startsWith('/api/')) {
96
+ return;
97
+ }
98
+
99
+ event.respondWith(
100
+ caches.match(request)
101
+ .then((cachedResponse) => {
102
+ if (cachedResponse) {
103
+ console.log('[Service Worker] Serving from cache:', request.url);
104
+ return cachedResponse;
105
+ }
106
+
107
+ // Not in cache, fetch from network
108
+ return fetch(request)
109
+ .then((response) => {
110
+ // Don't cache non-successful responses
111
+ if (!response || response.status !== 200 || response.type !== 'basic') {
112
+ return response;
113
+ }
114
+
115
+ // Clone response for caching
116
+ const responseToCache = response.clone();
117
+
118
+ // Cache runtime assets
119
+ caches.open(RUNTIME_CACHE)
120
+ .then((cache) => {
121
+ cache.put(request, responseToCache);
122
+ });
123
+
124
+ return response;
125
+ })
126
+ .catch((error) => {
127
+ console.error('[Service Worker] Fetch failed:', error);
128
+
129
+ // Return offline page for navigation requests
130
+ if (request.mode === 'navigate') {
131
+ return caches.match('/offline.html');
132
+ }
133
+
134
+ // Return fallback for images
135
+ if (request.destination === 'image') {
136
+ return caches.match('/logo.svg');
137
+ }
138
+
139
+ return new Response('Offline - content not available', {
140
+ status: 503,
141
+ statusText: 'Service Unavailable',
142
+ headers: new Headers({
143
+ 'Content-Type': 'text/plain'
144
+ })
145
+ });
146
+ });
147
+ })
148
+ );
149
+ });
150
+
151
+ // Background sync for offline analysis (future enhancement)
152
+ self.addEventListener('sync', (event) => {
153
+ console.log('[Service Worker] Background sync:', event.tag);
154
+
155
+ if (event.tag === 'sync-analyses') {
156
+ event.waitUntil(syncPendingAnalyses());
157
+ }
158
+ });
159
+
160
+ // Push notifications (future enhancement)
161
+ self.addEventListener('push', (event) => {
162
+ console.log('[Service Worker] Push notification received');
163
+
164
+ const data = event.data ? event.data.json() : {};
165
+ const title = data.title || 'DeepGuard';
166
+ const options = {
167
+ body: data.body || 'Analysis complete',
168
+ icon: '/icon-192.png',
169
+ badge: '/icon-192.png',
170
+ vibrate: [200, 100, 200],
171
+ data: {
172
+ url: data.url || '/history.html'
173
+ }
174
+ };
175
+
176
+ event.waitUntil(
177
+ self.registration.showNotification(title, options)
178
+ );
179
+ });
180
+
181
+ // Notification click handler
182
+ self.addEventListener('notificationclick', (event) => {
183
+ console.log('[Service Worker] Notification clicked');
184
+ event.notification.close();
185
+
186
+ const urlToOpen = event.notification.data.url || '/';
187
+
188
+ event.waitUntil(
189
+ clients.matchAll({ type: 'window', includeUncontrolled: true })
190
+ .then((windowClients) => {
191
+ // Check if there's already a window open
192
+ for (let client of windowClients) {
193
+ if (client.url === urlToOpen && 'focus' in client) {
194
+ return client.focus();
195
+ }
196
+ }
197
+ // Open new window
198
+ if (clients.openWindow) {
199
+ return clients.openWindow(urlToOpen);
200
+ }
201
+ })
202
+ );
203
+ });
204
+
205
+ // Helper function for background sync
206
+ async function syncPendingAnalyses() {
207
+ // Future implementation: sync pending analyses to backend
208
+ console.log('[Service Worker] Syncing pending analyses...');
209
+ return Promise.resolve();
210
+ }
211
+
212
+ // Message handler for skip waiting
213
+ self.addEventListener('message', (event) => {
214
+ if (event.data && event.data.type === 'SKIP_WAITING') {
215
+ self.skipWaiting();
216
+ }
217
+ });
218
+
219
+ console.log('[Service Worker] Loaded successfully');
frontend/style.css ADDED
The diff for this file is too large to render. See raw diff
 
frontend/three_bg.js ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 3D Background with Three.js
2
+ // Theme: Dark space with "Nano Yellow" stars/particles
3
+
4
+ function initThreeBackground() {
5
+ const container = document.getElementById('canvas-container');
6
+ if (!container) return;
7
+
8
+ // PERFORMANCE OPTIMIZATION: Reduce count on mobile
9
+ const isMobile = window.innerWidth < 768;
10
+ const particleCount = isMobile ? 400 : 1200; // significantly fewer particles on mobile
11
+
12
+ // SCENE
13
+ const scene = new THREE.Scene();
14
+ scene.fog = new THREE.FogExp2(0x000000, 0.002);
15
+
16
+ // CAMERA
17
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
18
+ camera.position.z = 500;
19
+
20
+ // RENDERER - PERFORMANCE TUNING
21
+ const renderer = new THREE.WebGLRenderer({
22
+ alpha: true,
23
+ antialias: !isMobile, // Disable antialias on mobile for performance
24
+ powerPreference: "high-performance" // Hint to browser
25
+ });
26
+
27
+ // Cap pixel ratio to 2 to avoid 9x rendering on 3x screens
28
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
29
+ renderer.setSize(window.innerWidth, window.innerHeight);
30
+ renderer.setClearColor(0x000000, 0); // Transparent background
31
+ container.appendChild(renderer.domElement);
32
+
33
+ // Theme Check
34
+ const getThemeColors = () => {
35
+ const theme = localStorage.getItem('theme') || 'dark';
36
+ if (theme === 'light') {
37
+ return {
38
+ primary: 0x00AEEF, // Cyan
39
+ secondary: 0x0044CC, // Blue
40
+ gridPrimary: 0x00AEEF, // Cyan Grid
41
+ gridSecondary: 0xE0E0E0 // Light Grey Grid
42
+ };
43
+ }
44
+ return {
45
+ primary: 0xE3F514, // Nano Yellow
46
+ secondary: 0xFFFFFF, // White
47
+ gridPrimary: 0xE3F514, // Nano Yellow Grid
48
+ gridSecondary: 0x333333 // Dark Grey Grid
49
+ };
50
+ };
51
+
52
+ let themeColors = getThemeColors();
53
+
54
+ const geometry = new THREE.BufferGeometry();
55
+ const vertices = [];
56
+ const colors = [];
57
+
58
+ const color1 = new THREE.Color(themeColors.primary);
59
+ const color2 = new THREE.Color(themeColors.secondary);
60
+
61
+ for (let i = 0; i < particleCount; i++) {
62
+ // Random position
63
+ const x = (Math.random() - 0.5) * 2000;
64
+ const y = (Math.random() - 0.5) * 2000;
65
+ const z = (Math.random() - 0.5) * 2000;
66
+ vertices.push(x, y, z);
67
+
68
+ // Random color mix
69
+ const mixedColor = color1.clone().lerp(color2, Math.random() * 0.5);
70
+ colors.push(mixedColor.r, mixedColor.g, mixedColor.b);
71
+ }
72
+
73
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
74
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
75
+
76
+ const material = new THREE.PointsMaterial({
77
+ size: 2,
78
+ vertexColors: true,
79
+ transparent: true,
80
+ opacity: 0.8,
81
+ sizeAttenuation: true
82
+ });
83
+
84
+ const particles = new THREE.Points(geometry, material);
85
+ scene.add(particles);
86
+
87
+ // GEOMETRIC SHAPES (Floating low-poly meshes)
88
+ const shapeGroup = new THREE.Group();
89
+ scene.add(shapeGroup);
90
+
91
+ function addFloatingShape(type, x, y, z, size) {
92
+ let geometry;
93
+ if (type === 'icosahedron') geometry = new THREE.IcosahedronGeometry(size, 0);
94
+ else if (type === 'octahedron') geometry = new THREE.OctahedronGeometry(size, 0);
95
+
96
+ const material = new THREE.MeshBasicMaterial({
97
+ color: themeColors.primary,
98
+ wireframe: true,
99
+ transparent: true,
100
+ opacity: 0.15
101
+ });
102
+
103
+ const mesh = new THREE.Mesh(geometry, material);
104
+ mesh.position.set(x, y, z);
105
+ shapeGroup.add(mesh);
106
+ return mesh;
107
+ }
108
+
109
+ // Add a few floating shapes
110
+ const shapes = [];
111
+ shapes.push(addFloatingShape('icosahedron', -300, 100, -200, 60));
112
+ shapes.push(addFloatingShape('octahedron', 400, -150, -300, 80));
113
+ shapes.push(addFloatingShape('icosahedron', 0, 200, -400, 40));
114
+
115
+ // 3. INTERACTIVE 3D GRID FLOOR
116
+ const gridSize = 2000;
117
+ const gridDivisions = 40;
118
+ // Change const to let to allow reassignment
119
+ let gridHelper = new THREE.GridHelper(gridSize, gridDivisions, themeColors.gridPrimary, themeColors.gridSecondary);
120
+ gridHelper.position.y = -200; // Floor level
121
+ gridHelper.material.transparent = true;
122
+ gridHelper.material.opacity = 0.15;
123
+ scene.add(gridHelper);
124
+
125
+ // Watch for theme changes
126
+ // Store initial theme to avoid redundant updates on load
127
+ let currentTheme = localStorage.getItem('theme') || 'dark';
128
+
129
+ const observer = new MutationObserver((mutations) => {
130
+ mutations.forEach((mutation) => {
131
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
132
+ const newTheme = document.documentElement.getAttribute('data-theme');
133
+
134
+ // Prevent infinite loop relative to initial set or same-value updates
135
+ if (newTheme === currentTheme) return;
136
+ currentTheme = newTheme;
137
+
138
+ const isLight = newTheme === 'light';
139
+
140
+ const newPrim = new THREE.Color(isLight ? 0x00AEEF : 0xE3F514);
141
+ const newSec = new THREE.Color(isLight ? 0x0044CC : 0xFFFFFF);
142
+
143
+ // Update Particles
144
+ const newColors = [];
145
+ for (let i = 0; i < particleCount; i++) {
146
+ const mixedColor = newPrim.clone().lerp(newSec, Math.random() * 0.5);
147
+ newColors.push(mixedColor.r, mixedColor.g, mixedColor.b);
148
+ }
149
+ particles.geometry.setAttribute('color', new THREE.Float32BufferAttribute(newColors, 3));
150
+ particles.geometry.attributes.color.needsUpdate = true;
151
+
152
+ // Update Shapes
153
+ shapes.forEach(shape => {
154
+ shape.material.color.set(newPrim);
155
+ });
156
+
157
+ // Update Grid
158
+ scene.remove(gridHelper);
159
+ // Create new grid using standard ThreeJS helper for fixed geometry colors
160
+ gridHelper = new THREE.GridHelper(gridSize, gridDivisions, isLight ? 0x00AEEF : 0xE3F514, isLight ? 0xE0E0E0 : 0x333333);
161
+ gridHelper.position.y = -200;
162
+ gridHelper.material.transparent = true;
163
+ gridHelper.material.opacity = 0.15;
164
+ scene.add(gridHelper);
165
+ }
166
+ });
167
+ });
168
+
169
+ observer.observe(document.documentElement, { attributes: true });
170
+
171
+
172
+ // MOUSE INTERACTION
173
+ let mouseX = 0;
174
+ let mouseY = 0;
175
+ let targetX = 0;
176
+ let targetY = 0;
177
+
178
+ const windowHalfX = window.innerWidth / 2;
179
+ const windowHalfY = window.innerHeight / 2;
180
+
181
+ document.addEventListener('mousemove', (event) => {
182
+ // Optimize: use requestAnimationFrame for mouse updates if needed, but direct is usually fine for coordinates
183
+ mouseX = (event.clientX - windowHalfX);
184
+ mouseY = (event.clientY - windowHalfY);
185
+ });
186
+
187
+ // RESIZE HANDLER (THROTTLED)
188
+ let resizeTimeout;
189
+ window.addEventListener('resize', () => {
190
+ if (!resizeTimeout) {
191
+ resizeTimeout = setTimeout(() => {
192
+ camera.aspect = window.innerWidth / window.innerHeight;
193
+ camera.updateProjectionMatrix();
194
+ renderer.setSize(window.innerWidth, window.innerHeight);
195
+
196
+ // Update constraints for mouse calc
197
+ // windowHalfX = window.innerWidth / 2; // const variable can't be reassigned, generally okay not to update center exactly on resize for this effect
198
+
199
+ resizeTimeout = null;
200
+ }, 100);
201
+ }
202
+ });
203
+
204
+ // SCROLL INTERACTION
205
+ let scrollY = 0;
206
+ let targetScrollY = 0;
207
+
208
+ // Use passive listener for better scroll performance
209
+ document.addEventListener('scroll', () => {
210
+ scrollY = window.scrollY;
211
+ }, { passive: true });
212
+
213
+ // ANIMATION LOOP
214
+ function animate() {
215
+ requestAnimationFrame(animate);
216
+
217
+ // Smooth Scroll Interpolation
218
+ targetScrollY += (scrollY - targetScrollY) * 0.05;
219
+
220
+ // Mouse Parallax Calculation
221
+ targetX = mouseX * 0.001;
222
+ targetY = mouseY * 0.001;
223
+
224
+ // 1. Particle System Rotation
225
+ particles.rotation.y += 0.0005;
226
+ particles.rotation.x = targetScrollY * 0.0002;
227
+
228
+ // Mouse interaction for rotation
229
+ particles.rotation.y += 0.05 * (targetX - particles.rotation.y);
230
+ particles.rotation.x += 0.05 * (targetY - particles.rotation.x);
231
+
232
+ // 2. Camera Scroll Movement
233
+ const zoomFactor = targetScrollY * 0.1;
234
+ camera.position.z = 500 - zoomFactor; // Move closer
235
+ camera.position.y = -targetScrollY * 0.05; // Pan down subtly
236
+
237
+ // Stop zooming too close
238
+ if (camera.position.z < 100) camera.position.z = 100;
239
+
240
+ // 3. Floating Shapes Animation
241
+ for (let i = 0; i < shapes.length; i++) {
242
+ const shape = shapes[i];
243
+ shape.rotation.x += 0.002 * (i + 1);
244
+ shape.rotation.y += 0.002 * (i + 1);
245
+ shape.rotation.z = targetScrollY * 0.001 * (i % 2 === 0 ? 1 : -1);
246
+ }
247
+
248
+ // 4. Grid Animation (Infinite Scroll)
249
+ // Move grid towards camera (z-axis)
250
+ // Using modulo based on local vars to avoid Date.now() overhead if high precision distinctness isn't needed
251
+ // But Date.now() is fine.
252
+ gridHelper.position.z = (Date.now() * 0.05) % (gridSize / gridDivisions);
253
+ gridHelper.position.z += targetScrollY * 0.5;
254
+
255
+ const cell = gridSize / gridDivisions;
256
+ if (gridHelper.position.z > cell) gridHelper.position.z -= cell;
257
+
258
+ renderer.render(scene, camera);
259
+ }
260
+
261
+ animate();
262
+ }
263
+
264
+ // Initialize when DOM is loaded
265
+ document.addEventListener('DOMContentLoaded', initThreeBackground);
frontend/vercel.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "version": 2,
3
+ "cleanUrls": true,
4
+ "trailingSlash": false
5
+ }
frontend/video_player.css ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Video Window Styles */
2
+ .video-window-container {
3
+ background: #1e1e1e;
4
+ border-radius: 12px;
5
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
6
+ border: 1px solid rgba(255, 255, 255, 0.1);
7
+ overflow: hidden;
8
+ max-width: 1200px;
9
+ margin: 0 auto;
10
+ position: relative;
11
+ transform: translateZ(0);
12
+ /* Hardware accel */
13
+ }
14
+
15
+ .window-header {
16
+ background: #2d2d2d;
17
+ padding: 12px 16px;
18
+ display: flex;
19
+ align-items: center;
20
+ border-bottom: 1px solid rgba(0, 0, 0, 0.5);
21
+ position: relative;
22
+ }
23
+
24
+ .window-controls {
25
+ display: flex;
26
+ gap: 8px;
27
+ z-index: 2;
28
+ }
29
+
30
+ .control {
31
+ width: 12px;
32
+ height: 12px;
33
+ border-radius: 50%;
34
+ }
35
+
36
+ .control.red {
37
+ background: #ff5f56;
38
+ }
39
+
40
+ .control.yellow {
41
+ background: #ffbd2e;
42
+ }
43
+
44
+ .control.green {
45
+ background: #27c93f;
46
+ }
47
+
48
+ .window-title {
49
+ position: absolute;
50
+ width: 100%;
51
+ left: 0;
52
+ text-align: center;
53
+ color: #999;
54
+ font-size: 13px;
55
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
56
+ font-weight: 500;
57
+ }
58
+
59
+ .video-content-wrapper {
60
+ position: relative;
61
+ background: #000;
62
+ aspect-ratio: 16/9;
63
+ /* Enforce aspect ratio */
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ }
68
+
69
+ .demo-video {
70
+ width: 100%;
71
+ height: 100%;
72
+ object-fit: cover;
73
+ /* Or contain, depending on video */
74
+ display: block;
75
+ }
76
+
77
+ .play-overlay {
78
+ position: absolute;
79
+ top: 0;
80
+ left: 0;
81
+ right: 0;
82
+ bottom: 0;
83
+ background: rgba(0, 0, 0, 0.4);
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ cursor: pointer;
88
+ transition: opacity 0.3s ease;
89
+ z-index: 10;
90
+ }
91
+
92
+ .play-overlay.hidden {
93
+ opacity: 0;
94
+ pointer-events: none;
95
+ }
96
+
97
+ .play-button {
98
+ width: 80px;
99
+ height: 80px;
100
+ background: rgba(227, 245, 20, 0.9);
101
+ border-radius: 50%;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ color: #000;
106
+ font-size: 40px;
107
+ padding-left: 5px;
108
+ /* Visual center adjustment */
109
+ transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
110
+ box-shadow: 0 0 30px rgba(227, 245, 20, 0.4);
111
+ }
112
+
113
+ .play-overlay:hover .play-button {
114
+ transform: scale(1.1);
115
+ }
116
+
117
+ .video-progress-bar {
118
+ position: absolute;
119
+ bottom: 0;
120
+ left: 0;
121
+ width: 100%;
122
+ height: 3px;
123
+ background: rgba(255, 255, 255, 0.1);
124
+ }
125
+
126
+ .progress-fill {
127
+ height: 100%;
128
+ width: 0%;
129
+ background: var(--accent-yellow);
130
+ transition: width 0.1s linear;
131
+ }
132
+
133
+ /* ==================== VIDEO PLAYER RESPONSIVE ==================== */
134
+ @media (max-width: 768px) {
135
+ .video-window-container {
136
+ border-radius: 16px;
137
+ }
138
+
139
+ .window-header {
140
+ padding: 10px 12px;
141
+ }
142
+
143
+ .play-button {
144
+ width: 60px;
145
+ height: 60px;
146
+ font-size: 28px;
147
+ }
148
+
149
+ .window-title {
150
+ font-size: 11px;
151
+ }
152
+ }
153
+
154
+ @media (max-width: 480px) {
155
+ .play-button {
156
+ width: 50px;
157
+ height: 50px;
158
+ font-size: 24px;
159
+ }
160
+ }