pitangent commited on
Commit
993038e
·
verified ·
1 Parent(s): 603a2e5

feat: implement minimalist Sudoku game with board generation and difficulty levels

Browse files
Files changed (3) hide show
  1. index.html +52 -18
  2. script.js +379 -0
  3. style.css +187 -17
index.html CHANGED
@@ -1,19 +1,53 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sudoku</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Source+Sans+3:wght@400;600&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <div class="app-container">
14
+ <header>
15
+ <h1>Sudoku</h1>
16
+ <div class="controls-row">
17
+ <div class="difficulty-selector">
18
+ <button class="diff-btn active" data-diff="easy">Easy</button>
19
+ <button class="diff-btn" data-diff="medium">Medium</button>
20
+ <button class="diff-btn" data-diff="hard">Hard</button>
21
+ </div>
22
+ <div class="timer" id="timerDisplay">00:00</div>
23
+ </div>
24
+ </header>
25
+
26
+ <main>
27
+ <div class="sudoku-board" id="board">
28
+ <!-- Grid cells will be generated via JS -->
29
+ </div>
30
+ </main>
31
+
32
+ <div class="actions">
33
+ <button id="btnNewGame">New Game</button>
34
+ <button id="btnUndo" disabled>Undo</button>
35
+ <button id="btnNotes" class="toggle-btn">Notes: Off</button>
36
+ </div>
37
+
38
+ <div class="numpad">
39
+ <button class="num-btn">1</button>
40
+ <button class="num-btn">2</button>
41
+ <button class="num-btn">3</button>
42
+ <button class="num-btn">4</button>
43
+ <button class="num-btn">5</button>
44
+ <button class="num-btn">6</button>
45
+ <button class="num-btn">7</button>
46
+ <button class="num-btn">8</button>
47
+ <button class="num-btn">9</button>
48
+ <button class="num-btn clear-btn">Clear</button>
49
+ </div>
50
+ </div>
51
+ <script src="script.js"></script>
52
+ </body>
53
  </html>
script.js ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const BOARD_SIZE = 9;
2
+ const BOX_SIZE = 3;
3
+
4
+ // True RNG Utilities
5
+ function getTrueRandomInt(max) {
6
+ const randomBuffer = new Uint32Array(1);
7
+ window.crypto.getRandomValues(randomBuffer);
8
+ return randomBuffer[0] % max;
9
+ }
10
+
11
+ function shuffleArray(array) {
12
+ for (let i = array.length - 1; i > 0; i--) {
13
+ const j = getTrueRandomInt(i + 1);
14
+ [array[i], array[j]] = [array[j], array[i]];
15
+ }
16
+ }
17
+
18
+ // Game State
19
+ let solutionBoard = [];
20
+ let initialBoard = [];
21
+ let currentBoard = [];
22
+ let notesBoard = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => []));
23
+ let history = []; // For undo
24
+ let selectedCell = null;
25
+ let notesMode = false;
26
+ let difficulty = 'easy'; // easy, medium, hard
27
+ let timerInterval = null;
28
+ let secondsElapsed = 0;
29
+
30
+ // Sudoku Core Logic
31
+ function getEmptyBoard() {
32
+ return Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(0));
33
+ }
34
+
35
+ function isValid(board, row, col, num) {
36
+ for (let x = 0; x < BOARD_SIZE; x++) {
37
+ if (board[row][x] === num) return false;
38
+ }
39
+ for (let x = 0; x < BOARD_SIZE; x++) {
40
+ if (board[x][col] === num) return false;
41
+ }
42
+ let startRow = row - row % BOX_SIZE, startCol = col - col % BOX_SIZE;
43
+ for (let i = 0; i < BOX_SIZE; i++) {
44
+ for (let j = 0; j < BOX_SIZE; j++) {
45
+ if (board[i + startRow][j + startCol] === num) return false;
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+
51
+ function fillBoard(board) {
52
+ for (let row = 0; row < BOARD_SIZE; row++) {
53
+ for (let col = 0; col < BOARD_SIZE; col++) {
54
+ if (board[row][col] === 0) {
55
+ let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];
56
+ shuffleArray(nums);
57
+ for (let num of nums) {
58
+ if (isValid(board, row, col, num)) {
59
+ board[row][col] = num;
60
+ if (fillBoard(board)) {
61
+ return true;
62
+ }
63
+ board[row][col] = 0;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+
73
+ function solveBoardCount(board, count = { value: 0 }) {
74
+ let row = -1;
75
+ let col = -1;
76
+ let isEmpty = false;
77
+ for (let i = 0; i < BOARD_SIZE; i++) {
78
+ for (let j = 0; j < BOARD_SIZE; j++) {
79
+ if (board[i][j] === 0) {
80
+ row = i;
81
+ col = j;
82
+ isEmpty = true;
83
+ break;
84
+ }
85
+ }
86
+ if (isEmpty) break;
87
+ }
88
+
89
+ if (!isEmpty) {
90
+ count.value++;
91
+ return;
92
+ }
93
+
94
+ for (let num = 1; num <= 9; num++) {
95
+ if (isValid(board, row, col, num)) {
96
+ board[row][col] = num;
97
+ solveBoardCount(board, count);
98
+ board[row][col] = 0;
99
+ }
100
+ if (count.value > 1) return; // Fast exit if more than 1 solution
101
+ }
102
+ }
103
+
104
+ function generatePuzzle(difficulty) {
105
+ let board = getEmptyBoard();
106
+ fillBoard(board);
107
+ solutionBoard = board.map(row => [...row]);
108
+
109
+ let cellsToRemove = 40; // Default easy
110
+ if (difficulty === 'medium') cellsToRemove = 50;
111
+ if (difficulty === 'hard') cellsToRemove = 60;
112
+
113
+ let cells = [];
114
+ for (let r = 0; r < BOARD_SIZE; r++) {
115
+ for (let c = 0; c < BOARD_SIZE; c++) {
116
+ cells.push({ r, c });
117
+ }
118
+ }
119
+ shuffleArray(cells);
120
+
121
+ let puzzle = solutionBoard.map(row => [...row]);
122
+ let removedCount = 0;
123
+
124
+ for (let cell of cells) {
125
+ if (removedCount >= cellsToRemove) break;
126
+ let temp = puzzle[cell.r][cell.c];
127
+ puzzle[cell.r][cell.c] = 0;
128
+
129
+ let count = { value: 0 };
130
+ let copy = puzzle.map(row => [...row]);
131
+ solveBoardCount(copy, count);
132
+
133
+ if (count.value !== 1) {
134
+ puzzle[cell.r][cell.c] = temp; // Put it back
135
+ } else {
136
+ removedCount++;
137
+ }
138
+ }
139
+
140
+ return puzzle;
141
+ }
142
+
143
+ // Timer Logic
144
+ function startTimer() {
145
+ clearInterval(timerInterval);
146
+ secondsElapsed = 0;
147
+ updateTimerDisplay();
148
+ timerInterval = setInterval(() => {
149
+ secondsElapsed++;
150
+ updateTimerDisplay();
151
+ }, 1000);
152
+ }
153
+
154
+ function updateTimerDisplay() {
155
+ let m = Math.floor(secondsElapsed / 60).toString().padStart(2, '0');
156
+ let s = (secondsElapsed % 60).toString().padStart(2, '0');
157
+ document.getElementById('timerDisplay').innerText = `${m}:${s}`;
158
+ }
159
+
160
+ function stopTimer() {
161
+ clearInterval(timerInterval);
162
+ }
163
+
164
+ // UI Initialization
165
+ function initGame() {
166
+ history = [];
167
+ selectedCell = null;
168
+ notesBoard = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => []));
169
+ updateUndoBtn();
170
+
171
+ currentBoard = generatePuzzle(difficulty);
172
+ initialBoard = currentBoard.map(row => [...row]);
173
+
174
+ renderBoard();
175
+ startTimer();
176
+ }
177
+
178
+ function renderBoard() {
179
+ const boardEl = document.getElementById('board');
180
+ boardEl.innerHTML = '';
181
+
182
+ for (let r = 0; r < BOARD_SIZE; r++) {
183
+ for (let c = 0; c < BOARD_SIZE; c++) {
184
+ const cell = document.createElement('div');
185
+ cell.classList.add('cell');
186
+ cell.dataset.r = r;
187
+ cell.dataset.c = c;
188
+
189
+ if (initialBoard[r][c] !== 0) {
190
+ cell.classList.add('fixed');
191
+ cell.innerText = initialBoard[r][c];
192
+ } else if (currentBoard[r][c] !== 0) {
193
+ cell.classList.add('user-input');
194
+ cell.innerText = currentBoard[r][c];
195
+ // Check if it's an error relative to solution board
196
+ if (currentBoard[r][c] !== solutionBoard[r][c]) {
197
+ cell.classList.add('error');
198
+ }
199
+ } else {
200
+ // Render Notes
201
+ if (notesBoard[r][c].length > 0) {
202
+ const notesGrid = document.createElement('div');
203
+ notesGrid.classList.add('notes-grid');
204
+ for (let i = 1; i <= 9; i++) {
205
+ const noteNum = document.createElement('div');
206
+ noteNum.classList.add('note-num');
207
+ if (notesBoard[r][c].includes(i)) {
208
+ noteNum.innerText = i;
209
+ }
210
+ notesGrid.appendChild(noteNum);
211
+ }
212
+ cell.appendChild(notesGrid);
213
+ }
214
+ }
215
+
216
+ if (selectedCell && selectedCell.r === r && selectedCell.c === c) {
217
+ cell.classList.add('selected');
218
+ } else if (selectedCell && currentBoard[selectedCell.r][selectedCell.c] !== 0 && currentBoard[r][c] === currentBoard[selectedCell.r][selectedCell.c]) {
219
+ cell.classList.add('highlight');
220
+ } else if (selectedCell && initialBoard[selectedCell.r][selectedCell.c] !== 0 && initialBoard[r][c] === initialBoard[selectedCell.r][selectedCell.c]) {
221
+ cell.classList.add('highlight');
222
+ }
223
+
224
+ cell.addEventListener('click', () => selectCell(r, c));
225
+ boardEl.appendChild(cell);
226
+ }
227
+ }
228
+ }
229
+
230
+ function selectCell(r, c) {
231
+ if (selectedCell && selectedCell.r === r && selectedCell.c === c) {
232
+ selectedCell = null;
233
+ } else {
234
+ selectedCell = { r, c };
235
+ }
236
+ renderBoard();
237
+ }
238
+
239
+ // Interaction
240
+ function handleInput(num) {
241
+ if (!selectedCell) return;
242
+ const { r, c } = selectedCell;
243
+
244
+ if (initialBoard[r][c] !== 0) return; // Cannot edit fixed cells
245
+
246
+ saveHistory();
247
+
248
+ if (num === 0) {
249
+ currentBoard[r][c] = 0;
250
+ } else if (notesMode) {
251
+ if (currentBoard[r][c] === 0) {
252
+ const idx = notesBoard[r][c].indexOf(num);
253
+ if (idx > -1) {
254
+ notesBoard[r][c].splice(idx, 1);
255
+ } else {
256
+ notesBoard[r][c].push(num);
257
+ notesBoard[r][c].sort((a, b) => a - b);
258
+ }
259
+ }
260
+ } else {
261
+ currentBoard[r][c] = num;
262
+ notesBoard[r][c] = []; // Clear notes when filled
263
+ clearRelatedNotes(r, c, num);
264
+ }
265
+
266
+ renderBoard();
267
+ checkWin();
268
+ }
269
+
270
+ function clearRelatedNotes(r, c, num) {
271
+ for (let i = 0; i < BOARD_SIZE; i++) {
272
+ removeNote(r, i, num);
273
+ removeNote(i, c, num);
274
+ }
275
+ let startR = r - r % BOX_SIZE;
276
+ let startC = c - c % BOX_SIZE;
277
+ for (let i = 0; i < BOX_SIZE; i++) {
278
+ for (let j = 0; j < BOX_SIZE; j++) {
279
+ removeNote(startR + i, startC + j, num);
280
+ }
281
+ }
282
+ }
283
+
284
+ function removeNote(r, c, num) {
285
+ const idx = notesBoard[r][c].indexOf(num);
286
+ if (idx > -1) notesBoard[r][c].splice(idx, 1);
287
+ }
288
+
289
+ // History & Undo
290
+ function saveHistory() {
291
+ history.push({
292
+ board: currentBoard.map(row => [...row]),
293
+ notes: notesBoard.map(row => row.map(cellNotes => [...cellNotes]))
294
+ });
295
+ updateUndoBtn();
296
+ }
297
+
298
+ function undo() {
299
+ if (history.length === 0) return;
300
+ const lastState = history.pop();
301
+ currentBoard = lastState.board;
302
+ notesBoard = lastState.notes;
303
+ updateUndoBtn();
304
+ renderBoard();
305
+ }
306
+
307
+ function updateUndoBtn() {
308
+ document.getElementById('btnUndo').disabled = history.length === 0;
309
+ }
310
+
311
+ // Win Check
312
+ function checkWin() {
313
+ let complete = true;
314
+ for (let r = 0; r < BOARD_SIZE; r++) {
315
+ for (let c = 0; c < BOARD_SIZE; c++) {
316
+ if (currentBoard[r][c] === 0 || currentBoard[r][c] !== solutionBoard[r][c]) {
317
+ complete = false;
318
+ break;
319
+ }
320
+ }
321
+ }
322
+ if (complete) {
323
+ stopTimer();
324
+ setTimeout(() => alert('Congratulations! You solved the Sudoku!'), 100);
325
+ }
326
+ }
327
+
328
+ // Event Listeners
329
+ document.getElementById('btnNewGame').addEventListener('click', initGame);
330
+
331
+ document.getElementById('btnUndo').addEventListener('click', undo);
332
+
333
+ document.getElementById('btnNotes').addEventListener('click', (e) => {
334
+ notesMode = !notesMode;
335
+ e.target.innerText = `Notes: ${notesMode ? 'On' : 'Off'}`;
336
+ e.target.classList.toggle('active', notesMode);
337
+ });
338
+
339
+ document.querySelectorAll('.num-btn').forEach(btn => {
340
+ btn.addEventListener('click', (e) => {
341
+ const text = e.target.innerText;
342
+ if (text === 'Clear') {
343
+ handleInput(0);
344
+ } else {
345
+ handleInput(parseInt(text));
346
+ }
347
+ });
348
+ });
349
+
350
+ document.querySelectorAll('.diff-btn').forEach(btn => {
351
+ btn.addEventListener('click', (e) => {
352
+ document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
353
+ e.target.classList.add('active');
354
+ difficulty = e.target.dataset.diff;
355
+ initGame();
356
+ });
357
+ });
358
+
359
+ document.addEventListener('keydown', (e) => {
360
+ if (!selectedCell) return;
361
+ if (e.key >= '1' && e.key <= '9') {
362
+ handleInput(parseInt(e.key));
363
+ } else if (e.key === 'Backspace' || e.key === 'Delete') {
364
+ handleInput(0);
365
+ } else if (e.key === 'n' || e.key === 'N') {
366
+ document.getElementById('btnNotes').click();
367
+ } else if (e.key === 'ArrowUp') {
368
+ if (selectedCell.r > 0) selectCell(selectedCell.r - 1, selectedCell.c);
369
+ } else if (e.key === 'ArrowDown') {
370
+ if (selectedCell.r < BOARD_SIZE - 1) selectCell(selectedCell.r + 1, selectedCell.c);
371
+ } else if (e.key === 'ArrowLeft') {
372
+ if (selectedCell.c > 0) selectCell(selectedCell.r, selectedCell.c - 1);
373
+ } else if (e.key === 'ArrowRight') {
374
+ if (selectedCell.c < BOARD_SIZE - 1) selectCell(selectedCell.r, selectedCell.c + 1);
375
+ }
376
+ });
377
+
378
+ // Start Game
379
+ initGame();
style.css CHANGED
@@ -1,28 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
  h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
28
  }
 
1
+ :root {
2
+ --bg-color: #fdfaf6; /* Warm off-white */
3
+ --text-primary: #3d2b1f; /* Dark brown */
4
+ --text-secondary: #5c4a3d;
5
+ --border-color: #d4c4b7;
6
+ --border-thick: #8a7360;
7
+ --cell-bg: #fffcf8;
8
+ --cell-bg-selected: #e9dec9;
9
+ --cell-bg-highlight: #f2e9d8;
10
+ --cell-bg-error: #f8dcd2;
11
+ --text-error: #a83232;
12
+ --btn-bg: #eae0d5;
13
+ --btn-hover: #dfcebd;
14
+ --font-heading: 'Libre Baskerville', serif;
15
+ --font-ui: 'Source Sans 3', sans-serif;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ }
23
+
24
  body {
25
+ background-color: var(--bg-color);
26
+ color: var(--text-primary);
27
+ font-family: var(--font-ui);
28
+ display: flex;
29
+ justify-content: center;
30
+ align-items: center;
31
+ min-height: 100vh;
32
+ padding: 1rem;
33
+ }
34
+
35
+ .app-container {
36
+ max-width: 500px;
37
+ width: 100%;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 1.5rem;
41
+ }
42
+
43
+ header {
44
+ text-align: center;
45
  }
46
 
47
  h1 {
48
+ font-family: var(--font-heading);
49
+ font-size: 2.5rem;
50
+ margin-bottom: 1rem;
51
+ color: var(--text-primary);
52
+ }
53
+
54
+ .controls-row {
55
+ display: flex;
56
+ justify-content: space-between;
57
+ align-items: center;
58
+ margin-bottom: 0.5rem;
59
+ }
60
+
61
+ .difficulty-selector {
62
+ display: flex;
63
+ gap: 0.5rem;
64
+ }
65
+
66
+ .timer {
67
+ font-family: var(--font-heading);
68
+ font-size: 1.2rem;
69
+ font-weight: bold;
70
+ color: var(--text-secondary);
71
+ }
72
+
73
+ button {
74
+ font-family: var(--font-ui);
75
+ background-color: var(--btn-bg);
76
+ color: var(--text-primary);
77
+ border: 1px solid var(--border-color);
78
+ padding: 0.5rem 1rem;
79
+ border-radius: 4px;
80
+ cursor: pointer;
81
+ font-size: 1rem;
82
+ transition: background-color 0.2s, transform 0.1s;
83
+ }
84
+
85
+ button:hover:not(:disabled) {
86
+ background-color: var(--btn-hover);
87
+ }
88
+
89
+ button:active:not(:disabled) {
90
+ transform: scale(0.98);
91
+ }
92
+
93
+ button:disabled {
94
+ opacity: 0.5;
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ .diff-btn.active {
99
+ background-color: var(--border-thick);
100
+ color: var(--bg-color);
101
+ border-color: var(--border-thick);
102
+ }
103
+
104
+ .sudoku-board {
105
+ display: grid;
106
+ grid-template-columns: repeat(9, 1fr);
107
+ grid-template-rows: repeat(9, 1fr);
108
+ width: 100%;
109
+ aspect-ratio: 1 / 1;
110
+ border: 2px solid var(--border-thick);
111
+ background-color: var(--border-color); /* Grid lines color */
112
+ gap: 1px;
113
+ }
114
+
115
+ .cell {
116
+ background-color: var(--cell-bg);
117
+ display: flex;
118
+ justify-content: center;
119
+ align-items: center;
120
+ font-family: var(--font-heading);
121
+ font-size: clamp(1.2rem, 5vw, 2rem);
122
+ cursor: pointer;
123
+ position: relative;
124
+ user-select: none;
125
+ color: var(--text-primary);
126
+ }
127
+
128
+ /* Thicker borders for 3x3 boxes */
129
+ .cell:nth-child(3n) { border-right: 2px solid var(--border-thick); }
130
+ .cell:nth-child(9n) { border-right: none; }
131
+ .cell:nth-child(n+19):nth-child(-n+27),
132
+ .cell:nth-child(n+46):nth-child(-n+54) {
133
+ border-bottom: 2px solid var(--border-thick);
134
+ }
135
+
136
+ .cell.selected { background-color: var(--cell-bg-selected); }
137
+ .cell.highlight { background-color: var(--cell-bg-highlight); }
138
+ .cell.error { color: var(--text-error); background-color: var(--cell-bg-error); }
139
+ .cell.fixed { font-weight: bold; color: var(--text-primary); }
140
+ .cell.user-input { color: var(--text-secondary); }
141
+
142
+ .notes-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(3, 1fr);
145
+ grid-template-rows: repeat(3, 1fr);
146
+ width: 100%;
147
+ height: 100%;
148
+ padding: 2px;
149
+ }
150
+
151
+ .note-num {
152
+ font-family: var(--font-ui);
153
+ font-size: clamp(0.5rem, 2vw, 0.7rem);
154
+ color: var(--text-secondary);
155
+ display: flex;
156
+ justify-content: center;
157
+ align-items: center;
158
+ line-height: 1;
159
+ }
160
+
161
+ .actions {
162
+ display: flex;
163
+ justify-content: space-between;
164
+ gap: 0.5rem;
165
+ }
166
+
167
+ .actions button {
168
+ flex: 1;
169
+ }
170
+
171
+ .toggle-btn.active {
172
+ background-color: var(--border-thick);
173
+ color: var(--bg-color);
174
+ }
175
+
176
+ .numpad {
177
+ display: grid;
178
+ grid-template-columns: repeat(5, 1fr);
179
+ gap: 0.5rem;
180
  }
181
 
182
+ .num-btn {
183
+ font-family: var(--font-heading);
184
+ font-size: 1.5rem;
185
+ padding: 0.75rem 0;
 
186
  }
187
 
188
+ .clear-btn {
189
+ grid-column: span 5;
190
+ font-family: var(--font-ui);
191
+ font-size: 1.2rem;
 
 
192
  }
193
 
194
+ @media (min-width: 600px) {
195
+ .app-container {
196
+ max-width: 600px;
197
+ }
198
  }