Abdul Rafay commited on
Commit
1b54c44
Β·
1 Parent(s): c44ccd7

updated UI

Browse files
__pycache__/main.cpython-310.pyc CHANGED
Binary files a/__pycache__/main.cpython-310.pyc and b/__pycache__/main.cpython-310.pyc differ
 
static/app.js CHANGED
@@ -3,109 +3,153 @@
3
  ══════════════════════════════════════ */
4
  (function () {
5
  const cells = document.querySelectorAll('.hg-cell');
 
6
  function randomLit() {
7
  cells.forEach(c => c.classList.remove('lit'));
8
  const count = 3 + Math.floor(Math.random() * 3);
9
- const indices = [...Array(cells.length).keys()]
10
  .sort(() => Math.random() - 0.5)
11
- .slice(0, count);
12
- indices.forEach(i => cells[i].classList.add('lit'));
13
  }
14
  randomLit();
15
  setInterval(randomLit, 1800);
16
  })();
17
 
18
  /* ══════════════════════════════════════
19
- CANVAS DRAWING
 
 
20
  ══════════════════════════════════════ */
21
  const canvas = document.getElementById('draw-canvas');
22
- const ctx = canvas.getContext('2d');
 
23
 
24
- // Size canvas to its CSS display size
25
  function resizeCanvas() {
 
26
  const rect = canvas.getBoundingClientRect();
27
- canvas.width = rect.width * window.devicePixelRatio;
28
- canvas.height = rect.height * window.devicePixelRatio;
29
- ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
30
- ctx.fillStyle = '#080a08';
31
- ctx.fillRect(0, 0, rect.width, rect.height);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
 
33
  resizeCanvas();
34
- window.addEventListener('resize', resizeCanvas);
35
 
36
- // Drawing state
37
- let drawing = false;
 
 
 
 
 
 
 
 
 
38
  let lastX = 0, lastY = 0;
39
  let hasDrawn = false;
40
 
 
 
 
 
41
  function getPos(e) {
42
  const rect = canvas.getBoundingClientRect();
43
- if (e.touches) {
44
- return {
45
- x: e.touches[0].clientX - rect.left,
46
- y: e.touches[0].clientY - rect.top
47
- };
48
- }
49
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
50
  }
51
 
52
  function startDraw(e) {
53
  e.preventDefault();
54
- drawing = true;
55
- hasDrawn = true;
56
  const { x, y } = getPos(e);
57
  lastX = x; lastY = y;
 
 
58
  ctx.beginPath();
59
  ctx.arc(x, y, 10, 0, Math.PI * 2);
60
  ctx.fillStyle = '#d4f5d0';
61
  ctx.fill();
62
  }
63
 
64
- function draw(e) {
65
- if (!drawing) return;
66
  e.preventDefault();
67
  const { x, y } = getPos(e);
 
68
  ctx.beginPath();
69
  ctx.moveTo(lastX, lastY);
70
  ctx.lineTo(x, y);
71
- ctx.strokeStyle = '#d4f5d0';
72
- ctx.lineWidth = 20;
73
- ctx.lineCap = 'round';
74
- ctx.lineJoin = 'round';
75
  ctx.stroke();
 
76
  lastX = x; lastY = y;
77
  }
78
 
79
- function endDraw() { drawing = false; }
 
 
80
 
 
81
  canvas.addEventListener('mousedown', startDraw);
82
- canvas.addEventListener('mousemove', draw);
83
  canvas.addEventListener('mouseup', endDraw);
84
  canvas.addEventListener('mouseleave', endDraw);
 
 
85
  canvas.addEventListener('touchstart', startDraw, { passive: false });
86
- canvas.addEventListener('touchmove', draw, { passive: false });
87
- canvas.addEventListener('touchend', endDraw);
 
88
 
89
  /* ══════════════════════════════════════
90
  CLEAR
91
  ══════════════════════════════════════ */
92
  document.getElementById('clearBtn').addEventListener('click', () => {
93
  const rect = canvas.getBoundingClientRect();
94
- ctx.fillStyle = '#080a08';
95
  ctx.fillRect(0, 0, rect.width, rect.height);
96
  hasDrawn = false;
97
  showEmpty();
98
  });
99
 
100
  /* ══════════════════════════════════════
101
- RESULTS RENDERING
102
  ══════════════════════════════════════ */
103
  function showEmpty() {
104
  document.getElementById('results-area').innerHTML = `
105
- <div class="empty-state" id="emptyState">
106
  <div class="empty-icon">β—Ž</div>
107
  <div class="empty-title">No prediction yet</div>
108
- <div class="empty-sub">Draw a digit on the left,<br>then click Run Inference</div>
109
  </div>`;
110
  }
111
 
@@ -117,15 +161,22 @@ function showLoading() {
117
  </div>`;
118
  }
119
 
 
 
 
 
 
 
 
 
 
120
  function showResults(data) {
121
- const pred = data.prediction;
122
- const conf = data.confidence;
123
- const probs = data.probabilities;
124
  const confPct = (conf * 100).toFixed(1);
125
 
126
- // Sort all 10 digits by probability descending
127
- const sorted = Object.entries(probs)
128
- .sort((a, b) => b[1] - a[1]);
129
 
130
  const barsHTML = sorted.map(([digit, prob]) => {
131
  const isTop = parseInt(digit) === pred;
@@ -151,44 +202,31 @@ function showResults(data) {
151
  <div class="conf-track">
152
  <div class="conf-fill" style="width:${confPct}%"></div>
153
  </div>
154
- <div class="conf-range">
155
- <span>0%</span>
156
- <span>50%</span>
157
- <span>100%</span>
158
- </div>
159
  </div>
160
  </div>
161
-
162
  <div class="probs-section">
163
  <div class="probs-label">Full probability distribution</div>
164
- <div class="probs-grid">
165
- ${barsHTML}
166
- </div>
167
  </div>
168
  </div>`;
169
- }
170
 
171
- function showError(msg) {
172
- document.getElementById('results-area').innerHTML = `
173
- <div class="empty-state">
174
- <div class="empty-icon" style="color:#ff6b6b;border-color:rgba(255,107,107,0.2);background:rgba(255,107,107,0.07);">⚠</div>
175
- <div class="empty-title" style="color:#ff9999;">${msg}</div>
176
- <div class="empty-sub">Check that the server is running</div>
177
- </div>`;
178
  }
179
 
180
  /* ══════════════════════════════════════
181
  PREDICT
182
  ══════════════════════════════════════ */
183
- document.getElementById('predictBtn').addEventListener('click', async () => {
184
- if (!hasDrawn) {
185
- showError('Draw a digit first');
186
- return;
187
- }
188
 
 
 
189
  showLoading();
190
 
191
- // Export canvas to blob
192
  canvas.toBlob(async (blob) => {
193
  const fd = new FormData();
194
  fd.append('file', blob, 'digit.png');
@@ -200,12 +238,14 @@ document.getElementById('predictBtn').addEventListener('click', async () => {
200
  showResults(data);
201
  } catch (err) {
202
  showError(err.message || 'Request failed');
 
 
203
  }
204
  }, 'image/png');
205
  });
206
 
207
  /* ══════════════════════════════════════
208
- NAV TABS (cosmetic)
209
  ══════════════════════════════════════ */
210
  document.querySelectorAll('.nav-tab').forEach(tab => {
211
  tab.addEventListener('click', () => {
 
3
  ══════════════════════════════════════ */
4
  (function () {
5
  const cells = document.querySelectorAll('.hg-cell');
6
+ if (!cells.length) return;
7
  function randomLit() {
8
  cells.forEach(c => c.classList.remove('lit'));
9
  const count = 3 + Math.floor(Math.random() * 3);
10
+ [...Array(cells.length).keys()]
11
  .sort(() => Math.random() - 0.5)
12
+ .slice(0, count)
13
+ .forEach(i => cells[i].classList.add('lit'));
14
  }
15
  randomLit();
16
  setInterval(randomLit, 1800);
17
  })();
18
 
19
  /* ══════════════════════════════════════
20
+ CANVAS SETUP
21
+ Key fix: never accumulate ctx.scale().
22
+ Instead, always setTransform() to start fresh.
23
  ══════════════════════════════════════ */
24
  const canvas = document.getElementById('draw-canvas');
25
+ const ctx = canvas.getContext('2d');
26
+ const DPR = window.devicePixelRatio || 1;
27
 
 
28
  function resizeCanvas() {
29
+ // Read the CSS-rendered size
30
  const rect = canvas.getBoundingClientRect();
31
+ const w = Math.round(rect.width);
32
+ const h = Math.round(rect.height);
33
+
34
+ // Save current drawing as image before resizing
35
+ let snapshot = null;
36
+ if (canvas.width > 0 && canvas.height > 0) {
37
+ try { snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height); } catch (_) {}
38
+ }
39
+
40
+ // Resize the backing buffer
41
+ canvas.width = w * DPR;
42
+ canvas.height = h * DPR;
43
+
44
+ // Reset transform cleanly β€” no cumulative scaling
45
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
46
+
47
+ // Fill background
48
+ ctx.fillStyle = '#070908';
49
+ ctx.fillRect(0, 0, w, h);
50
+
51
+ // Restore drawing (best-effort; ignored if sizes differ)
52
+ if (snapshot) {
53
+ try { ctx.putImageData(snapshot, 0, 0); } catch (_) {}
54
+ }
55
  }
56
+
57
  resizeCanvas();
 
58
 
59
+ // Debounce resize to avoid jank during orientation change
60
+ let resizeTimer;
61
+ window.addEventListener('resize', () => {
62
+ clearTimeout(resizeTimer);
63
+ resizeTimer = setTimeout(resizeCanvas, 80);
64
+ });
65
+
66
+ /* ══════════════════════════════════════
67
+ DRAWING
68
+ ══════════════════════════════════════ */
69
+ let isDrawing = false;
70
  let lastX = 0, lastY = 0;
71
  let hasDrawn = false;
72
 
73
+ /**
74
+ * Get pointer position relative to canvas CSS size.
75
+ * Works for both mouse and touch (single touch).
76
+ */
77
  function getPos(e) {
78
  const rect = canvas.getBoundingClientRect();
79
+ const src = e.touches ? e.touches[0] : e;
80
+ return {
81
+ x: src.clientX - rect.left,
82
+ y: src.clientY - rect.top,
83
+ };
 
 
84
  }
85
 
86
  function startDraw(e) {
87
  e.preventDefault();
88
+ isDrawing = true;
89
+ hasDrawn = true;
90
  const { x, y } = getPos(e);
91
  lastX = x; lastY = y;
92
+
93
+ // Draw a dot on tap/click so single points register
94
  ctx.beginPath();
95
  ctx.arc(x, y, 10, 0, Math.PI * 2);
96
  ctx.fillStyle = '#d4f5d0';
97
  ctx.fill();
98
  }
99
 
100
+ function moveDraw(e) {
101
+ if (!isDrawing) return;
102
  e.preventDefault();
103
  const { x, y } = getPos(e);
104
+
105
  ctx.beginPath();
106
  ctx.moveTo(lastX, lastY);
107
  ctx.lineTo(x, y);
108
+ ctx.strokeStyle = '#d4f5d0';
109
+ ctx.lineWidth = 20;
110
+ ctx.lineCap = 'round';
111
+ ctx.lineJoin = 'round';
112
  ctx.stroke();
113
+
114
  lastX = x; lastY = y;
115
  }
116
 
117
+ function endDraw(e) {
118
+ isDrawing = false;
119
+ }
120
 
121
+ // Mouse
122
  canvas.addEventListener('mousedown', startDraw);
123
+ canvas.addEventListener('mousemove', moveDraw);
124
  canvas.addEventListener('mouseup', endDraw);
125
  canvas.addEventListener('mouseleave', endDraw);
126
+
127
+ // Touch β€” passive:false lets us call e.preventDefault() to stop scroll
128
  canvas.addEventListener('touchstart', startDraw, { passive: false });
129
+ canvas.addEventListener('touchmove', moveDraw, { passive: false });
130
+ canvas.addEventListener('touchend', endDraw, { passive: false });
131
+ canvas.addEventListener('touchcancel',endDraw, { passive: false });
132
 
133
  /* ══════════════════════════════════════
134
  CLEAR
135
  ══════════════════════════════════════ */
136
  document.getElementById('clearBtn').addEventListener('click', () => {
137
  const rect = canvas.getBoundingClientRect();
138
+ ctx.fillStyle = '#070908';
139
  ctx.fillRect(0, 0, rect.width, rect.height);
140
  hasDrawn = false;
141
  showEmpty();
142
  });
143
 
144
  /* ══════════════════════════════════════
145
+ UI STATE HELPERS
146
  ══════════════════════════════════════ */
147
  function showEmpty() {
148
  document.getElementById('results-area').innerHTML = `
149
+ <div class="empty-state">
150
  <div class="empty-icon">β—Ž</div>
151
  <div class="empty-title">No prediction yet</div>
152
+ <div class="empty-sub">Draw a digit above,<br>then tap <strong>Run Inference</strong></div>
153
  </div>`;
154
  }
155
 
 
161
  </div>`;
162
  }
163
 
164
+ function showError(msg) {
165
+ document.getElementById('results-area').innerHTML = `
166
+ <div class="empty-state">
167
+ <div class="empty-icon" style="color:#ff8080;border-color:rgba(255,80,80,0.2);background:rgba(255,80,80,0.07);">⚠</div>
168
+ <div class="empty-title" style="color:#ff9999;">${msg}</div>
169
+ <div class="empty-sub">Make sure the server is running</div>
170
+ </div>`;
171
+ }
172
+
173
  function showResults(data) {
174
+ const pred = data.prediction;
175
+ const conf = data.confidence;
176
+ const probs = data.probabilities;
177
  const confPct = (conf * 100).toFixed(1);
178
 
179
+ const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
 
 
180
 
181
  const barsHTML = sorted.map(([digit, prob]) => {
182
  const isTop = parseInt(digit) === pred;
 
202
  <div class="conf-track">
203
  <div class="conf-fill" style="width:${confPct}%"></div>
204
  </div>
205
+ <div class="conf-range"><span>0%</span><span>50%</span><span>100%</span></div>
 
 
 
 
206
  </div>
207
  </div>
 
208
  <div class="probs-section">
209
  <div class="probs-label">Full probability distribution</div>
210
+ <div class="probs-grid">${barsHTML}</div>
 
 
211
  </div>
212
  </div>`;
 
213
 
214
+ // Scroll results into view on mobile
215
+ if (window.innerWidth <= 680) {
216
+ document.getElementById('results-area').scrollIntoView({ behavior: 'smooth', block: 'start' });
217
+ }
 
 
 
218
  }
219
 
220
  /* ══════════════════════════════════════
221
  PREDICT
222
  ══════════════════════════════════════ */
223
+ document.getElementById('predictBtn').addEventListener('click', () => {
224
+ if (!hasDrawn) { showError('Draw a digit first'); return; }
 
 
 
225
 
226
+ const btn = document.getElementById('predictBtn');
227
+ btn.disabled = true;
228
  showLoading();
229
 
 
230
  canvas.toBlob(async (blob) => {
231
  const fd = new FormData();
232
  fd.append('file', blob, 'digit.png');
 
238
  showResults(data);
239
  } catch (err) {
240
  showError(err.message || 'Request failed');
241
+ } finally {
242
+ btn.disabled = false;
243
  }
244
  }, 'image/png');
245
  });
246
 
247
  /* ══════════════════════════════════════
248
+ NAV TABS (cosmetic only)
249
  ══════════════════════════════════════ */
250
  document.querySelectorAll('.nav-tab').forEach(tab => {
251
  tab.addEventListener('click', () => {
static/index.html CHANGED
@@ -3,8 +3,8 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Numera - Digit Classifier</title>
7
- <link rel="stylesheet" href="static/style.css" />
8
  </head>
9
  <body>
10
 
@@ -16,6 +16,15 @@
16
  <div class="nav-logo-mark">🧿</div>
17
  <span class="nav-logo-text">Numera</span>
18
  </div>
 
 
 
 
 
 
 
 
 
19
  </nav>
20
 
21
  <!-- ══════════════════════════════════════
@@ -32,7 +41,7 @@
32
  recognition, <em>instantly.</em>
33
  </h1>
34
  <p class="hero-sub">
35
- Draw any digit from 0 to 9 and prediction in real time β€” with full probability
36
  distribution across all classes.
37
  </p>
38
  <div class="hero-stats">
@@ -45,7 +54,7 @@
45
  <div class="stat-label">Training samples</div>
46
  </div>
47
  <div>
48
- <div class="stat-val">&lt; 1 sec</div>
49
  <div class="stat-label">Inference time</div>
50
  </div>
51
  </div>
@@ -71,7 +80,7 @@
71
  ══════════════════════════════════════ -->
72
  <div class="section-divider">
73
  <div class="div-line"></div>
74
- <span class="div-label">Classifier</span>
75
  <div class="div-line"></div>
76
  </div>
77
 
@@ -88,7 +97,7 @@
88
  <div class="bar-dot dot-y"></div>
89
  <div class="bar-dot dot-g"></div>
90
  </div>
91
- <span class="bar-title">digit-classifier.app</span>
92
  <div class="bar-status">
93
  <span class="bar-status-dot"></span>
94
  model ready
@@ -150,11 +159,13 @@
150
  ══════════════════════════════════════ -->
151
  <footer>
152
  <div class="footer-left">
 
 
153
  <span class="footer-note">PyTorch Β· MNIST Β· FastAPI</span>
154
  </div>
155
  <div class="footer-right">Built with β™₯ by Abdul Rafay</div>
156
  </footer>
157
 
158
- <script src="static/app.js"></script>
159
  </body>
160
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Numera β€” Digit Recognition</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
  </head>
9
  <body>
10
 
 
16
  <div class="nav-logo-mark">🧿</div>
17
  <span class="nav-logo-text">Numera</span>
18
  </div>
19
+
20
+ <div class="nav-right">
21
+ <span class="nav-pill">v1.0 Β· MNIST</span>
22
+ <a class="nav-github" href="https://github.com/abdurafay19/Digit-Classifier" title="GitHub">
23
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
24
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
25
+ </svg>
26
+ </a>
27
+ </div>
28
  </nav>
29
 
30
  <!-- ══════════════════════════════════════
 
41
  recognition, <em>instantly.</em>
42
  </h1>
43
  <p class="hero-sub">
44
+ Draw any digit from 0 to 9 and get prediction in real time β€” with full probability
45
  distribution across all classes.
46
  </p>
47
  <div class="hero-stats">
 
54
  <div class="stat-label">Training samples</div>
55
  </div>
56
  <div>
57
+ <div class="stat-val">&lt;50ms</div>
58
  <div class="stat-label">Inference time</div>
59
  </div>
60
  </div>
 
80
  ══════════════════════════════════════ -->
81
  <div class="section-divider">
82
  <div class="div-line"></div>
83
+ <span class="div-label">Live classifier</span>
84
  <div class="div-line"></div>
85
  </div>
86
 
 
97
  <div class="bar-dot dot-y"></div>
98
  <div class="bar-dot dot-g"></div>
99
  </div>
100
+ <span class="bar-title">numera β€” digit-classifier.app</span>
101
  <div class="bar-status">
102
  <span class="bar-status-dot"></span>
103
  model ready
 
159
  ══════════════════════════════════════ -->
160
  <footer>
161
  <div class="footer-left">
162
+ <span class="footer-logo">Numera</span>
163
+ <span class="footer-sep">Β·</span>
164
  <span class="footer-note">PyTorch Β· MNIST Β· FastAPI</span>
165
  </div>
166
  <div class="footer-right">Built with β™₯ by Abdul Rafay</div>
167
  </footer>
168
 
169
+ <script src="/static/app.js"></script>
170
  </body>
171
  </html>
static/style.css CHANGED
@@ -1,35 +1,27 @@
1
  @import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Instrument+Sans:ital,wght@0,400;0,500;0,600;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap');
2
 
3
- /* ══════════════════════════════════════
4
- TOKENS
5
- ══════════════════════════════════════ */
6
  :root {
7
- --bg: #080909;
8
- --bg2: #0c0d0e;
9
- --surface: #111314;
10
- --surface2: #181b1d;
11
- --surface3: #1e2225;
12
- --border: rgba(255,255,255,0.06);
13
- --border2: rgba(255,255,255,0.10);
14
- --border3: rgba(255,255,255,0.16);
15
- --accent: #a8f0a0;
16
- --accent2: #6ee87a;
17
- --accent-glow: rgba(168,240,160,0.18);
18
- --accent-dim: rgba(168,240,160,0.07);
19
- --text: #eeeef0;
20
- --text-sec: #7e8590;
21
- --text-muted: #40464f;
22
- --radius: 18px;
23
- --radius-sm: 10px;
24
- --radius-xs: 7px;
25
- --ease: cubic-bezier(0.4,0,0.2,1);
26
  }
27
 
28
- /* ══════════════════════════════════════
29
- RESET & BASE
30
- ══════════════════════════════════════ */
31
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
32
- html { scroll-behavior: smooth; font-size: 16px; }
33
 
34
  body {
35
  background: var(--bg);
@@ -38,15 +30,13 @@ body {
38
  line-height: 1.5;
39
  min-height: 100vh;
40
  overflow-x: hidden;
41
- cursor: default;
42
  -webkit-font-smoothing: antialiased;
 
43
  }
44
 
45
- /* subtle grid texture on bg */
46
  body::before {
47
  content: '';
48
- position: fixed;
49
- inset: 0;
50
  background-image:
51
  linear-gradient(rgba(255,255,255,0.012) 1px, transparent 1px),
52
  linear-gradient(90deg, rgba(255,255,255,0.012) 1px, transparent 1px);
@@ -55,669 +45,459 @@ body::before {
55
  z-index: 0;
56
  }
57
 
58
- /* ══════════════════════════════════════
59
- NAV
60
- ══════════════════════════════════════ */
61
  nav {
62
  position: fixed;
63
- top: 0; left: 0; right: 0;
64
- z-index: 200;
65
- height: 58px;
66
  display: flex;
67
  align-items: center;
68
  justify-content: space-between;
69
- padding: 0 32px;
70
- background: rgba(8,9,9,0.75);
71
  backdrop-filter: blur(24px);
72
  -webkit-backdrop-filter: blur(24px);
73
  border-bottom: 1px solid var(--border);
 
74
  }
75
 
76
- .nav-logo {
77
- display: flex;
78
- align-items: center;
79
- gap: 9px;
80
- }
81
  .nav-logo-mark {
82
- width: 30px; height: 30px;
83
- background: linear-gradient(135deg, var(--accent) 0%, #4dd65a 100%);
84
- border-radius: 8px;
85
- display: grid;
86
- place-items: center;
87
- font-family: 'JetBrains Mono', monospace;
88
- font-size: 0.78rem;
89
- font-weight: 500;
90
- color: #060907;
91
- letter-spacing: -0.04em;
92
- flex-shrink: 0;
93
- }
94
- .nav-logo-text {
95
- font-weight: 600;
96
- font-size: 0.9rem;
97
- letter-spacing: -0.01em;
98
- color: var(--text);
99
  }
 
100
 
101
- .nav-center {
102
- display: flex;
103
- align-items: center;
104
- gap: 4px;
105
- background: var(--surface);
106
- border: 1px solid var(--border2);
107
- border-radius: 100px;
108
- padding: 4px;
109
- }
110
  .nav-tab {
111
- font-size: 0.78rem;
112
- font-weight: 500;
113
- color: var(--text-sec);
114
- padding: 5px 16px;
115
- border-radius: 100px;
116
- cursor: pointer;
117
- transition: all 0.18s var(--ease);
118
- letter-spacing: 0.005em;
119
- border: none;
120
- background: none;
121
- }
122
- .nav-tab:hover { color: var(--text); }
123
- .nav-tab.active {
124
- background: var(--surface3);
125
- color: var(--text);
126
- border: 1px solid var(--border2);
127
- }
128
-
129
- .nav-right {
130
- display: flex;
131
- align-items: center;
132
- gap: 12px;
133
- }
134
  .nav-pill {
135
- font-family: 'JetBrains Mono', monospace;
136
- font-size: 0.66rem;
137
- letter-spacing: 0.08em;
138
- color: var(--accent);
139
- border: 1px solid rgba(168,240,160,0.2);
140
- background: var(--accent-dim);
141
- padding: 4px 12px;
142
- border-radius: 100px;
143
- }
144
-
145
- /* ══════════════════════════════════════
146
- HERO
147
- ══════════════════════════════════════ */
 
 
 
 
 
 
 
 
 
 
148
  .hero {
149
- position: relative;
150
- z-index: 1;
151
- padding: 130px 32px 70px;
152
- max-width: 1080px;
153
- margin: 0 auto;
154
- display: grid;
155
- grid-template-columns: 1fr auto;
156
- gap: 64px;
157
- align-items: center;
158
  }
159
-
160
- /* radial glow behind hero */
161
  .hero::before {
162
- content: '';
163
- position: absolute;
164
- top: 80px; left: 50%;
165
- transform: translateX(-50%);
166
- width: 600px; height: 300px;
167
- background: radial-gradient(ellipse, rgba(168,240,160,0.07) 0%, transparent 70%);
168
- pointer-events: none;
169
  }
170
 
171
  .hero-eyebrow {
172
- display: inline-flex;
173
- align-items: center;
174
- gap: 8px;
175
- font-family: 'JetBrains Mono', monospace;
176
- font-size: 0.68rem;
177
- letter-spacing: 0.12em;
178
- text-transform: uppercase;
179
- color: var(--accent);
180
- margin-bottom: 22px;
181
  }
182
  .eyebrow-dot {
183
- width: 5px; height: 5px;
184
- background: var(--accent);
185
- border-radius: 50%;
186
- animation: blink 2.4s ease-in-out infinite;
187
- }
188
- @keyframes blink {
189
- 0%,100% { opacity:1; } 50% { opacity:0.25; }
190
  }
 
191
 
192
  .hero-title {
193
- font-family: 'Instrument Serif', serif;
194
- font-size: clamp(2.6rem, 5vw, 4rem);
195
- font-weight: 400;
196
- line-height: 1.08;
197
- letter-spacing: -0.025em;
198
- color: var(--text);
199
- margin-bottom: 20px;
200
- }
201
- .hero-title em {
202
- font-style: italic;
203
- color: var(--accent);
204
  }
 
205
 
206
  .hero-sub {
207
- font-size: 1.0rem;
208
- color: var(--text-sec);
209
- line-height: 1.72;
210
- max-width: 440px;
211
- margin-bottom: 36px;
212
  }
213
 
214
  .hero-stats {
215
- display: flex;
216
- gap: 36px;
217
- padding-top: 30px;
218
- border-top: 1px solid var(--border);
219
- }
220
- .stat-val {
221
- font-family: 'Instrument Serif', serif;
222
- font-size: 1.7rem;
223
- color: var(--text);
224
- line-height: 1;
225
- margin-bottom: 4px;
226
  }
 
227
  .stat-label {
228
- font-family: 'JetBrains Mono', monospace;
229
- font-size: 0.63rem;
230
- letter-spacing: 0.1em;
231
- text-transform: uppercase;
232
- color: var(--text-muted);
233
  }
234
 
235
- /* digit grid on right */
236
  .hero-grid {
237
- display: grid;
238
- grid-template-columns: repeat(5, 48px);
239
- grid-template-rows: repeat(2, 48px);
240
- gap: 7px;
241
- flex-shrink: 0;
242
  }
243
  .hg-cell {
244
- width: 48px; height: 48px;
245
- border-radius: 10px;
246
- border: 1px solid var(--border2);
247
- background: var(--surface);
248
- display: grid;
249
- place-items: center;
250
- font-family: 'Instrument Serif', serif;
251
- font-size: 1.4rem;
252
- color: var(--text-muted);
253
- transition: all 0.4s var(--ease);
254
- user-select: none;
255
  }
256
  .hg-cell.lit {
257
- border-color: rgba(168,240,160,0.3);
258
- background: var(--accent-dim);
259
- color: var(--accent);
260
- box-shadow: 0 0 16px rgba(168,240,160,0.12);
261
  }
262
 
263
- /* ══════════════════════════════════════
264
- DIVIDER
265
- ══════════════════════════════════════ */
266
  .section-divider {
267
- position: relative;
268
- z-index: 1;
269
- max-width: 1080px;
270
- margin: 0 auto 20px;
271
- padding: 0 32px;
272
- display: flex;
273
- align-items: center;
274
- gap: 14px;
275
  }
276
- .div-line { flex:1; height:1px; background: var(--border); }
277
  .div-label {
278
- font-family: 'JetBrains Mono', monospace;
279
- font-size: 0.63rem;
280
- letter-spacing: 0.12em;
281
- text-transform: uppercase;
282
- color: var(--text-muted);
283
- white-space: nowrap;
284
  }
285
 
286
- /* ══════════════════════════════════════
287
- APP CARD
288
- ══════════════════════════════════════ */
289
  .app-wrap {
290
- position: relative;
291
- z-index: 1;
292
- max-width: 1080px;
293
- margin: 0 auto 100px;
294
- padding: 0 32px;
295
  }
296
-
297
  .app-card {
298
- background: var(--surface);
299
- border: 1px solid var(--border2);
300
- border-radius: 22px;
301
- overflow: hidden;
302
  box-shadow:
303
  0 0 0 1px rgba(255,255,255,0.025),
304
- 0 32px 80px rgba(0,0,0,0.55),
305
- 0 0 120px rgba(168,240,160,0.03);
306
  }
307
 
308
- /* macOS-style title bar */
309
  .card-bar {
310
- height: 44px;
311
- background: var(--bg2);
312
- border-bottom: 1px solid var(--border);
313
- display: flex;
314
- align-items: center;
315
- justify-content: space-between;
316
- padding: 0 18px;
317
- gap: 12px;
318
- }
319
- .bar-dots { display:flex; gap:7px; align-items:center; }
320
- .bar-dot {
321
- width: 11px; height: 11px;
322
- border-radius: 50%;
323
- }
324
- .dot-r { background: #ff5f57; }
325
- .dot-y { background: #ffbd2e; }
326
- .dot-g { background: #29c940; }
327
-
328
  .bar-title {
329
- font-family: 'JetBrains Mono', monospace;
330
- font-size: 0.7rem;
331
- letter-spacing: 0.06em;
332
- color: var(--text-muted);
333
  }
334
  .bar-status {
335
- display: flex;
336
- align-items: center;
337
- gap: 6px;
338
- font-family: 'JetBrains Mono', monospace;
339
- font-size: 0.66rem;
340
- color: var(--accent);
341
- letter-spacing: 0.05em;
342
  }
343
  .bar-status-dot {
344
- width: 5px; height: 5px;
345
- background: var(--accent);
346
- border-radius: 50%;
347
- animation: blink 2.4s ease-in-out infinite;
348
  }
349
 
350
- /* two-col layout */
351
  .app-grid {
352
- display: grid;
353
- grid-template-columns: 300px 1fr;
354
  }
355
 
356
  /* ── LEFT PANE ── */
357
- .left-pane {
358
- border-right: 1px solid var(--border);
359
- display: flex;
360
- flex-direction: column;
361
- }
362
- .pane-head {
363
- padding: 16px 18px 14px;
364
- border-bottom: 1px solid var(--border);
365
- }
366
  .pane-eyebrow {
367
- font-family: 'JetBrains Mono', monospace;
368
- font-size: 0.6rem;
369
- letter-spacing: 0.14em;
370
- text-transform: uppercase;
371
- color: var(--text-muted);
372
- margin-bottom: 3px;
373
- }
374
- .pane-name {
375
- font-size: 0.88rem;
376
- font-weight: 600;
377
- color: var(--text);
378
  }
 
379
 
380
  .canvas-wrap {
381
- flex: 1;
382
- padding: 16px;
383
- display: flex;
384
- flex-direction: column;
385
- gap: 12px;
386
- background: var(--bg2);
387
  }
388
 
389
- /* The actual HTML5 canvas drawing area */
390
  #draw-canvas {
391
- width: 100%;
392
- aspect-ratio: 1;
393
- border-radius: 12px;
394
- border: 1px solid var(--border2);
395
- background: #080a08;
396
- cursor: crosshair;
397
- display: block;
398
- touch-action: none;
 
 
 
399
  }
400
 
401
  .canvas-hint {
402
- display: flex;
403
- align-items: center;
404
- gap: 8px;
405
- font-family: 'JetBrains Mono', monospace;
406
- font-size: 0.64rem;
407
- letter-spacing: 0.06em;
408
- color: var(--text-muted);
409
- background: var(--surface);
410
- border: 1px solid var(--border);
411
- border-radius: var(--radius-xs);
412
- padding: 8px 12px;
413
- }
414
- .hint-icon { font-size: 0.8rem; opacity: 0.5; }
415
-
416
- .btn-row {
417
- display: flex;
418
- gap: 8px;
419
  }
 
 
 
420
  .btn {
421
- font-family: 'Instrument Sans', sans-serif;
422
- font-size: 0.82rem;
423
- font-weight: 600;
424
- border-radius: var(--radius-xs);
425
- height: 40px;
426
- border: none;
427
- cursor: pointer;
428
- transition: all 0.18s var(--ease);
429
- letter-spacing: 0.01em;
430
- display: flex;
431
- align-items: center;
432
- justify-content: center;
433
- gap: 6px;
434
  }
435
  .btn-primary {
436
- flex: 1;
437
- background: var(--accent);
438
- color: #060907;
439
- box-shadow: 0 0 24px rgba(168,240,160,0.18);
440
- }
441
- .btn-primary:hover {
442
- background: #bdf6b5;
443
- box-shadow: 0 0 32px rgba(168,240,160,0.3);
444
- transform: translateY(-1px);
445
- }
446
- .btn-primary:active { transform: translateY(0); box-shadow: none; }
447
- .btn-primary:disabled {
448
- opacity: 0.45;
449
- cursor: not-allowed;
450
- transform: none;
451
- box-shadow: none;
452
  }
 
 
 
453
 
454
  .btn-secondary {
455
- width: 40px;
456
- background: var(--surface);
457
- color: var(--text-sec);
458
- border: 1px solid var(--border2);
459
- }
460
- .btn-secondary:hover {
461
- background: var(--surface2);
462
- color: var(--text);
463
- border-color: var(--border3);
464
  }
 
 
465
 
466
  /* ── RIGHT PANE ── */
467
- .right-pane {
468
- display: flex;
469
- flex-direction: column;
470
- min-height: 520px;
471
- }
472
-
473
- #results-area {
474
- flex: 1;
475
- display: flex;
476
- flex-direction: column;
477
- }
478
 
479
- /* empty state */
480
  .empty-state {
481
- flex: 1;
482
- display: flex;
483
- flex-direction: column;
484
- align-items: center;
485
- justify-content: center;
486
- gap: 12px;
487
- padding: 60px 40px;
488
- color: var(--text-muted);
489
  }
490
  .empty-icon {
491
- width: 56px; height: 56px;
492
- border-radius: 14px;
493
- border: 1px solid var(--border2);
494
- background: var(--surface2);
495
- display: grid;
496
- place-items: center;
497
- font-size: 1.4rem;
498
- margin-bottom: 4px;
499
- }
500
- .empty-title {
501
- font-size: 0.82rem;
502
- font-weight: 600;
503
- color: var(--text-sec);
504
  }
 
505
  .empty-sub {
506
- font-family: 'JetBrains Mono', monospace;
507
- font-size: 0.65rem;
508
- letter-spacing: 0.06em;
509
- color: var(--text-muted);
510
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
511
  }
512
 
513
- /* results content */
514
  .results-content {
515
- padding: 24px 28px;
516
- display: flex;
517
- flex-direction: column;
518
- gap: 24px;
519
- animation: fadeIn 0.3s var(--ease);
520
  }
521
- @keyframes fadeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
522
 
523
- /* prediction hero */
524
  .pred-hero {
525
- display: flex;
526
- align-items: center;
527
- gap: 22px;
528
- padding-bottom: 22px;
529
- border-bottom: 1px solid var(--border);
530
  }
531
  .pred-digit {
532
- width: 88px; height: 88px;
533
- border-radius: 16px;
534
- background: var(--accent-dim);
535
- border: 1px solid rgba(168,240,160,0.18);
536
- display: grid;
537
- place-items: center;
538
- font-family: 'Instrument Serif', serif;
539
- font-size: 3.8rem;
540
- color: var(--accent);
541
- flex-shrink: 0;
542
- box-shadow:
543
- 0 0 40px rgba(168,240,160,0.1),
544
- inset 0 1px 0 rgba(255,255,255,0.04);
545
- position: relative;
546
- overflow: hidden;
547
  }
548
- .pred-digit::after {
549
- content: '';
550
- position: absolute;
551
- inset: 0;
552
- background: radial-gradient(circle at 30% 20%, rgba(168,240,160,0.08), transparent 60%);
553
- }
554
-
555
  .pred-info { flex:1; min-width:0; }
556
  .pred-label {
557
- font-family: 'JetBrains Mono', monospace;
558
- font-size: 0.6rem;
559
- letter-spacing: 0.14em;
560
- text-transform: uppercase;
561
- color: var(--text-muted);
562
- margin-bottom: 6px;
563
  }
564
  .pred-conf {
565
- font-family: 'Instrument Serif', serif;
566
- font-size: 2.2rem;
567
- color: var(--text);
568
- line-height: 1;
569
- margin-bottom: 12px;
570
  }
571
  .pred-conf small {
572
- font-family: 'Instrument Sans', sans-serif;
573
- font-size: 1rem;
574
- font-weight: 400;
575
- color: var(--text-sec);
576
  }
577
  .conf-track {
578
- height: 3px;
579
- background: var(--surface3);
580
- border-radius: 2px;
581
- overflow: hidden;
582
- margin-bottom: 6px;
583
  }
584
  .conf-fill {
585
- height: 100%;
586
- background: linear-gradient(90deg, var(--accent2), var(--accent));
587
- border-radius: 2px;
588
- transition: width 0.55s var(--ease);
589
  }
590
  .conf-range {
591
- display: flex;
592
- justify-content: space-between;
593
- font-family: 'JetBrains Mono', monospace;
594
- font-size: 0.58rem;
595
- color: var(--text-muted);
596
- letter-spacing: 0.06em;
597
  }
598
 
599
- /* probability distribution */
600
- .probs-section {}
601
  .probs-label {
602
- font-family: 'JetBrains Mono', monospace;
603
- font-size: 0.6rem;
604
- letter-spacing: 0.14em;
605
- text-transform: uppercase;
606
- color: var(--text-muted);
607
- margin-bottom: 14px;
608
- }
609
- .probs-grid {
610
- display: flex;
611
- flex-direction: column;
612
- gap: 7px;
613
  }
 
614
  .prob-row {
615
- display: grid;
616
- grid-template-columns: 18px 1fr 58px;
617
- align-items: center;
618
- gap: 12px;
619
- padding: 6px 10px;
620
- border-radius: var(--radius-xs);
621
- transition: background 0.15s;
622
- }
623
- .prob-row:hover { background: var(--surface2); }
624
- .prob-row.is-top {
625
- background: var(--accent-dim);
626
- border: 1px solid rgba(168,240,160,0.12);
627
- }
628
- .prob-num {
629
- font-family: 'JetBrains Mono', monospace;
630
- font-size: 0.72rem;
631
- color: var(--text-muted);
632
- text-align: right;
633
- line-height: 1;
634
- }
635
- .prob-row.is-top .prob-num { color: var(--accent); font-weight: 500; }
636
-
637
- .prob-bar-track {
638
- height: 4px;
639
- background: var(--surface3);
640
- border-radius: 2px;
641
- overflow: hidden;
642
- border: 1px solid var(--border);
643
- }
644
- .prob-bar-fill {
645
- height: 100%;
646
- background: var(--surface3);
647
- border-radius: 2px;
648
- transition: width 0.55s var(--ease);
649
- }
650
- .prob-row.is-top .prob-bar-fill {
651
- background: linear-gradient(90deg, var(--accent2), var(--accent));
652
- }
653
- .prob-pct {
654
- font-family: 'JetBrains Mono', monospace;
655
- font-size: 0.68rem;
656
- color: var(--text-muted);
657
- text-align: right;
658
- white-space: nowrap;
659
- }
660
- .prob-row.is-top .prob-pct { color: var(--accent); font-weight: 500; }
661
-
662
- /* loading spinner */
663
- .loading-state {
664
- flex:1;
665
- display: flex;
666
- flex-direction: column;
667
- align-items: center;
668
- justify-content: center;
669
- gap: 14px;
670
- padding: 60px;
671
- }
672
- .spinner {
673
- width: 32px; height: 32px;
674
- border: 2px solid var(--border2);
675
- border-top-color: var(--accent);
676
- border-radius: 50%;
677
- animation: spin 0.7s linear infinite;
678
  }
679
- @keyframes spin { to { transform: rotate(360deg); } }
680
- .loading-text {
681
- font-family: 'JetBrains Mono', monospace;
682
- font-size: 0.68rem;
683
- letter-spacing: 0.1em;
684
- text-transform: uppercase;
685
- color: var(--text-muted);
686
  }
687
 
688
  /* ══════════════════════════════════════
689
- FOOTER
690
  ══════════════════════════════════════ */
691
- footer {
692
- position: relative;
693
- z-index: 1;
694
- border-top: 1px solid var(--border);
695
- padding: 20px 32px;
696
- display: flex;
697
- align-items: center;
698
- justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  }
700
- .footer-left {
701
- display: flex;
702
- align-items: center;
703
- gap: 20px;
704
- }
705
- .footer-logo {
706
- font-weight: 600;
707
- font-size: 0.82rem;
708
- color: var(--text-sec);
709
- letter-spacing: -0.01em;
710
- }
711
- .footer-sep { color: var(--text-muted); font-size: 0.8rem; }
712
- .footer-note {
713
- font-family: 'JetBrains Mono', monospace;
714
- font-size: 0.62rem;
715
- letter-spacing: 0.08em;
716
- color: var(--text-muted);
717
- }
718
- .footer-right {
719
- font-family: 'JetBrains Mono', monospace;
720
- font-size: 0.62rem;
721
- letter-spacing: 0.08em;
722
- color: var(--text-muted);
723
  }
 
1
  @import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Instrument+Sans:ital,wght@0,400;0,500;0,600;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap');
2
 
 
 
 
3
  :root {
4
+ --bg: #080909;
5
+ --bg2: #0c0d0e;
6
+ --surface: #111314;
7
+ --surface2: #181b1d;
8
+ --surface3: #1e2225;
9
+ --border: rgba(255,255,255,0.06);
10
+ --border2: rgba(255,255,255,0.10);
11
+ --border3: rgba(255,255,255,0.16);
12
+ --accent: #a8f0a0;
13
+ --accent2: #6ee87a;
14
+ --accent-dim: rgba(168,240,160,0.07);
15
+ --text: #eeeef0;
16
+ --text-sec: #7e8590;
17
+ --text-muted: #40464f;
18
+ --radius-sm: 10px;
19
+ --radius-xs: 7px;
20
+ --ease: cubic-bezier(0.4,0,0.2,1);
 
 
21
  }
22
 
 
 
 
23
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
24
+ html { scroll-behavior: smooth; }
25
 
26
  body {
27
  background: var(--bg);
 
30
  line-height: 1.5;
31
  min-height: 100vh;
32
  overflow-x: hidden;
 
33
  -webkit-font-smoothing: antialiased;
34
+ -webkit-text-size-adjust: 100%;
35
  }
36
 
 
37
  body::before {
38
  content: '';
39
+ position: fixed; inset: 0;
 
40
  background-image:
41
  linear-gradient(rgba(255,255,255,0.012) 1px, transparent 1px),
42
  linear-gradient(90deg, rgba(255,255,255,0.012) 1px, transparent 1px);
 
45
  z-index: 0;
46
  }
47
 
48
+ /* ══ NAV ══ */
 
 
49
  nav {
50
  position: fixed;
51
+ top: 0; left: 0; right: 0; z-index: 200;
52
+ height: 54px;
 
53
  display: flex;
54
  align-items: center;
55
  justify-content: space-between;
56
+ padding: 0 20px;
57
+ background: rgba(8,9,9,0.88);
58
  backdrop-filter: blur(24px);
59
  -webkit-backdrop-filter: blur(24px);
60
  border-bottom: 1px solid var(--border);
61
+ gap: 12px;
62
  }
63
 
64
+ .nav-logo { display:flex; align-items:center; gap:9px; flex-shrink:0; }
 
 
 
 
65
  .nav-logo-mark {
66
+ width:28px; height:28px;
67
+ background: linear-gradient(135deg, var(--accent), #4dd65a);
68
+ border-radius:7px;
69
+ display:grid; place-items:center;
70
+ font-family:'JetBrains Mono',monospace;
71
+ font-size:0.7rem; font-weight:500;
72
+ color:#060907; letter-spacing:-0.04em;
 
 
 
 
 
 
 
 
 
 
73
  }
74
+ .nav-logo-text { font-weight:600; font-size:0.9rem; color:var(--text); }
75
 
 
 
 
 
 
 
 
 
 
76
  .nav-tab {
77
+ font-size:0.75rem; font-weight:500;
78
+ color:var(--text-sec);
79
+ padding:5px 14px;
80
+ border-radius:100px;
81
+ cursor:pointer;
82
+ transition:all 0.18s var(--ease);
83
+ border:none; background:none;
84
+ white-space:nowrap;
85
+ -webkit-tap-highlight-color:transparent;
86
+ }
87
+ .nav-tab:hover { color:var(--text); }
88
+ .nav-tab.active { background:var(--surface3); color:var(--text); border:1px solid var(--border2); }
89
+
90
+ .nav-right { display:flex; align-items:center; gap:10px; flex-shrink:0; }
 
 
 
 
 
 
 
 
 
91
  .nav-pill {
92
+ font-family:'JetBrains Mono',monospace;
93
+ font-size:0.62rem; letter-spacing:0.06em;
94
+ color:var(--accent);
95
+ border:1px solid rgba(168,240,160,0.2);
96
+ background:var(--accent-dim);
97
+ padding:4px 10px; border-radius:100px;
98
+ white-space:nowrap;
99
+ }
100
+ .nav-github {
101
+ width:30px; height:30px;
102
+ border-radius:7px;
103
+ border:1px solid var(--border2);
104
+ background:var(--surface);
105
+ display:grid; place-items:center;
106
+ cursor:pointer;
107
+ transition:all 0.18s var(--ease);
108
+ color:var(--text-sec); text-decoration:none;
109
+ -webkit-tap-highlight-color:transparent;
110
+ }
111
+ .nav-github:hover { border-color:var(--border3); color:var(--text); }
112
+ .nav-github svg { width:14px; height:14px; fill:currentColor; }
113
+
114
+ /* ══ HERO ══ */
115
  .hero {
116
+ position:relative; z-index:1;
117
+ padding:100px 20px 50px;
118
+ max-width:1080px; margin:0 auto;
119
+ display:grid;
120
+ grid-template-columns:1fr auto;
121
+ gap:48px;
122
+ align-items:center;
 
 
123
  }
 
 
124
  .hero::before {
125
+ content:'';
126
+ position:absolute; top:60px; left:50%;
127
+ transform:translateX(-50%);
128
+ width:500px; height:260px;
129
+ background:radial-gradient(ellipse,rgba(168,240,160,0.07) 0%,transparent 70%);
130
+ pointer-events:none;
 
131
  }
132
 
133
  .hero-eyebrow {
134
+ display:inline-flex; align-items:center; gap:8px;
135
+ font-family:'JetBrains Mono',monospace;
136
+ font-size:0.65rem; letter-spacing:0.12em; text-transform:uppercase;
137
+ color:var(--accent); margin-bottom:18px;
 
 
 
 
 
138
  }
139
  .eyebrow-dot {
140
+ width:5px; height:5px;
141
+ background:var(--accent); border-radius:50%;
142
+ animation:blink 2.4s ease-in-out infinite; flex-shrink:0;
 
 
 
 
143
  }
144
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.25} }
145
 
146
  .hero-title {
147
+ font-family:'Instrument Serif',serif;
148
+ font-size:clamp(2rem,5vw,3.8rem);
149
+ font-weight:400; line-height:1.1;
150
+ letter-spacing:-0.025em; color:var(--text); margin-bottom:16px;
 
 
 
 
 
 
 
151
  }
152
+ .hero-title em { font-style:italic; color:var(--accent); }
153
 
154
  .hero-sub {
155
+ font-size:0.95rem; color:var(--text-sec);
156
+ line-height:1.72; max-width:420px; margin-bottom:30px;
 
 
 
157
  }
158
 
159
  .hero-stats {
160
+ display:flex; gap:28px; flex-wrap:wrap;
161
+ padding-top:24px; border-top:1px solid var(--border);
 
 
 
 
 
 
 
 
 
162
  }
163
+ .stat-val { font-family:'Instrument Serif',serif; font-size:1.6rem; color:var(--text); margin-bottom:4px; }
164
  .stat-label {
165
+ font-family:'JetBrains Mono',monospace;
166
+ font-size:0.6rem; letter-spacing:0.1em; text-transform:uppercase; color:var(--text-muted);
 
 
 
167
  }
168
 
 
169
  .hero-grid {
170
+ display:grid;
171
+ grid-template-columns:repeat(5,46px);
172
+ grid-template-rows:repeat(2,46px);
173
+ gap:6px; flex-shrink:0;
 
174
  }
175
  .hg-cell {
176
+ width:46px; height:46px; border-radius:9px;
177
+ border:1px solid var(--border2); background:var(--surface);
178
+ display:grid; place-items:center;
179
+ font-family:'Instrument Serif',serif; font-size:1.3rem; color:var(--text-muted);
180
+ transition:all 0.4s var(--ease); user-select:none;
 
 
 
 
 
 
181
  }
182
  .hg-cell.lit {
183
+ border-color:rgba(168,240,160,0.3); background:var(--accent-dim);
184
+ color:var(--accent); box-shadow:0 0 14px rgba(168,240,160,0.12);
 
 
185
  }
186
 
187
+ /* ══ DIVIDER ══ */
 
 
188
  .section-divider {
189
+ position:relative; z-index:1;
190
+ max-width:1080px; margin:0 auto 16px;
191
+ padding:0 20px;
192
+ display:flex; align-items:center; gap:14px;
 
 
 
 
193
  }
194
+ .div-line { flex:1; height:1px; background:var(--border); }
195
  .div-label {
196
+ font-family:'JetBrains Mono',monospace;
197
+ font-size:0.6rem; letter-spacing:0.12em; text-transform:uppercase;
198
+ color:var(--text-muted); white-space:nowrap;
 
 
 
199
  }
200
 
201
+ /* ══ APP CARD ══ */
 
 
202
  .app-wrap {
203
+ position:relative; z-index:1;
204
+ max-width:1080px; margin:0 auto 80px;
205
+ padding:0 20px;
 
 
206
  }
 
207
  .app-card {
208
+ background:var(--surface);
209
+ border:1px solid var(--border2);
210
+ border-radius:20px; overflow:hidden;
 
211
  box-shadow:
212
  0 0 0 1px rgba(255,255,255,0.025),
213
+ 0 24px 64px rgba(0,0,0,0.5),
214
+ 0 0 100px rgba(168,240,160,0.03);
215
  }
216
 
217
+ /* title bar */
218
  .card-bar {
219
+ height:42px; background:var(--bg2);
220
+ border-bottom:1px solid var(--border);
221
+ display:flex; align-items:center;
222
+ justify-content:space-between;
223
+ padding:0 16px; gap:12px;
224
+ }
225
+ .bar-dots { display:flex; gap:6px; align-items:center; }
226
+ .bar-dot { width:10px; height:10px; border-radius:50%; }
227
+ .dot-r { background:#ff5f57; }
228
+ .dot-y { background:#ffbd2e; }
229
+ .dot-g { background:#29c940; }
 
 
 
 
 
 
 
230
  .bar-title {
231
+ font-family:'JetBrains Mono',monospace;
232
+ font-size:0.65rem; letter-spacing:0.06em; color:var(--text-muted);
233
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
 
234
  }
235
  .bar-status {
236
+ display:flex; align-items:center; gap:6px;
237
+ font-family:'JetBrains Mono',monospace;
238
+ font-size:0.62rem; color:var(--accent); letter-spacing:0.05em;
239
+ flex-shrink:0; white-space:nowrap;
 
 
 
240
  }
241
  .bar-status-dot {
242
+ width:5px; height:5px; background:var(--accent); border-radius:50%;
243
+ animation:blink 2.4s ease-in-out infinite;
 
 
244
  }
245
 
246
+ /* two-col desktop layout */
247
  .app-grid {
248
+ display:grid;
249
+ grid-template-columns:300px 1fr;
250
  }
251
 
252
  /* ── LEFT PANE ── */
253
+ .left-pane { border-right:1px solid var(--border); display:flex; flex-direction:column; }
254
+ .pane-head { padding:14px 16px 12px; border-bottom:1px solid var(--border); }
 
 
 
 
 
 
 
255
  .pane-eyebrow {
256
+ font-family:'JetBrains Mono',monospace;
257
+ font-size:0.58rem; letter-spacing:0.14em; text-transform:uppercase;
258
+ color:var(--text-muted); margin-bottom:2px;
 
 
 
 
 
 
 
 
259
  }
260
+ .pane-name { font-size:0.85rem; font-weight:600; color:var(--text); }
261
 
262
  .canvas-wrap {
263
+ flex:1; padding:14px;
264
+ display:flex; flex-direction:column; gap:10px;
265
+ background:var(--bg2);
 
 
 
266
  }
267
 
 
268
  #draw-canvas {
269
+ width:100%;
270
+ aspect-ratio:1/1;
271
+ border-radius:10px;
272
+ border:1px solid var(--border2);
273
+ background:#070908;
274
+ cursor:crosshair;
275
+ display:block;
276
+ touch-action:none; /* stops page scroll on touch */
277
+ -webkit-touch-callout:none; /* no iOS long-press popup */
278
+ -webkit-user-select:none;
279
+ user-select:none;
280
  }
281
 
282
  .canvas-hint {
283
+ display:flex; align-items:center; gap:8px;
284
+ font-family:'JetBrains Mono',monospace;
285
+ font-size:0.6rem; letter-spacing:0.05em; color:var(--text-muted);
286
+ background:var(--surface); border:1px solid var(--border);
287
+ border-radius:var(--radius-xs); padding:7px 10px;
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
+ .hint-icon { opacity:0.5; }
290
+
291
+ .btn-row { display:flex; gap:8px; }
292
  .btn {
293
+ font-family:'Instrument Sans',sans-serif;
294
+ font-size:0.84rem; font-weight:600;
295
+ border-radius:var(--radius-xs);
296
+ height:46px; /* tall = good mobile tap target */
297
+ border:none; cursor:pointer;
298
+ transition:all 0.18s var(--ease);
299
+ display:flex; align-items:center; justify-content:center; gap:6px;
300
+ -webkit-tap-highlight-color:transparent;
 
 
 
 
 
301
  }
302
  .btn-primary {
303
+ flex:1;
304
+ background:var(--accent); color:#060907;
305
+ box-shadow:0 0 20px rgba(168,240,160,0.15);
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
+ .btn-primary:hover { background:#bdf6b5; box-shadow:0 0 28px rgba(168,240,160,0.28); transform:translateY(-1px); }
308
+ .btn-primary:active { transform:translateY(0); box-shadow:none; }
309
+ .btn-primary:disabled { opacity:0.45; cursor:not-allowed; transform:none; box-shadow:none; }
310
 
311
  .btn-secondary {
312
+ width:46px;
313
+ background:var(--surface); color:var(--text-sec);
314
+ border:1px solid var(--border2);
 
 
 
 
 
 
315
  }
316
+ .btn-secondary:hover { background:var(--surface2); color:var(--text); border-color:var(--border3); }
317
+ .btn-secondary:active { background:var(--surface3); }
318
 
319
  /* ── RIGHT PANE ── */
320
+ .right-pane { display:flex; flex-direction:column; min-height:480px; }
321
+ #results-area { flex:1; display:flex; flex-direction:column; }
 
 
 
 
 
 
 
 
 
322
 
 
323
  .empty-state {
324
+ flex:1; display:flex; flex-direction:column;
325
+ align-items:center; justify-content:center;
326
+ gap:10px; padding:48px 24px; text-align:center;
 
 
 
 
 
327
  }
328
  .empty-icon {
329
+ width:52px; height:52px; border-radius:13px;
330
+ border:1px solid var(--border2); background:var(--surface2);
331
+ display:grid; place-items:center; font-size:1.3rem; margin-bottom:4px;
 
 
 
 
 
 
 
 
 
 
332
  }
333
+ .empty-title { font-size:0.82rem; font-weight:600; color:var(--text-sec); }
334
  .empty-sub {
335
+ font-family:'JetBrains Mono',monospace;
336
+ font-size:0.63rem; letter-spacing:0.06em; color:var(--text-muted); line-height:1.7;
337
+ }
338
+
339
+ .loading-state {
340
+ flex:1; display:flex; flex-direction:column;
341
+ align-items:center; justify-content:center; gap:14px; padding:60px;
342
+ }
343
+ .spinner {
344
+ width:30px; height:30px;
345
+ border:2px solid var(--border2); border-top-color:var(--accent);
346
+ border-radius:50%; animation:spin 0.7s linear infinite;
347
+ }
348
+ @keyframes spin { to{transform:rotate(360deg)} }
349
+ .loading-text {
350
+ font-family:'JetBrains Mono',monospace;
351
+ font-size:0.65rem; letter-spacing:0.1em; text-transform:uppercase; color:var(--text-muted);
352
  }
353
 
 
354
  .results-content {
355
+ padding:20px 22px; display:flex; flex-direction:column; gap:20px;
356
+ animation:fadeIn 0.28s var(--ease);
 
 
 
357
  }
358
+ @keyframes fadeIn { from{opacity:0;transform:translateY(5px)} to{opacity:1;transform:none} }
359
 
 
360
  .pred-hero {
361
+ display:flex; align-items:center; gap:18px;
362
+ padding-bottom:18px; border-bottom:1px solid var(--border);
 
 
 
363
  }
364
  .pred-digit {
365
+ width:82px; height:82px; border-radius:14px;
366
+ background:var(--accent-dim); border:1px solid rgba(168,240,160,0.18);
367
+ display:grid; place-items:center;
368
+ font-family:'Instrument Serif',serif; font-size:3.5rem; color:var(--accent);
369
+ flex-shrink:0;
370
+ box-shadow:0 0 36px rgba(168,240,160,0.09),inset 0 1px 0 rgba(255,255,255,0.04);
 
 
 
 
 
 
 
 
 
371
  }
 
 
 
 
 
 
 
372
  .pred-info { flex:1; min-width:0; }
373
  .pred-label {
374
+ font-family:'JetBrains Mono',monospace;
375
+ font-size:0.58rem; letter-spacing:0.14em; text-transform:uppercase;
376
+ color:var(--text-muted); margin-bottom:5px;
 
 
 
377
  }
378
  .pred-conf {
379
+ font-family:'Instrument Serif',serif; font-size:2rem; color:var(--text);
380
+ line-height:1; margin-bottom:10px;
 
 
 
381
  }
382
  .pred-conf small {
383
+ font-family:'Instrument Sans',sans-serif; font-size:0.95rem;
384
+ font-weight:400; color:var(--text-sec);
 
 
385
  }
386
  .conf-track {
387
+ height:3px; background:var(--surface3); border-radius:2px;
388
+ overflow:hidden; margin-bottom:5px;
 
 
 
389
  }
390
  .conf-fill {
391
+ height:100%;
392
+ background:linear-gradient(90deg,var(--accent2),var(--accent));
393
+ border-radius:2px; transition:width 0.55s var(--ease);
 
394
  }
395
  .conf-range {
396
+ display:flex; justify-content:space-between;
397
+ font-family:'JetBrains Mono',monospace; font-size:0.55rem; color:var(--text-muted);
 
 
 
 
398
  }
399
 
 
 
400
  .probs-label {
401
+ font-family:'JetBrains Mono',monospace;
402
+ font-size:0.58rem; letter-spacing:0.14em; text-transform:uppercase;
403
+ color:var(--text-muted); margin-bottom:10px;
 
 
 
 
 
 
 
 
404
  }
405
+ .probs-grid { display:flex; flex-direction:column; gap:5px; }
406
  .prob-row {
407
+ display:grid;
408
+ grid-template-columns:18px 1fr 54px;
409
+ align-items:center; gap:10px;
410
+ padding:6px 9px; border-radius:var(--radius-xs);
411
+ transition:background 0.15s;
412
+ }
413
+ .prob-row:hover { background:var(--surface2); }
414
+ .prob-row.is-top { background:var(--accent-dim); border:1px solid rgba(168,240,160,0.12); }
415
+ .prob-num { font-family:'JetBrains Mono',monospace; font-size:0.7rem; color:var(--text-muted); text-align:right; }
416
+ .prob-row.is-top .prob-num { color:var(--accent); font-weight:500; }
417
+ .prob-bar-track { height:4px; background:var(--surface3); border-radius:2px; overflow:hidden; border:1px solid var(--border); }
418
+ .prob-bar-fill { height:100%; background:var(--surface3); border-radius:2px; transition:width 0.55s var(--ease); }
419
+ .prob-row.is-top .prob-bar-fill { background:linear-gradient(90deg,var(--accent2),var(--accent)); }
420
+ .prob-pct { font-family:'JetBrains Mono',monospace; font-size:0.65rem; color:var(--text-muted); text-align:right; white-space:nowrap; }
421
+ .prob-row.is-top .prob-pct { color:var(--accent); font-weight:500; }
422
+
423
+ /* ══ FOOTER ══ */
424
+ footer {
425
+ position:relative; z-index:1;
426
+ border-top:1px solid var(--border);
427
+ padding:18px 20px;
428
+ display:flex; align-items:center;
429
+ justify-content:space-between;
430
+ flex-wrap:wrap; gap:8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  }
432
+ .footer-left { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
433
+ .footer-logo { font-weight:600; font-size:0.8rem; color:var(--text-sec); }
434
+ .footer-sep { color:var(--text-muted); }
435
+ .footer-note, .footer-right {
436
+ font-family:'JetBrains Mono',monospace;
437
+ font-size:0.6rem; letter-spacing:0.07em; color:var(--text-muted);
 
438
  }
439
 
440
  /* ══════════════════════════════════════
441
+ MOBILE ≀ 680px
442
  ══════════════════════════════════════ */
443
+ @media (max-width: 680px) {
444
+
445
+ /* Nav β€” hide tabs & pill, keep logo + github */
446
+ .nav-center, .nav-pill { display:none; }
447
+ nav { padding:0 16px; }
448
+
449
+ /* Hero β€” single column */
450
+ .hero {
451
+ grid-template-columns:1fr;
452
+ padding:80px 16px 36px;
453
+ gap:24px;
454
+ }
455
+ .hero-grid { display:none; } /* decorative only, hide on mobile */
456
+ .hero-title { font-size:clamp(1.9rem,8vw,2.6rem); }
457
+ .hero-sub { font-size:0.88rem; max-width:100%; }
458
+
459
+ .section-divider { padding:0 12px; }
460
+
461
+ /* App card */
462
+ .app-wrap { padding:0 12px; margin-bottom:60px; }
463
+
464
+ /* Stack panes vertically β€” canvas on top, results below */
465
+ .app-grid { grid-template-columns:1fr; }
466
+ .left-pane { border-right:none; border-bottom:1px solid var(--border); }
467
+
468
+ /* Canvas: centred, max size that fits comfortably */
469
+ .canvas-wrap { padding:12px; }
470
+ #draw-canvas {
471
+ max-width:min(340px, 100%);
472
+ margin:0 auto;
473
+ }
474
+
475
+ /* Bigger tap targets */
476
+ .btn { height:50px; font-size:0.9rem; }
477
+ .btn-secondary { width:50px; }
478
+
479
+ /* Results */
480
+ .results-content { padding:14px 14px; gap:16px; }
481
+ .pred-digit { width:64px; height:64px; font-size:2.8rem; border-radius:11px; }
482
+ .pred-conf { font-size:1.6rem; }
483
+ .right-pane { min-height:unset; }
484
+
485
+ .footer-right { display:none; }
486
+ .bar-title { display:none; } /* too cramped on mobile title bar */
487
  }
488
+
489
+ /* ══════════════════════════════════════
490
+ TABLET 681px – 900px
491
+ ══════════════════════════════════════ */
492
+ @media (min-width:681px) and (max-width:900px) {
493
+ .nav-center { display:none; }
494
+ .hero { padding:100px 24px 48px; gap:32px; }
495
+ .hero-grid {
496
+ grid-template-columns:repeat(5,40px);
497
+ grid-template-rows:repeat(2,40px);
498
+ gap:5px;
499
+ }
500
+ .hg-cell { width:40px; height:40px; font-size:1.1rem; }
501
+ .app-wrap { padding:0 16px; }
502
+ .app-grid { grid-template-columns:260px 1fr; }
 
 
 
 
 
 
 
 
503
  }