0xarchit commited on
Commit
70b44e7
·
0 Parent(s):

Initial commit: NeuroScan Brain Tumor Analyzer

Browse files
Files changed (9) hide show
  1. .gitattributes +2 -0
  2. README.md +39 -0
  3. app.py +59 -0
  4. dockerfile +21 -0
  5. model/model.keras +3 -0
  6. requirements.txt +6 -0
  7. template/index.html +83 -0
  8. template/script.js +105 -0
  9. template/styles.css +337 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.h5 filter=lfs diff=lfs merge=lfs -text
2
+ *.keras filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: NeuroScan Brain Tumor Analyzer
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ # NeuroScan: Advanced Brain Tumor Analysis
12
+
13
+ This application uses a deep learning model to classify brain MRI images into four categories:
14
+ - Glioma
15
+ - Meningioma
16
+ - No Tumor
17
+ - Pituitary
18
+
19
+ ## Technology Stack
20
+ - **Backend**: FastAPI
21
+ - **Model**: TensorFlow/Keras CNN
22
+ - **Frontend**: Vanilla HTML/CSS/JS with Futuristic UI
23
+ - **Deployment**: Docker on Hugging Face Spaces (CPU Inference)
24
+
25
+ ## Local Setup
26
+ 1. Create a virtual environment:
27
+ ```bash
28
+ python -m venv .venv
29
+ source .venv/bin/activate
30
+ ```
31
+ 2. Install dependencies:
32
+ ```bash
33
+ pip install -r requirements.txt
34
+ ```
35
+ 3. Run the application:
36
+ ```bash
37
+ python app.py
38
+ ```
39
+ 4. Open `http://localhost:7860` in your browser.
app.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import numpy as np
4
+ from PIL import Image
5
+ from fastapi import FastAPI, File, UploadFile
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import HTMLResponse
9
+ import tensorflow as tf
10
+
11
+ app = FastAPI()
12
+
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"],
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ MODEL_PATH = "model/model.keras"
21
+ model = tf.keras.models.load_model(MODEL_PATH)
22
+ class_names = ['glioma', 'meningioma', 'notumor', 'pituitary']
23
+
24
+ from fastapi.concurrency import run_in_threadpool
25
+
26
+ def preprocess_image(image_bytes):
27
+ img = Image.open(io.BytesIO(image_bytes)).convert('RGB')
28
+ img = img.resize((224, 224))
29
+ img_array = np.array(img).astype('float32') / 255.0
30
+ img_array = np.expand_dims(img_array, axis=0)
31
+ return img_array
32
+
33
+ async def get_prediction(processed_img):
34
+ return await run_in_threadpool(model.predict, processed_img)
35
+
36
+ @app.post("/predict")
37
+ async def predict(file: UploadFile = File(...)):
38
+ contents = await file.read()
39
+ processed_img = preprocess_image(contents)
40
+ predictions = await get_prediction(processed_img)
41
+ pred_idx = np.argmax(predictions[0])
42
+ confidence = float(np.max(predictions[0]))
43
+
44
+ return {
45
+ "class": class_names[pred_idx],
46
+ "confidence": confidence,
47
+ "probabilities": {class_names[i]: float(predictions[0][i]) for i in range(len(class_names))}
48
+ }
49
+
50
+ @app.get("/")
51
+ async def read_index():
52
+ with open("template/index.html", "r") as f:
53
+ return HTMLResponse(content=f.read(), status_code=200)
54
+
55
+ app.mount("/static", StaticFiles(directory="template"), name="static")
56
+
57
+ if __name__ == "__main__":
58
+ import uvicorn
59
+ uvicorn.run(app, host="0.0.0.0", port=7860)
dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ RUN useradd -m -u 1000 user
15
+ USER user
16
+ ENV HOME=/home/user \
17
+ PATH=/home/user/.local/bin:$PATH
18
+
19
+ EXPOSE 7860
20
+
21
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
model/model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19c07897096a6a120e26c54018410405812501b90f31d48359abfdc2a169d580
3
+ size 1020597
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ tensorflow-cpu
4
+ python-multipart
5
+ Pillow
6
+ numpy
template/index.html ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NeuroScan | Advanced Brain Tumor Analysis</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/styles.css">
11
+ </head>
12
+ <body>
13
+ <div class="background-mesh"></div>
14
+ <div class="scan-line"></div>
15
+
16
+ <main class="container">
17
+ <header class="header">
18
+ <div class="logo">
19
+ <span class="logo-icon">◈</span>
20
+ <h1>NEURO<span class="accent">SCAN</span></h1>
21
+ </div>
22
+ <p class="subtitle">Clinical Grade MRI Classification System</p>
23
+ </header>
24
+
25
+ <section class="analysis-core">
26
+ <div class="upload-zone" id="dropZone">
27
+ <input type="file" id="fileInput" accept="image/*" hidden>
28
+ <div class="upload-content">
29
+ <div class="hex-icon">
30
+ <div class="hex-inner"></div>
31
+ </div>
32
+ <h3>Initialize Scan</h3>
33
+ <p>Drag MRI data or click to browse</p>
34
+ </div>
35
+ <div class="preview-container" id="previewContainer">
36
+ <img id="imagePreview" src="" alt="MRI Preview">
37
+ <div class="scanning-overlay" id="scanningOverlay">
38
+ <div class="scanner-bar"></div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="results-panel" id="resultsPanel">
44
+ <div class="status-indicator">
45
+ <span class="pulse"></span>
46
+ <span id="statusText">System Ready</span>
47
+ </div>
48
+
49
+ <div class="prediction-data" id="predictionData">
50
+ <div class="main-result">
51
+ <label>Diagnosis</label>
52
+ <h2 id="predictedClass">Awaiting Input</h2>
53
+ </div>
54
+ <div class="confidence-meter">
55
+ <div class="meter-header">
56
+ <span>Confidence Score</span>
57
+ <span id="confidenceValue">0%</span>
58
+ </div>
59
+ <div class="meter-bar">
60
+ <div class="meter-fill" id="confidenceFill"></div>
61
+ </div>
62
+ </div>
63
+ <div class="probability-breakdown" id="probBreakdown">
64
+ <!-- Probabilities will be injected here -->
65
+ </div>
66
+ </div>
67
+
68
+ <button class="reset-btn" id="resetBtn">New Analysis</button>
69
+ </div>
70
+ </section>
71
+
72
+ <footer class="footer">
73
+ <div class="system-stats">
74
+ <span>GPU_ACCEL: OFF</span>
75
+ <span>LATENCY: <span id="latencyValue">0ms</span></span>
76
+ <span>ENGINE: TENSORFLOW_2.x</span>
77
+ </div>
78
+ </footer>
79
+ </main>
80
+
81
+ <script src="/static/script.js"></script>
82
+ </body>
83
+ </html>
template/script.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const dropZone = document.getElementById('dropZone');
2
+ const fileInput = document.getElementById('fileInput');
3
+ const imagePreview = document.getElementById('imagePreview');
4
+ const previewContainer = document.getElementById('previewContainer');
5
+ const scanningOverlay = document.getElementById('scanningOverlay');
6
+ const resultsPanel = document.getElementById('resultsPanel');
7
+ const statusText = document.getElementById('statusText');
8
+ const predictedClass = document.getElementById('predictedClass');
9
+ const confidenceValue = document.getElementById('confidenceValue');
10
+ const confidenceFill = document.getElementById('confidenceFill');
11
+ const probBreakdown = document.getElementById('probBreakdown');
12
+ const resetBtn = document.getElementById('resetBtn');
13
+ const latencyValue = document.getElementById('latencyValue');
14
+
15
+ dropZone.addEventListener('click', () => fileInput.click());
16
+
17
+ dropZone.addEventListener('dragover', (e) => {
18
+ e.preventDefault();
19
+ dropZone.style.borderColor = 'var(--accent)';
20
+ });
21
+
22
+ dropZone.addEventListener('dragleave', () => {
23
+ dropZone.style.borderColor = 'var(--glass-border)';
24
+ });
25
+
26
+ dropZone.addEventListener('drop', (e) => {
27
+ e.preventDefault();
28
+ const file = e.dataTransfer.files[0];
29
+ if (file && file.type.startsWith('image/')) {
30
+ handleFile(file);
31
+ }
32
+ });
33
+
34
+ fileInput.addEventListener('change', (e) => {
35
+ const file = e.target.files[0];
36
+ if (file) handleFile(file);
37
+ });
38
+
39
+ function handleFile(file) {
40
+ const reader = new FileReader();
41
+ reader.onload = (e) => {
42
+ imagePreview.src = e.target.result;
43
+ previewContainer.style.display = 'block';
44
+ startAnalysis(file);
45
+ };
46
+ reader.readAsDataURL(file);
47
+ }
48
+
49
+ async function startAnalysis(file) {
50
+ statusText.innerText = 'Analyzing Neural Data...';
51
+ scanningOverlay.style.display = 'block';
52
+ const startTime = performance.now();
53
+
54
+ const formData = new FormData();
55
+ formData.append('file', file);
56
+
57
+ try {
58
+ const response = await fetch('/predict', {
59
+ method: 'POST',
60
+ body: formData
61
+ });
62
+
63
+ const data = await response.json();
64
+ const endTime = performance.now();
65
+ latencyValue.innerText = `${Math.round(endTime - startTime)}ms`;
66
+
67
+ displayResults(data);
68
+ } catch (error) {
69
+ statusText.innerText = 'Analysis Failed';
70
+ console.error(error);
71
+ } finally {
72
+ scanningOverlay.style.display = 'none';
73
+ }
74
+ }
75
+
76
+ function displayResults(data) {
77
+ statusText.innerText = 'Scan Complete';
78
+ predictedClass.innerText = data.class.toUpperCase();
79
+
80
+ const conf = Math.round(data.confidence * 100);
81
+ confidenceValue.innerText = `${conf}%`;
82
+ confidenceFill.style.width = `${conf}%`;
83
+
84
+ probBreakdown.innerHTML = '';
85
+ Object.entries(data.probabilities).forEach(([name, prob]) => {
86
+ const div = document.createElement('div');
87
+ div.className = 'prob-item';
88
+ div.innerHTML = `
89
+ <span>${name.toUpperCase()}</span>
90
+ <span>${(prob * 100).toFixed(2)}%</span>
91
+ `;
92
+ probBreakdown.appendChild(div);
93
+ });
94
+ }
95
+
96
+ resetBtn.addEventListener('click', () => {
97
+ fileInput.value = '';
98
+ previewContainer.style.display = 'none';
99
+ predictedClass.innerText = 'Awaiting Input';
100
+ confidenceValue.innerText = '0%';
101
+ confidenceFill.style.width = '0%';
102
+ probBreakdown.innerHTML = '';
103
+ statusText.innerText = 'System Ready';
104
+ latencyValue.innerText = '0ms';
105
+ });
template/styles.css ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-color: #050a0f;
3
+ --card-bg: rgba(13, 25, 35, 0.7);
4
+ --accent: #00f2ff;
5
+ --accent-glow: rgba(0, 242, 255, 0.3);
6
+ --text-primary: #e0f2f1;
7
+ --text-secondary: #94a3b8;
8
+ --danger: #ff4d4d;
9
+ --success: #00ff88;
10
+ --glass-border: rgba(255, 255, 255, 0.1);
11
+ }
12
+
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ font-family: 'Outfit', sans-serif;
21
+ background-color: var(--bg-color);
22
+ color: var(--text-primary);
23
+ min-height: 100vh;
24
+ overflow-x: hidden;
25
+ display: flex;
26
+ justify-content: center;
27
+ align-items: center;
28
+ }
29
+
30
+ .background-mesh {
31
+ position: fixed;
32
+ top: 0;
33
+ left: 0;
34
+ width: 100%;
35
+ height: 100%;
36
+ background-image:
37
+ radial-gradient(circle at 20% 30%, rgba(0, 242, 255, 0.05) 0%, transparent 40%),
38
+ radial-gradient(circle at 80% 70%, rgba(0, 242, 255, 0.05) 0%, transparent 40%);
39
+ z-index: -1;
40
+ }
41
+
42
+ .background-mesh::after {
43
+ content: "";
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ width: 100%;
48
+ height: 100%;
49
+ background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
50
+ background-size: 50px 50px;
51
+ }
52
+
53
+ .scan-line {
54
+ position: fixed;
55
+ top: 0;
56
+ left: 0;
57
+ width: 100%;
58
+ height: 2px;
59
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
60
+ opacity: 0.1;
61
+ animation: scanMove 8s linear infinite;
62
+ z-index: 100;
63
+ }
64
+
65
+ @keyframes scanMove {
66
+ 0% { transform: translateY(-100vh); }
67
+ 100% { transform: translateY(100vh); }
68
+ }
69
+
70
+ .container {
71
+ width: 90%;
72
+ max-width: 1000px;
73
+ padding: 2rem;
74
+ z-index: 1;
75
+ }
76
+
77
+ .header {
78
+ text-align: center;
79
+ margin-bottom: 3rem;
80
+ }
81
+
82
+ .logo {
83
+ display: flex;
84
+ justify-content: center;
85
+ align-items: center;
86
+ gap: 1rem;
87
+ margin-bottom: 0.5rem;
88
+ }
89
+
90
+ .logo-icon {
91
+ font-size: 2.5rem;
92
+ color: var(--accent);
93
+ text-shadow: 0 0 15px var(--accent-glow);
94
+ }
95
+
96
+ h1 {
97
+ font-family: 'Orbitron', sans-serif;
98
+ font-size: 2.5rem;
99
+ letter-spacing: 4px;
100
+ }
101
+
102
+ .accent {
103
+ color: var(--accent);
104
+ text-shadow: 0 0 10px var(--accent-glow);
105
+ }
106
+
107
+ .subtitle {
108
+ color: var(--text-secondary);
109
+ letter-spacing: 2px;
110
+ text-transform: uppercase;
111
+ font-size: 0.8rem;
112
+ }
113
+
114
+ .analysis-core {
115
+ display: grid;
116
+ grid-template-columns: 1fr 1fr;
117
+ gap: 2rem;
118
+ align-items: start;
119
+ }
120
+
121
+ .upload-zone {
122
+ background: var(--card-bg);
123
+ border: 2px dashed var(--glass-border);
124
+ border-radius: 20px;
125
+ height: 400px;
126
+ position: relative;
127
+ cursor: pointer;
128
+ overflow: hidden;
129
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
130
+ display: flex;
131
+ justify-content: center;
132
+ align-items: center;
133
+ backdrop-filter: blur(10px);
134
+ }
135
+
136
+ .upload-zone:hover {
137
+ border-color: var(--accent);
138
+ box-shadow: 0 0 30px var(--accent-glow);
139
+ }
140
+
141
+ .upload-content {
142
+ text-align: center;
143
+ transition: opacity 0.3s;
144
+ }
145
+
146
+ .hex-icon {
147
+ width: 80px;
148
+ height: 80px;
149
+ margin: 0 auto 1.5rem;
150
+ background: var(--accent);
151
+ clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
152
+ display: flex;
153
+ justify-content: center;
154
+ align-items: center;
155
+ position: relative;
156
+ }
157
+
158
+ .hex-inner {
159
+ width: 70px;
160
+ height: 70px;
161
+ background: var(--bg-color);
162
+ clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
163
+ }
164
+
165
+ .preview-container {
166
+ position: absolute;
167
+ top: 0;
168
+ left: 0;
169
+ width: 100%;
170
+ height: 100%;
171
+ display: none;
172
+ background: var(--bg-color);
173
+ }
174
+
175
+ #imagePreview {
176
+ width: 100%;
177
+ height: 100%;
178
+ object-fit: contain;
179
+ }
180
+
181
+ .scanning-overlay {
182
+ position: absolute;
183
+ top: 0;
184
+ left: 0;
185
+ width: 100%;
186
+ height: 100%;
187
+ background: rgba(0, 242, 255, 0.1);
188
+ display: none;
189
+ }
190
+
191
+ .scanner-bar {
192
+ width: 100%;
193
+ height: 4px;
194
+ background: var(--accent);
195
+ box-shadow: 0 0 20px var(--accent);
196
+ position: absolute;
197
+ top: 0;
198
+ animation: scannerAnimate 2s linear infinite;
199
+ }
200
+
201
+ @keyframes scannerAnimate {
202
+ 0% { top: 0; }
203
+ 50% { top: 100%; }
204
+ 100% { top: 0; }
205
+ }
206
+
207
+ .results-panel {
208
+ background: var(--card-bg);
209
+ border: 1px solid var(--glass-border);
210
+ border-radius: 20px;
211
+ padding: 2rem;
212
+ min-height: 400px;
213
+ display: flex;
214
+ flex-direction: column;
215
+ backdrop-filter: blur(10px);
216
+ }
217
+
218
+ .status-indicator {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 0.75rem;
222
+ margin-bottom: 2rem;
223
+ font-size: 0.9rem;
224
+ color: var(--text-secondary);
225
+ }
226
+
227
+ .pulse {
228
+ width: 8px;
229
+ height: 8px;
230
+ background: var(--accent);
231
+ border-radius: 50%;
232
+ box-shadow: 0 0 10px var(--accent);
233
+ animation: blink 1.5s infinite;
234
+ }
235
+
236
+ @keyframes blink {
237
+ 0%, 100% { opacity: 1; }
238
+ 50% { opacity: 0.3; }
239
+ }
240
+
241
+ .main-result {
242
+ margin-bottom: 2rem;
243
+ }
244
+
245
+ .main-result label {
246
+ font-size: 0.7rem;
247
+ text-transform: uppercase;
248
+ letter-spacing: 2px;
249
+ color: var(--text-secondary);
250
+ display: block;
251
+ margin-bottom: 0.5rem;
252
+ }
253
+
254
+ #predictedClass {
255
+ font-family: 'Orbitron', sans-serif;
256
+ font-size: 1.8rem;
257
+ color: var(--accent);
258
+ }
259
+
260
+ .meter-header {
261
+ display: flex;
262
+ justify-content: space-between;
263
+ font-size: 0.8rem;
264
+ margin-bottom: 0.5rem;
265
+ }
266
+
267
+ .meter-bar {
268
+ height: 10px;
269
+ background: rgba(255,255,255,0.05);
270
+ border-radius: 5px;
271
+ overflow: hidden;
272
+ margin-bottom: 2rem;
273
+ }
274
+
275
+ .meter-fill {
276
+ height: 100%;
277
+ width: 0%;
278
+ background: linear-gradient(90deg, var(--accent), var(--success));
279
+ transition: width 1s ease-out;
280
+ }
281
+
282
+ .probability-breakdown {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 0.75rem;
286
+ margin-bottom: 2rem;
287
+ }
288
+
289
+ .prob-item {
290
+ display: flex;
291
+ justify-content: space-between;
292
+ font-size: 0.85rem;
293
+ padding: 0.5rem;
294
+ background: rgba(255,255,255,0.03);
295
+ border-radius: 5px;
296
+ }
297
+
298
+ .reset-btn {
299
+ margin-top: auto;
300
+ background: transparent;
301
+ border: 1px solid var(--accent);
302
+ color: var(--accent);
303
+ padding: 1rem;
304
+ border-radius: 10px;
305
+ font-family: 'Orbitron', sans-serif;
306
+ cursor: pointer;
307
+ transition: all 0.3s;
308
+ text-transform: uppercase;
309
+ letter-spacing: 2px;
310
+ }
311
+
312
+ .reset-btn:hover {
313
+ background: var(--accent);
314
+ color: var(--bg-color);
315
+ box-shadow: 0 0 20px var(--accent-glow);
316
+ }
317
+
318
+ .footer {
319
+ margin-top: 2rem;
320
+ border-top: 1px solid var(--glass-border);
321
+ padding-top: 1rem;
322
+ }
323
+
324
+ .system-stats {
325
+ display: flex;
326
+ justify-content: center;
327
+ gap: 2rem;
328
+ font-size: 0.7rem;
329
+ color: var(--text-secondary);
330
+ font-family: monospace;
331
+ }
332
+
333
+ @media (max-width: 768px) {
334
+ .analysis-core {
335
+ grid-template-columns: 1fr;
336
+ }
337
+ }