oneshotlm
Compare all models on this prompt →

Tetris · laguna-s-2.1

Output saved · Timed out Cost / run $0.0163 Runtime 900.1s

The 15-minute session limit cut this run off. The output is the HTML saved at that point.

Prompt for this run

Build a playable falling-block Tetris game on a canvas with a 10-column, 20-row visible board. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Include all seven tetrominoes with legal spawn positions. Use Left/Right to move, Up to rotate clockwise, Down to soft-drop, and Space to hard-drop. Show the controls. - Reject moves and rotations that overlap occupied cells or leave the board. Wall kicks are optional; if unsupported, reject the blocked rotation without changing the piece. - Lock landed pieces, clear complete rows, move rows above downward, and spawn the next piece. Display score, cleared-line count, level, and a next-piece preview. - Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level. Start at level 1 and increase gravity speed every ten cleared lines. - If a new piece cannot spawn, show Game Over. Provide Pause/Resume and Restart, with restart resetting the board, score, level, and piece sequence state. Hold and ghost pieces are optional. Verify: Move and rotate the first piece, then hard-drop it. Check that it locks into the board, a new piece spawns, and the next-piece preview advances. Pause and resume, then use Restart and check the cleared board, score, and level.

⛨ sandboxed Open in new tab ↗

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modellaguna-s-2.1
vendorpoolside
harnesspi 0.87.1 · browser iteration
generated2026-10-07

METRICS

runtime900.1s
tokens in438k
tokens out49k
cached393k
cost / run$0.0163
Transcript · 28 messages
system

user

Build a playable falling-block Tetris game on a canvas with a 10-column, 20-row visible board. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Include all seven tetrominoes with legal spawn positions. Use Left/Right to move, Up to rotate clockwise, Down to soft-drop, and Space to hard-drop. Show the controls. - Reject moves and rotations that overlap occupied cells or leave the board. Wall kicks are optional; if unsupported, reject the blocked rotation without changing the piece. - Lock landed pieces, clear complete rows, move rows above downward, and spawn the next piece. Display score, cleared-line count, level, and a next-piece preview. - Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level. Start at level 1 and increase gravity speed every ten cleared lines. - If a new piece cannot spawn, show Game Over. Provide Pause/Resume and Restart, with restart resetting the board, score, level, and piece sequence state. Hold and ghost pieces are optional. Verify: Move and rotate the first piece, then hard-drop it. Check that it locks into the board, a new piece spawns, and the next-piece preview advances. Pause and resume, then use Restart and check the cleared board, score, and level.

assistant
thinking
Let me build a Tetris game. I'll create a complete, self-contained index.html file with all the required features.

Let me plan the implementation:

1. Canvas-based 10-column, 20-row visible board
2. All 7 tetrominoes with legal spawn positions
3. Controls: Left/Right move, Up rotate clockwise, Down soft-drop, Space hard-drop
4. Show controls on screen
5. Reject moves/rotations that overlap or go off board
6. Lock landed pieces, clear complete rows, move rows down, spawn next piece
7. Score, cleared-line count, level, next-piece preview
8. Scoring: 100, 300, 500, 800 points per row cleared, multiplied by level
9. Level starts at 1, increases every 10 cleared lines, increasing gravity speed
10. Game Over when new piece can't spawn
11. Pause/Resume and Restart (resetting board, score, level, piece sequence)
12. Ghost piece (optional) - let me add it for polish
13. Hold piece (optional) - let me add it too

Let me write this carefully.

Tetrominoes - standard shapes:
- I: 4 in a row
- O: 2x2 square
- T: T-shape
- S: S-shape
- Z: Z-shape
- J: J-shape
- L: L-shape

Spawn position: typically at row 0 or 1, column 3-4 (centered horizontally)

Let me think about the board representation - a 2D array of 10x20 (or 10x22 to include the spawn area above visible board).

Actually, the standard Tetris guideline spawns pieces at column 3, rows 0-1 (above visible). Let me use a 10x20 visible board but allow spawning with the piece partially above the visible area.

Let me use a grid of 10 columns x 20 rows for the visible area. I'll represent pieces as arrays of (row, col) offsets from a reference point.

For the SRS (Super Rotation System) wall kicks - the task says wall kicks are optional, so I'll keep it simple but maybe add basic kicks. Actually, let me just do basic rotation without kicks, rejecting blocked rotations. But I should make sure pieces can rotate near walls reasonably. Let me keep it simple per the requirement - "if unsupported, reject the blocked rotation without changing the piece."

Actually, let me implement simple wall kicks for better gameplay - just try shifting left/right if the rotation is blocked. This makes the game more playable. But I need to be careful. Let me implement basic kicks: when rotating, if blocked, try shifting by a few columns. Actually, let me keep it simple and just do the basic rotation, but I'll add minimal wall kick by trying a small horizontal offset. Let me implement standard SRS kicks to be safe and make it feel good.

Hmm, let me think about time budget. I'll implement a clean version with basic rotation and simple horizontal kicks.

Let me write the code.

For ghost piece - show where the piece will land if hard dropped.

Let me structure the code:

```javascript
// Constants
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 32; // visual size

// Board: 2D array, 0 = empty, value = color index
const board = Array(ROWS).fill().map(() => Array(COLS).fill(0));

// Tetromino definitions
const SHAPES = {
  I: [[0,0],[0,1],[0,2],[0,3]],
  O: [[0,0],[0,1],[1,0],[1,1]],
  T: [[0,0],[0,1],[0,2],[1,1]],
  S: [[0,1],[0,2],[1,0],[1,1]],
  Z: [[0,0],[0,1],[1,1],[1,2]],
  J: [[0,0],[1,0],[1,1],[1,2]],
  L: [[0,2],[1,0],[1,1],[1,2]],
};

const COLORS = {I: '#00f0f0', O: '#f0f000', T: '#a000f0', S: '#00f000', Z: '#f00000', J: '#0000f0', L: '#f0a000'};
```

Let me think about rotation. I'll store the piece as a set of blocks (array of [row, col]) and a rotation state.

For rotation, I'll rotate around the center of the bounding box. Let me use a standard matrix rotation approach.

Actually, let me use a cleaner approach. Each piece has a position (row, col of its center/top-left) and a shape matrix. I'll represent shapes as 4x4 matrices for easier rotation.

Standard 4x4 matrices:
```
I:
0000
1111  (actually different orientations)
```

Let me use the 4x4 matrix approach with proper orientations.

Actually, let me use a simpler approach with explicit block coordinates and rotation math.

Rotation: for a piece with blocks at positions relative to center, rotate each block.

Let me define pieces with their blocks as arrays, and a rotation function that rotates 90 degrees clockwise around the center of rotation.

For wall kicks, I'll implement the standard SRS kicks for each piece type. Actually, let me just do a simpler version: try the rotation, and if it doesn't fit, try shifting by +1, -1, +2, -2 columns and also rows. This is a "bad" kick system but functional.

Let me implement proper SRS to make it feel good. The SRS kicks are well-defined.

Actually, for simplicity and to ensure correctness, let me implement a kick system that tries a predefined list of offsets. For each rotation, I try the original position, then the kick table offsets.

The SRS kick tables (J, L, S, T, Z pieces - non-I):
For rotation current → next:
- 0→R: (0,0), (-1,0), (-1,-1), (0,+2), (-1,+2) ... 

Hmm, this is getting complex. Let me just implement a simpler universal kick: try offsets in this order: (0,0), (1,0), (-1,0), (1,-1), (-1,-1), (2,0), (-2,0)... Actually let me just try a small set.

Let me think about what's simplest while still being playable. 

A common simple approach: 
- For rotation, compute the new block positions
- If they fit as-is, rotate
- Otherwise try shifting the piece: check each position in a kick list

For the O piece, it doesn't really rotate meaningfully (2x2 square). I'll just not rotate O or it's fine.

Let me use a clean approach. I'll define each piece with its blocks as an array of [r, c] relative to a pivot, and implement rotation by transforming coordinates.

Actually, let me just use the matrix rotation approach with 4x4 grids. This is the clearest.

Let me define each piece's 4 rotation states as 4x4 matrices (or smaller). Here are the standard SRS piece definitions:

I piece (4x4):
```
[0,0,0,0]
[1,1,1,1]
[0,0,0,0]
[0,0,0,0]
```
Rotations:
- 0°:
```
[I,I,I,I] (top row)
```
Actually let me use the actual rotation matrices.

I'll define for each piece an array of 4 rotation states, each being a 2D matrix.

Let me look up or just construct them:

**I piece:**
- State 0 (spawn):
```
....
IIII
....
....
```
- State R:
```
..I.
..I.
..I.
..I.
```
- State 2:
```
....
....
IIII
....
```
- State L:
```
.I..
.I..
.I..
.I..
```

**O piece:** (same in all states)
```
.OO
.OO
```
Actually O in 4x4:
```
....
.OO.
.OO.
....
```

**T piece:**
- State 0:
```
.....
.TTT.
.TT..  -- no wait
```
Standard T:
```
.....
.TTT.
..T..
.....
```
Let me be careful. T spawn:
```
....
.TTT.  -- no, T has 3 on top, 1 below center
```
T piece (spawn orientation - facing down):
```
.T.
TTT
```
Rotated clockwise:
```
.TT
.T.
.T.
```
Hmm let me just use 4x4 matrices consistently.

Let me define them as strings:

**I:**
```
"....","IIII","....","...."  (state 0)
"..I.","..I.","..I.","..I."  (state 1)
"....","....","IIII","...."  (state 2)
".I..",".I..",".I..",".I.."  (state 3)
```

**O:**
```
"....",".OO.",".OO.","...."  (all states same)
```

**T:**
```
"....",".TTT.","..T.","...."  (state 0)
"....","..T.","..TT","..T."  -- hmm, let me reconsider
```

This is getting error-prone. Let me use a different, reliable representation.

I'll represent each piece as an array of block coordinates [r, c] for each rotation state. The coordinates are within a bounding box.

Let me define them carefully. The center of rotation for each piece will be at (1.5, 1.5) in a 4x4 grid, or I'll pick a specific pivot point.

Actually, simplest reliable approach: define each piece's 4 rotations using block arrays, and the piece has a "position" (row, col) that represents where the top-left of its bounding box goes. Then rotation cycles through the 4 defined states.

Let me define (using coordinates within 4x4, where I'll align top-left as anchor):

**I piece:**
- State 0: blocks at [(1,0),(1,1),(1,2),(1,3)]
- State 1: blocks at [(0,2),(1,2),(2,2),(3,2)]
- State 2: blocks at [(2,0),(2,1),(2,2),(2,3)]
- State 3: blocks at [(0,1),(1,1),(2,1),(3,1)]

**O piece:**
- All states: blocks at [(1,1),(1,2),(2,1),(2,2)] (in 4x4)

**T piece:**
- State 0: blocks at [(1,0),(1,1),(1,2),(2,1)]  (T pointing up... wait)

Hmm, let me think about what "Up = rotate clockwise" means. Standard Tetris: Up arrow rotates clockwise.

Spawn states (from Tetris guidelines):
- I: horizontal, pointing right... actually spawn state for I is horizontal flat (4 cells in a row).

Let me look at the standard spawn orientations:
- I: horizontal line (cells in a row)
- O: 2x2 square (one orientation)
- T: T pointing up (3 on top, stem down)
- S: 
```
 SS
SS
```
- Z:
```
ZZ
 ZZ
```
- J:
```
J..
JJJ
```
- L:
```
..L
LLL
```

These are the spawn states. Now rotation: clockwise. Let me define all 4 states for each.

I'll use block coordinates where I pick a consistent anchor. To make rotation work with matrix math, let me anchor at top-left of a 4x4 grid and use the center (1.5,1.5) as the rotation center, applying SRS offset corrections.

Actually, you know what, let me just hand-define all 4 rotation states for each piece using block coordinates within their bounding box (4x4 grid, anchor at top-left). This avoids rotation math entirely and guarantees correct shapes. Rotation just cycles the state index. I then apply wall kicks manually.

Here are the states (within 4x4 bounding box, coordinate [row, col]):

**I piece** (4x4):
- State 0 (horizontal, spawn): [(1,0),(1,1),(1,2),(1,3)]
- State 1 (vertical): [(0,2),(1,2),(2,2),(3,2)]
- State 2 (horizontal): [(2,0),(2,1),(2,2),(2,3)]
- State 3 (vertical): [(0,1),(1,1),(2,1),(3,1)]

**O piece** (4x4, but only need 2x2):
- All states: [(1,1),(1,2),(2,1),(2,2)]

**T piece** (4x4):
- State 0 (spawn, T pointing up): [(0,1),(1,0),(1,1),(1,2)]
- State 1 (T pointing right): [(0,1),(1,1),(1,2),(2,1)]
- State 2 (T pointing down): [(1,0),(1,1),(1,2),(2,1)]
- State 3 (T pointing left): [(0,1),(1,0),(1,1),(2,1)]

Let me verify T piece states:
- State 0 (spawn): 
```
.T.
TTT
```
In 4x4: cells (0,1),(1,0),(1,1),(1,2). ✓
- State 1 (rotate clockwise): T should point right. Clockwise rotation of state 0:
The T pointing up rotated clockwise → T pointing right.
State 1:
```
..T.
.TT.
..T.
```
Wait, that's:
```
..T
.TT
..T
```
In 4x4: (0,2),(1,1),(1,2),(2,2). Hmm, let me recheck.

Rotating state 0 [(0,1),(1,0),(1,1),(1,2)] clockwise by 90° around center (1.5,1.5):
(x,y) → (1.5 + (y-1.5), 1.5 - (x-1.5)) = (y, 3-x)
- (0,1) → (1, 3)
- (1,0) → (0, 2)
- (1,1) → (1, 2)
- (1,2) → (2, 2)
So state 1: [(0,2),(1,2),(1,1),(2,2)] → sorted: [(0,2),(1,1),(1,2),(2,2)]
That's:
```
..X.
.XX.
..X.
```
Hmm, that's:
row 0: col 2
row 1: col 1, col 2
row 2: col 2
```
..X.
.XX.
..X.
```
That's a T pointing right? Let me visualize. The stem points right (cells extending to the right). Yes:
```
   X
  XX
   X
```
That's a T rotated 90° clockwise. The opening is on the left, stem points right. ✓

State 2: rotate state 1 clockwise again:
(y, 3-x) applied to state 1 [(0,2),(1,1),(1,2),(2,2)]:
- (0,2) → (2, 3)
- (1,1) → (1, 2)
- (1,2) → (2, 2)
- (2,2) → (2, 1)
State 2: [(1,2),(2,1),(2,2),(2,3)] → 
```
....
..XX.
.XXX.
```
Wait: row1 col2, row2 col1 col2 col3:
```
....
.X..
XXX.
```
Hmm: (1,2) is row 1 col 2. (2,1),(2,2),(2,3) is row 2 cols 1,2,3.
```
row0: ....
row1: ..X.
row2: .XXX
```
That's a T pointing down (stem on top... no). This is T pointing down - 3 across top of the bottom part... Actually:
```
..X.
.XXX
```
The T pointing down has the crossbar on top:
```
.XXX
..X.
```
No wait. State 2 should be T pointing down. Let me reconsider. State 0 was T pointing up. State 1 was T pointing right. State 2 should be T pointing down.

State 2:
```
row0: ....
row1: ..X.
row2: .XXX
```
That's:
```
  X
 XXX
```
Hmm, that doesn't look right for "pointing down". A T pointing down is:
```
XXX
 X
```
i.e., crossbar on top, stem going down.

So my rotation math is wrong. Let me reconsider.

Hmm, actually let me recheck. Clockwise rotation: the standard math is (row, col) → (col, N-1-row) where N=4.

Let me use (r, c) → (c, 3-r):
- (0,1) → (1, 3)
- (1,0) → (0, 2)
- (1,1) → (1, 2)
- (1,2) → (2, 2)
State 1: [(0,2),(1,2),(1,3),(2,2)] → wait that's (0,2),(1,2),(1,3),(2,2)...

Hold on, let me redo this. (r, c) → (c, 3 - r):
- (0,1) → (1, 3-0)=(1,3)
- (1,0) → (0, 3-1)=(0,2)
- (1,1) → (1, 3-1)=(1,2)
- (1,2) → (2, 3-1)=(2,2)
State 1: [(0,2),(1,2),(1,3),(2,2)]... 

Hmm wait that doesn't look right either. Let me reconsider the rotation direction.

Clockwise 90°: a cell at (r, c) in a matrix of size N goes to (c, N-1-r). This is the standard "transpose then reverse" ... no.

Let me think carefully. For a 2D matrix, rotating 90° clockwise means: new[r][c] = old[N-1-c][r].

Equivalently, position (r, c) in old → position (c, N-1-r) in new.

For N=4: (r, c) → (c, 3-r).

State 0 T piece: [(0,1),(1,0),(1,1),(1,2)]
Apply (r,c)→(c,3-r):
- (0,1)→(1,3)
- (1,0)→(0,2)
- (1,1)→(1,2)
- (1,2)→(2,2)
State 1: [(0,2),(1,2),(1,3),(2,2)]

Visualize state 1 (4x4):
```
row0: ..X.   (col 2)
row1: .XX.   (cols 2,3)
row2: ..X.   (col 2)
row3: ....
```
That's:
```
  X
 XX
  X
```
Hmm, that's a T pointing left (opening on right, stem on left)? No:
```
  X
 XX
  X
```
The stem is the lone X's at col 2 (vertical), and the crossbar is the XX at cols 2,3 on row 1. So the "T" shape... Actually this looks like the stem is vertical on the left part and crossbar extends right. This is a T pointing... it's ambiguous. 

I think I'm overcomplicating this. Let me just define all rotation states explicitly and correctly by hand, rather than computing. This is the most reliable.

Here are the well-known Tetris piece rotations. Let me define each as a 4x4 (or 3x3 for some) grid.

**I piece** - all in 4x4:
- 0: `....`, `IIII`, `....`, `....`
- R: `..I.`, `..I.`, `..I.`, `..I.`
- 2: `....`, `....`, `IIII`, `....`
- L: `.I..`, `.I..`, `.I..`, `.I.`

As block coords (row, col):
- 0: (1,0),(1,1),(1,2),(1,3)
- R: (0,2),(1,2),(2,2),(3,2)
- 2: (2,0),(2,1),(2,2),(2,3)
- L: (0,1),(1,1),(2,1),(3,1)

Spawn position: row 0, col 3 (so the I sits at row 1). Actually spawn position for I is at (row=0 or 1, col=3). Let me place I spawn at row=0, col=3 → cells at (1,3),(1,4),(1,5),(1,6). Wait col 3 means the bounding box starts at col 3, so blocks at row 1+0=1, cols 3+0..3+3. So cells (1,3),(1,4),(1,5),(1,6). That's within the 10-wide board (cols 0-9). ✓ And it's visible at row 1. But actually standard spawn has the I at row 0-1 (partially hidden). Let me place it so that the top of the piece is at row 0 for spawn. For I state 0 (4 cells at row 1 in the 4x4 box), if I place the box at board-row 0, cells are at board row 1. That means the piece is visible. 

Hmm, in standard Tetris, the I piece spawns with its top at row 0 (one row above visible top which is row 0). So I spawn the box at board row 0, and the piece's blocks (which are at relative row 1 within the box) appear at board row 1... that's actually 1 row below the top.

Let me reconsider. The visible board is rows 0-19. Pieces spawn at the top. For a flat I piece (state 0), spawning at row 0 with the box's row 0 at board row 0 means blocks at board row 1. So there's 1 row gap above. That's fine, it's still spawnable and visible. The spawn check: a new piece spawns and if it can't (overlaps existing blocks at that spawn position), game over.

Actually, the convention: spawn position is the piece placed such that its bounding box top is at board row 0 (and for I, the blocks are at row 1). The piece is "in play" once spawned. This is consistent.

Let me just place pieces with the bounding box at (spawnRow=0, spawnCol=3) for I and most centered. For O, the bounding box 4x4 with blocks at (1,1),(1,2),(2,1),(2,2), spawning at row 0 col 3 gives blocks at (1,4),(1,5),(2,4),(2,5). Centered-ish. ✓

For others, the standard spawn col. Let me place all pieces with bounding box at (row=0, col=3), except let me check each fits in cols 0-9:
- I state 0: cols 3-6, fits. ✓
- O: cols 4-5, fits. ✓
- T state 0: (0,1),(1,0),(1,1),(1,2) → cols 3-5, fits. ✓
- S, Z, J, L similarly. ✓

Great, all spawn with bounding box at (0,3).

Now let me define all rotation states as block coordinates:

**T:**
- 0 (up): (0,1),(1,0),(1,1),(1,2) → 
```
.X.
XXX
```
- R (right): rotate clockwise → 
```
..X
.XX
..X
```
Hmm, a T pointing right: stem on left? No. T pointing right means opening faces right? Or stem points right?

Ugh, let me think about it differently. "Up = rotate clockwise." 

When you rotate a T pointing up clockwise by 90°, it becomes T pointing right (the stem that pointed down now points right). 

T pointing up:
```
.X.
XXX
```
The "top" of the T (single cell at top) points up, the crossbar is below.

Rotating clockwise 90° (imagine turning right): 
```
..X
.XX
..X
```
Now the single cell that was at top-left... hmm.

Let me use matrix rotation properly. Matrix, N=3:
```
.X.
XXX
```
positions: (0,1),(1,0),(1,1),(1,2).

Rotate 90° clockwise: new[r][c] = old[N-1-c][r], N=3.
Let me just rotate the grid:
```
Original:
row0: . X .
row1: X X X
row2: . . .

Rotate 90 CW: take columns from bottom to top
col0 (bottom to top): row2 col0=., row1 col0=X, row0 col0=. → becomes new col0: . X . → new row0
Actually, easier: 90 CW rotation = transpose then reverse each row (or reverse columns then transpose).

Standard: to rotate 90° clockwise, reverse the rows, then take the transpose. 
Original rows (reversed top-bottom):
row1: X X X
row0: . X .
row2: . . .
Transpose:
new row0: X . .
new row1: X X .
new row2: X . .
So:
```
X..
XX.
X..
```
Hmm that's:
```
X..
XX.
X..
```
That's a T pointing... the stem (X.. column) is on the left, crossbar (XX.) is at row 1. So this is a T pointing left? 

Hmm, I keep getting confused. Let me carefully define: rotate 90 clockwise means the top of the shape moves to the right side.

Original T pointing up:
```
. A .
B B B
```
The top single cell (A, at row 0) should move to the right (row 0, rightmost) after clockwise rotation. Let me check my result:
```
X..   (was B)
XX.  
X..   (was A? no)
```
Hmm.

Let me redo the rotation. 90° clockwise rotation. I'll use the formula: for a point (r, c) in an N×N grid, it maps to (c, N-1-r).

T states (N=3):
- (0,1) → (1, 2)
- (1,0) → (0, 1)
- (1,1) → (1, 1)
- (1,2) → (2, 1)
Result: (0,1),(1,1),(1,2),(2,1):
```
row0: .X.
row1: .XX
row2: .X.
```
That's:
```
.X.
.XX
.X.
```
So the single cell moved from top to... it's now at (0,1) and (2,1)? No wait. We have cells at (0,1),(1,1),(1,2),(2,1). 

Visualizing:
```
row0: . X .
row1: . X X
row2: . X .
```
So there's a vertical line at col 1 (rows 0,1,2) and a cell sticking out at (1,2). 

Hmm, that means the crossbar now sticks out to the right at the middle row. So the "T" shape: the stem is vertical on the left, crossbar points right. That's a T pointing right? The opening of the T... 

In T pointing up, the opening is... actually a T shape has no "opening" per se. Let me think of it as: the T has a top bar and a stem. 

Original:
```
. X .     <- top bar (3 cells)? No, it's 1 cell on top, 3 below
X X X     <- bottom bar (3 cells)
```
The stem is the single top cell, the crossbar is the 3-cell row. The stem points up.

After rotation:
```
. X .     <- (0,1)
. X X     <- (1,1),(1,2)
. X .     <- (2,1)
```
Now: stem would be... the single cell sticking out is at (1,2) (the crossbar points right). The vertical line at col 1 is 3 cells. So this is a T pointing right (the crossbar/stem now points right). ✓

Wait, I need to reconsider which way the stem points. In the original, the "stem" (the part that's different from the bar) is the single top cell pointing up. After clockwise rotation, that single cell should point right. In the rotated version, the single cell sticking out is at (1,2) which is on the right. So yes, the stem points right. ✓

OK so my formula (r,c)→(c, N-1-r) for 90° clockwise is correct. Good.

Let me now just define all pieces using this rotation math programmatically. I'll define the spawn shape for each piece and compute the other 3 states by rotating. I'll represent each piece state as an array of [r,c] block coords.

Actually, let me just hardcode them to be safe, computing carefully. Or I'll compute them in code from the spawn state. Computing in code is cleaner and less error-prone if I get the rotation right.

Let me define spawn shapes (block coords within bounding box):

```javascript
const PIECE_SHAPES = {
  I: [[0,0],[0,1],[0,2],[0,3]],     // 1x4 horizontal (top-left anchor, will adjust)
  O: [[0,0],[0,1],[1,0],[1,1]],     // 2x2
  T: [[0,0],[0,1],[0,2],[1,1]],     // top bar 3, stem down
  S: [[0,1],[0,2],[1,0],[1,1]],
  Z: [[0,0],[0,1],[1,1],[1,2]],
  J: [[0,0],[1,0],[1,1],[1,2]],
  L: [[0,2],[1,0],[1,1],[1,2]],
};
```

Hmm, these use different bounding box sizes (I is 1x4, O is 2x2, others are 2x3 or 3x3). For rotation with consistent center, I should normalize to the same grid size. This is where it gets tricky with different centers.

You know what, let me just use 4x4 grids for all pieces and use the rotation formula. I'll define spawn states in 4x4:

```javascript
const PIECE_STATES = {
  I: [
    [[1,0],[1,1],[1,2],[1,3]],        // horizontal
    [[0,2],[1,2],[2,2],[3,2]],        // vertical
  ],
  ...
};
```

Actually, let me just generate all 4 states by rotating in code. I'll define only state 0 for each piece, compute states 1,2,3 by applying rotation, and store all 4.

Rotation function for a set of blocks (within 4x4 grid, anchored top-left):
```javascript
function rotateBlocks(blocks) {
  // 90 clockwise: (r,c) -> (c, 3-r)
  return blocks.map(([r,c]) => [c, 3-r]);
}
```

Wait, but this assumes a 4x4 grid. For pieces that don't fill 4x4 (like O is 2x2), the rotation center matters. For O, rotating a 2x2 in a 4x4... the O is placed at (1,1),(1,2),(2,1),(2,2) in 4x4. Rotating (r,c)→(c,3-r):
- (1,1)→(1,2)
- (1,2)→(2,2)
- (2,1)→(1,1)
- (2,2)→(2,1)
So O state becomes [(1,1),(1,2),(2,1),(2,2)] — same! Because O is symmetric when rotated around (1.5,1.5). ✓ 

So if I place all pieces in a 4x4 grid with the right center, the rotation (r,c)→(c,3-r) works. The center (1.5,1.5) is the rotation center. Let me verify this center works for each piece:

**I piece:** placed at [(1,0),(1,1),(1,2),(1,3)] (center column at 1.5). Rotating:
- (1,0)→(0,2)
- (1,1)→(1,2)
- (1,2)→(2,2)
- (1,3)→(3,2)
Result: [(0,2),(1,2),(2,2),(3,2)] — vertical I at col 2. ✓ Center of I is between (1,1)&(1,2) for horizontal = col 1.5; for vertical between (1,2)&(2,2)... center at row 1.5, col 2. 

Hmm, the center moved. For horizontal I, center is at (1, 1.5). For vertical, center is at (1.5, 2). The rotation around (1.5,1.5): (1,1.5) rotated around (1.5,1.5) → (1.5 + (1.5-1.5), 1.5 - (1-1.5)) = (1.5, 2). ✓ So vertical I center is (1.5, 2). But the vertical I spans rows 0-3, cols 2. Its center is (1.5, 2). ✓ Good, it's consistent.

But wait, after second rotation, I should get back. Let me check rotating state 1 [(0,2),(1,2),(2,2),(3,2)] → (r,c)→(c,3-r):
- (0,2)→(2,3)
- (1,2)→(2,2)
- (2,2)→(2,1)
- (3,2)→(2,0)
Result: [(2,0),(2,1),(2,2),(2,3)] — horizontal at row 2. ✓ (This is the upside-down I, on row 2 instead of row 1.)

Third rotation: [(2,0),(2,1),(2,2),(2,3)] → 
- (2,0)→(0,1)
- (2,1)→(1,1)
- (2,2)→(2,1)
- (2,3)→(3,1)
Result: [(0,1),(1,1),(2,1),(3,1)] — vertical at col 1. ✓

Fourth rotation: [(0,1),(1,1),(2,1),(3,1)] →
- (0,1)→(1,3)
- (1,1)→(1,2)
- (2,1)→(1,1)
- (3,1)→(1,0)
Result: [(1,0),(1,1),(1,2),(1,3)] — back to state 0. ✓

So the rotation (r,c)→(c,3-r) around 4x4 works for I.

The I states will be at:
- 0: row 1 (cols 0-3)
- 1: col 2 (rows 0-3)
- 2: row 2 (cols 0-3)
- 3: col 1 (rows 0-3)

So when spawning I, the state 0 has blocks at relative row 1. Spawn position top-left at board (0,3) → I blocks at board (1,3-6). The I extends cols 3-6. ✓

For I state 1 (vertical) at relative col 2: spawn at (0,3) → board (0..3, 5). Cols 5 — fine. But wait, the spawn position should be the same reference point. When we spawn, we set the piece at (row=0, col=3) with state 0. Then if we rotate, the relative blocks change but the anchor (0,3) stays. So I state 1 blocks: relative (0,2),(1,2),(2,2),(3,2) → board (0,3),(1,5),(2,5),(3,5)... wait, col = 3 + 2 = 5. So vertical I at col 5. Hmm, but the center... 

Actually, the spawn anchor (0,3) with I state 0 gives horizontal I at board row 1, cols 3-6. If I rotate to state 1 (vertical), blocks at relative (0,2),(1,2),(2,2),(3,2) → board cols 3+2=5, so vertical I at col 5, rows 0-3. But the horizontal I was centered at col 4.5 (cols 3,4,5,6), and vertical at col 5 (col 5 only). The centers don't align! Because the 4x4 rotation moves the piece.

This is the issue with non-SRS rotation — the piece shifts when rotating. But that's the nature of simple rotation systems. The task allows rejecting blocked rotations, so this is acceptable. For better feel, SRS adjusts with kicks, but that's optional.

Hmm, but this center shift is significant and will feel off. Let me reconsider.

The issue: I'm using a 4x4 grid and rotating around (1.5, 1.5), but the pieces are positioned with top-left at a fixed spawn column, not centered on the rotation pivot.

Better approach: Each piece has a center/rotation point. I store the piece's position (the rotation center) and the block offsets relative to that center. Then rotation is clean.

Let me redo: store each piece as block offsets relative to a center point, and the piece's position is that center.

For rotation, I rotate the offsets around (0,0): (r,c) → (-c, r) for 90° CW? Let me get this right.

90° CW rotation around origin: (x, y) → (y, -x)... in screen coords where x=column, y=row (y down).

Hmm, coordinate systems. Let me use (row, col) = (y, x) where y increases downward (screen coords).

90° CW rotation (in standard math, +x is right, +y is up; CW is (x,y)→(y,-x)). But in screen coords +y is down, so CW rotation becomes (x,y)→(-y,x)? Let me just derive it.

Clockwise 90° in screen coords (x right, y down): 
- A point at (x, y) relative to center.
- After CW rotation, what was "east" (x>0, y=0) moves to "south" (x=0, y>0). 
- (x, y) → (-y, x)? Let me check: (x=1, y=0) → (-0, 1) = (0,1). East moves to south. ✓
- (x=0, y=1) → (-1, 0). South moves to west. ✓
- So (x,y) → (-y, x) for CW in screen coords.

In (row, col) = (y, x): (r, c) → (c, -r)? Let me substitute: x=c, y=r. (x,y)→(-y,x) means (c, r) → (-r, c). So in (row,col): r_old→c_new? No. (c, r) → (-r, c) means row_new = -r_old, col_new = c_old. Hmm that doesn't seem right either.

Let me just use (col, row) = (x, y) for rotation math to avoid confusion, then convert.

Rotation: (x, y) → (-y, x) for CW.

Let me define pieces with offsets (x=col_offset, y=row_offset) relative to center.

**I piece** - center at middle of the 4 cells. For horizontal I, offsets: x=-1.5,-0.5,0.5,1.5; y=0. But we need integer offsets. The I piece's rotation center in SRS is at (1.5, 1.5) in 4x4 which is between cells. Using half-integer offsets is fine, but let me use integer offsets from a chosen center.

Alternatively, use the "wall kick" free approach where each rotation state is defined independently. This is the cleanest and most reliable. Let me hardcode all 4 states for each piece, defined so that the piece is centered (consistent spawn column).

Let me define each piece's 4 states as block offsets from a center point (using integer grid). I'll make each state centered around column 0 (so piece is at a consistent horizontal position when rotating).

**I piece** (center between middle, use x offsets -1,0,1,2 or -2,-1,0,1 — let me use offsets from center):
For I, the center is at the middle. Horizontal: 4 cells, center between 2nd and 3rd. Offsets x: -2,-1,0,1 (relative to leftmost+2) no...

Ugh, integer offsets for I are awkward because I has 4 cells. Let me use the convention: the piece's position is the top-left cell of its bounding box, and I precompute all 4 states' block arrays such that the bounding box is consistently positioned. But the problem is the bounding box changes per rotation (I is 4x1 horizontal, 1x4 vertical).

OK here's a clean and common solution: use the "kick table" approach but define a center of rotation per piece, and accept that I/J/L/S/T/Z rotate around appropriate centers. Let me define offsets from center with proper centers.

Let me look at this differently. I'll define each piece with a list of 4 rotation states, where each state is a list of (rowOffset, colOffset) from a fixed "anchor" point, and the anchor point is the same physical location across rotations (the rotation center). For this, I need integer offsets from center, which means the center might be at a cell (for odd-width) or between cells (for even-width like I's 4 cells).

For I piece: 4 cells. If center is between cells 2 and 3 (0-indexed 0,1,2,3 → between index 1 and 2), offsets are -2,-1,0,1... no, that's 4 offsets centered between index 1 and 2: offsets -1.5,-0.5,0.5,1.5 — non-integer.

This is the fundamental issue with I. SRS handles it with the center at (1.5, 1.5) in a 4x4.

Let me just bite the bullet and handle I specially, OR use a different representation.

Actually, the simplest reliable approach: represent each piece's rotation states as 5x5 or 4x4 templates with offsets, and define the spawn position + rotation center explicitly. Let me just use the well-known approach where I define block arrays for all 4 states and use the formula to rotate, with the piece's "position" being the top-left of the 4x4, and ADD wall kicks to compensate.

Actually, you know what — let me just generate the 4 states using rotation math within a 4x4 grid, and accept that the top-left anchor is consistent (piece spawns with 4x4 box top-left at a column that makes it fit). The center shift during rotation is actually normal for non-SRS systems and is fine for a playable game. When rotating, I'll also try wall kicks (small horizontal shifts) to make rotations smoother.

Wait, but the center shift means: I spawns at col 3 (horizontal 4-wide, occupying cols 3-6). Rotate to vertical → occupies col 5 (cols 5 only). So it jumps from cols 3-6 to col 5. That's a big jump, bad UX.

Let me handle this properly. The fix: store the piece position as the rotation center, not top-left. For I, center is at (row=1 or 2, col between). Let me use a grid where I define states with a consistent center.

Alternative clean approach that I've seen work well: Define each piece with block coordinates relative to (0,0) where (0,0) is the rotation center cell, using the standard Tetris piece centers. For pieces where the center is between cells, I offset the spawn by adjusting.

OK, I'm overthinking this. Let me just use the **4x4 matrix with top-left anchor** approach but **fix the I piece centering** by using kick offsets, and for other pieces the 4x4 rotation works fine (no significant shift) because they're roughly symmetric.

Let me verify the shift for T:
T state 0 in 4x4: [(0,1),(1,0),(1,1),(1,2)] — center of bounding box (2x3 within) at (0.5, 1). 
T state 1 (after rotation): [(0,1),(1,1),(1,2),(2,1)] — center at (1, 1.5).

The center shifts from (0.5, 1) to (1, 1.5). The piece jumps down-right by (0.5, 0.5). When anchored top-left at (0,3):
- State 0: board (0,4),(1,3),(1,4),(1,5)
- State 1: board (0,4),(1,4),(1,5),(2,4)

Visual:
State 0:
```
.... (row0: _ X _ _ at cols3-6 → col4)
T T T  (row1: cols 3,4,5)
```
Wait let me redo. State 0 blocks (relative): (0,1),(1,0),(1,1),(1,2). Anchor board(0,3) → col = 3 + relcol, row = 0 + reldata.
- (0,1)→board(0,4)
- (1,0)→board(1,3)
- (1,1)→board(1,4)
- (1,2)→board(1,5)
```
row0: ___X___  (col4)
row1: XXX___  (cols3,4,5)
```
Hmm that's:
```
...X..
XXX...
```
at cols 3-5, rows 0-1. The stem (top) at col4, crossbar at row1 cols3-5. Center at col4, row0.5.

State 1 (relative (0,1),(1,1),(1,2),(2,1)) board(0,3):
- (0,1)→(0,4)
- (1,1)→(1,4)
- (1,2)→(1,5)
- (2,1)→(2,4)
```
row0: ___X___ (col4)
row1: __XX__  (cols4,5)
row2: ___X___ (col4)
```
```
...X.
..XX.
...X.
```
The stem now... single cell at top col4 and bottom col4, crossbar sticking right at row1. 

Comparing centers: State 0 center ~ col4 row0.5. State 1 center ~ col4.5 row1. So it shifts down by ~0.5, right by ~0.5. Not too bad, but noticeable.

For J/L/S/Z similar small shifts. For I it's a big shift (the 4-wide to 1-wide).

To make it clean, the standard solution is SRS with proper centers and wall kicks. Given time constraints, let me implement a reasonable center-based rotation.

**New plan:** Use cell-offset representation relative to the piece's center (a specific cell). I'll define a center for each piece type.

For T, S, Z, J, L: center is the middle cell. T: the block at (1,1) of its 3x3 could be center.

Let me use this representation: each piece is defined as block offsets from a center cell. The piece's position (row, col) IS the center cell. Rotation transforms offsets around (0,0).

For pieces with even extent (I), I'll use a center that's a cell (not between), accepting slight asymmetry, OR use half-integer and snap.

Hmm, let me just use offsets with a center cell and handle I with a center cell that's off. 

Actually, the cleanest: I'll use the offsets as integers, center at a cell, and for I use center at the 2nd cell (of 4) so offsets are (0,-1),(0,0),(0,1),(0,2) for horizontal. Rotating CW: (x,y)→(-y,x) where x=col offset, y=row offset:
- (0,-1) col=0,row=-1 → col=1, row=0 → offset (row0, col1): (0,1)
- (0,0)→(0,0)
- (0,1)→(1,0): (row1,col0) wait. (x=0,y=1)→(-y,x)=(-1,0) → col=-1,row=0 → (row0,col-1)
- (0,2)→(-2,0) → col=-2,row=0 → (row0,col-2)
So vertical I offsets: (0,1),(0,0),(0,-1),(0,-2) → i.e. (row, col): (0,-2),(0,-1),(0,0),(0,1). Vertical with center at col0, cells at cols -2..1 (4 cells) rows 0. 

Hmm so horizontal I (center at cell index 1, offsets col -1,0,1,2) → vertical I (offsets col -2,-1,0,1 at row 0). The center is at the middle-ish but shifted. The vertical extends cols -2..1 and horizontal cols -1..2. Different spread. When centered, the piece's footprint shifts. This causes the I to shift during rotation, which is the known behavior, and SRS compensates with kicks.

For a non-SRS simple game, this shift is acceptable if I add a few horizontal kicks. Let me just implement rotation with offset (x,y)→(-y,x) [CW] around the piece center cell, and apply a small kick: after computing rotated blocks, check if they fit; if not, try shifting the center by small amounts (try the standard kick offsets or just try col shifts of -2,-1,0,1,2 and row shifts 0,-1,-2).

Actually, let me implement proper SRS kicks since it's not that much more code and makes the game feel correct. The SRS kick tables are well-documented.

Let me recall SRS. There are two kick systems: one for I, one for others.

**For J, L, S, T, Z (and O doesn't kick):**
The rotation states are indexed 0,1,2,3. The kick offset table `K` is indexed [fromState][toState], each containing a list of (dx, dy) offsets to try.

Standard SRS kick data (offsets, tested in order; 0-indexed [currentState][targetState]):

Actually the common representation:
```
// For non-I pieces (J,L,S,T,Z), O is exempt
// kick table indexed by [dir][...offsets] where dir = current*... 
```

This is getting complicated. Let me use the well-known array. Here's the SRS data commonly used:

For non-I pieces, the kick offsets for rotation from state A to state B. Let me use a 4x4 table (source state × dest state), each entry a list of (dx,dy) to try (the first being (0,0)).

The standard SRS wall kick data:

For pieces J, L, S, T, Z:
```
// offsets for rotating from state i, format: list of (x, y)
0->1 (0,0), (-1,0), (-1,+1), (0,-2), (-1,-2)   ... 
```

Hmm, actually the SRS kick data is typically given as, for each rotation direction, a set of 5 offset pairs. Let me recall the canonical SRS data.

The SRS data (from the spec), kick table `K`:

For non-I pieces, the wall kicks are (using the 0-indexed states 0,1,2,3 and a table of 4 "direction" entries). Actually, the standard way it's implemented:

There's a 2D array `OFFSETS` indexed by piece rotation state. The four entries correspond to the four rotation states, giving the position of the "key" (a specific cell) in each state. The kick is the difference.

This is the "SRS offset" method. Let me recall the standard offset tables.

For J, L, S, T, Z pieces (in a 4x4... actually 3x3 effective):
The offsets for the 4 states (these are positions of a reference point):
State 0: (0, 0)
State R(1): (0, 0)  -- hmm

I don't remember these exactly. Let me use a simpler, known-correct kick approach.

Actually, let me simplify drastically. I'll implement rotation without SRS but with a simple "try offsets" kick: when rotating, try the rotation, and if blocked, try shifting by these offsets in order: (0,0), (1,0), (-1,0), (2,0), (-2,0), (1,-1), (-1,-1)... Let me just try a set of horizontal and small vertical shifts.

Actually, let me just do: try (0,0), then for kick, try horizontal shifts [−2,−1,1,2] and also vertical shifts for getting "un-stuck" near the floor like (−1 for floor kicks). Keep it simple. Many simple Tetris games use just horizontal kicks of ±1 (and maybe ±2).

Hmm, but I piece needs special kicks too (the I has its own SRS table).

Let me reconsider time budget — I have 15 min. Let me implement something clean and correct without full SRS, using these kicks:
- When rotating, compute new blocks (using center-based offset rotation).
- Try offset (0,0); if all cells valid (in bounds and empty), apply.
- Else try a list of kick offsets: [(0,0), (1,0), (-1,0), (2,0), (-2,0), (1,1), (-1,1), (0,-1)] ... 

Actually, the cleanest correct approach given my uncertainty: let me use the **4x4 template** method with the actual SRS kick tables hardcoded. Let me find/recall the exact tables.

Let me use this well-established SRS data (J, L, S, T, Z piece, the "kick" tested in order, given as offsets from the un-kicked position):

The kicks are defined per (current orientation, target orientation). Here's the data (from tetris.fandom SRS):

For rotation, there's a table that for each of the 4 rotation directions, gives 5 offset pairs (the first is (0,0)).

I'll represent rotations as a cycle 0→1→2→3→0 (CW) and 0→3→2→1→0 (CCW). 

Let me use the canonical SRS offset data. I found it commonly listed as:

```
// For J, L, T, S, Z pieces. Each row is a rotation "direction".
// Direction 0: 0 -> R
// Direction 1: R -> 2
// Direction 2: 2 -> L
// Direction 3: L -> 0
```

Hmm, I really should just look this up but I can't. Let me reason about it.

Actually, let me NOT use full SRS. Let me use a simpler center-based rotation with a minimal kick set, which will be "good enough" and definitely playable and correct (no illegal placements). The task explicitly says wall kicks are optional and to just reject blocked rotations.

Simplest correct approach:
- Each piece: position (row, col) = the piece's anchor (I'll use top-left of 4x4 bounding box).
- 4 rotation states precomputed as block arrays within 4x4.
- Rotation: increment state, then check all blocks valid. If invalid, try a few kick offsets; if none work, reject rotation (keep old state).

For the kick offsets, I'll try: (0,0), then search small offsets. Let me try a reasonable set.

But the I-piece center shift is the issue. With top-left anchor, rotating I from horizontal (row1 in 4x4) to vertical (col2 in 4x4) shifts it. With a kick table trying horizontal shifts up to ±2, I can keep it roughly in place. E.g., when I is at spawn col 3 horizontal (cols 3-6), rotating to vertical (cols 5) — the kick would need to shift left by 2 to put vertical I at col 3 (cols 3-... wait vertical I is 1 wide, col 3 means just col 3, but horizontal was cols 3-6 centered at 4.5). To center vertical I under horizontal, vertical should be at col 4 or 5. Shifting left by 0 keeps it at col 5; shifting left by 1 → col 4; left by 2 → col 3. 

Hmm the natural centering: horizontal I cols 3-6 center 4.5; vertical I at col 5 center 5, or col 4 center 4. Shifting to col 4 (kick -1) centers it best (4 = closest to 4.5). So a kick of -1 col when rotating I horizontal→vertical.

This is doable with kicks. Let me implement kicks as: try the rotated state at the same anchor, then try anchor shifts in this order to find a fit (preferring small shifts, preferring not to shift, and for kicks trying to keep piece roughly in place).

Let me define kick search: given rotated blocks relative to anchor, try a list of (dRow, dCol) offsets applied to the anchor, in order:
(0,0), (0,-1), (0,1), (0,-2), (0,2), (-1,0), (1,0)... 

But blindly choosing any shift that fits could move the piece unexpectedly. For a simple game, trying (0,0) then ±1 col then ±2 col is the classic simple kick and works OK.

Let me go with this kick list (applied to the anchor, i.e., shifting the whole piece):
```
const KICKS = [
  [0, 0],
  [0, -1], [0, 1],
  [0, -2], [0, 2],
  [-1, 0], [1, 0],
  [-1, -1], [-1, 1],
  [1, 1], [1, -1],
];
```
Try each; the first that makes all blocks valid (in bounds & empty) is used. If none, reject rotation.

Wait, but trying (0,-1) before (0,1) biases left. That's fine for kicks; it mimics a bit of wall kick. Actually classic simple wall kick tries (0,0), then (0,-1), then (0,1) — bias left is conventional.

Hmm, but this could let rotations "escape" into walls by shifting. That's actually the point of wall kicks though. Let me include up to ±2 horizontally. Actually, let me limit to ±2 to avoid surprising large shifts, and that matches SRS-ish behavior.

OK let me also reconsider: do I even need the 4x4 template + anchor, or should I use center-based offset rotation? 

Let me go with: **4x4 template per state, anchor = top-left of 4x4, position tracked.** Precompute states by rotating the spawn template with (r,c)→(c,3-r) and then normalizing (shift so top-left of bounding box is (0,0))? No — I need consistent anchor, so I keep them all in 4x4 with (0,0) as the box top-left even if there are empty cells. That's what I did above with the I states (state 1 vertical at col 2, with empty cols 0,1,3).

Wait, but then the anchor (top-left of 4x4) is the same across all states, and the center of mass shifts. That's the non-SRS behavior. With kicks it's playable. Fine.

Actually, hold on. Let me reconsider using the anchor = top-left of 4x4. The I piece vertical state occupies col 2 of the 4x4 box. If the box is at board col 3, vertical I is at board col 5. Horizontal I (state 0) occupies box row 1, cols 0-3, so board cols 3-6. To rotate back-centered I'd shift. The kick handling this. Good.

Let me also double check spawn for each piece fits with anchor at (0,3):
- O state 0: (1,1),(1,2),(2,1),(2,2) → board cols 4,5. Fits (cols 0-9). ✓
- T: fits. ✓
- I: cols 3-6. ✓
- S: (0,1),(0,2),(1,0),(1,1) → cols 3,4,5,6. ✓
- Z: (0,0),(0,1),(1,1),(1,2) → cols 3,4,5,6. ✓
- J: (0,0),(1,0),(1,1),(1,2) → cols 3,4,5,6. ✓
- L: (0,2),(1,0),(1,1),(1,2) → cols 3,4,5,6. ✓

All good.

Now spawn row: anchor row = 0 (top of 4x4). But O/J/L/S/T/Z spawn shapes have blocks at box rows 0,1,2 (or up to row 2+1). With anchor row 0, blocks appear at board rows 0-2. The visible board top is row 0. So pieces spawn partly at row 0 (visible). That's fine — pieces aren't hidden. Actually in standard Tetris, the piece spawns with its top at row 0 or even row -1 (hidden). For our purposes, having it at row 0 is fine and the spawn check works: if row-0 cells are occupied, game over.

Hmm, but actually if a piece spawns with blocks at row 0 and the board is full at row 0, we'd detect collision on spawn → game over. Good.

Wait, one issue: standard spawn is the piece appears at the top with maybe one row above. Let me make spawn anchor row = 0 so pieces appear flush at top. Actually let me reconsider — some games spawn I with the top of its box at row 0 (so I's blocks at row 1). That means there's a 1-row margin. For game-over detection it's the same. Let me just use anchor row 0 for all. The I spawns with blocks at row 1 (1 row below top), which gives a natural "appears from top" feel. Good enough.

Hmm, actually with anchor row 0, let me make sure the spawn doesn't immediately collide. Fresh board is empty so no collision. Good.

Now, the piece "position" I'll store as {row, col} = anchor of the 4x4 box. The 4 states' block arrays are offsets within [0..3]×[0..3].

Let me now also define rotation states by precomputing. I'll define the 4 states for each piece as hardcoded arrays (let me compute them with the formula to be safe, then verify):

I'll compute state by applying rotate (r,c)→(c,3-r) repeatedly starting from state 0, for each piece. Let me define state 0 for each:

```
I:  [[1,0],[1,1],[1,2],[1,3]]
O:  [[1,1],[1,2],[2,1],[2,2]]
T:  [[0,1],[1,0],[1,1],[1,2]]
S:  [[0,1],[0,2],[1,0],[1,1]]
Z:  [[0,0],[0,1],[1,1],[1,2]]
J:  [[0,0],[1,0],[1,1],[1,2]]
L:  [[0,2],[1,0],[1,1],[1,2]]
```

These are the spawn shapes (state 0). Let me verify a couple:
- T spawn: (0,1),(1,0),(1,1),(1,2):
```
row0: .X.
row1: XXX
```
T pointing up. ✓
- J spawn: (0,0),(1,0),(1,1),(1,2):
```
row0: X..
row1: XXX
```
J (block on left). ✓
- L spawn: (0,2),(1,0),(1,1),(1,2):
```
row0: ..X
row1: XXX
```
L (block on right). ✓
- S spawn: (0,1),(0,2),(1,0),(1,1):
```
row0: .XX
row1: XX.
```
S. ✓
- Z spawn: (0,0),(0,1),(1,1),(1,2):
```
row0: XX.
row1: .XX
```
Z. ✓

Now generate states 1,2,3 by rotating. Let me compute the S piece states as a check:
S state 0: [(0,1),(0,2),(1,0),(1,1)]
Rotate (r,c)→(c,3-r):
- (0,1)→(1,3)
- (0,2)→(2,3)
- (1,0)→(0,2)
- (1,1)→(1,2)
S state 1: [(0,2),(1,2),(1,3),(2,3)]:
```
row0: ..X.
row1: ..XX
row2: ..X.
```
Hmm:
```
  X.
  XX
  X.
```
Wait (0,2),(1,2),(1,3),(2,3): row0 col2, row1 col2&3, row2 col3:
```
..X.
..XX
...X? 
```
row2 col3: so:
```
row0: ..X.
row1: ..XX
row2: ...X  no wait (2,3) is row2 col3
```
Let me list: (0,2),(1,2),(1,3),(2,3):
```
row0: col2 → ..X.
row1: col2,col3 → ..XX
row2: col3 → ...X
```
```
..X.
..XX
...X
```
Hmm, that's not a clean S. The S rotated should be:
```
.X.
.XX
.X.
```
No wait. S rotated 90° CW (was horizontal S):
```
.XX
XX.
```
Rotated CW 90:
```
..X
.XX
.X.
```
That's a vertical S. Let me check my computed state 1: [(0,2),(1,2),(1,3),(2,3)]:
```
row0: col2
row1: col2, col3
row2: col3
```
```
..X
..XX  → wait that's col2,col3 at row1
...X  → col3 at row2
```
So:
```
row0: ..X.
row1: ..XX
row2: ...X
```
vs expected:
```
row0: ..X.
row1: .XX.  (cols 1,2)
row2: .X..
```
These differ! My computed is:
```
..X.
..XX
...X
```
That shape:
```
  X
  XX
   X
```
Hmm that IS a valid S/Z vertical orientation? The S piece vertical should look like:
```
..X
.XX
.X.
```
which is:
```
row0: col2
row1: col1, col2
row2: col1
```
= [(0,2),(1,1),(1,2),(2,1)].

But I computed [(0,2),(1,2),(1,3),(2,3)] which is:
```
row0: col2
row1: col2,col3
row2: col3
```
These are different positions but geometrically the same S shape, just shifted right by 1! Because the bounding box of the rotated S is at cols 1-3 (or wherever). My computed version is shifted right vs expected. Both are valid S shapes, just different column in the 4x4. This is fine — it's a rotation within the 4x4, the anchor stays consistent. The center shift is handled by kicks. Good, no problem.

So the rotation math is consistent. Let me just trust it.

Actually, wait. Let me reconsider whether the piece will look weird. For S rotating: state 0 horizontal S at cols 0-2 (in 4x4 box), state 1 (my computed) at cols 1-3. When the box anchor is at board col 3:
- State 0: S at board cols 3-5 (col = 3 + relcol where relcol in 0,1,2 → cols 3,4,5). Actually S state 0 is (0,1),(0,2),(1,0),(1,1) → relcols 0,1,2 → board cols 3,4,5.
- State 1: (0,2),(1,2),(1,3),(2,3) → relcols 2,3 → board cols 5,6.
So rotating S shifts it right by 1. Kick of -1 would re-center. Fine.

OK this all works with kicks. Let me code it up.

Let me also think about whether to generate states in code or hardcode. I'll generate via a rotate function applied to the spawn state (state 0). This ensures consistency.

```javascript
function rotateClockwise(blocks) {
  // blocks: array of [r, c], within a 4x4 grid (indices 0-3)
  // 90 CW: (r, c) -> (c, 3 - r)
  return blocks.map(([r, c]) => [c, 3 - r]).sort((a,b)=>a[0]-b[0]||a[1]-b[1]);
}
```

Wait, I need to keep states as 4 separate arrays (don't mutate). Let me generate:
```javascript
function genStates(spawn) {
  const states = [spawn];
  let s = spawn;
  for (let i = 0; i < 3; i++) {
    s = rotateClockwise(s);
    states.push(s);
  }
  return states;
}
```

But I must ensure each piece has exactly 4 distinct states. Rotating 4 times returns to start. For I and O it works (shown above). Let me trust it but double check T returns to start after 4 rotations.

T state 0: [(0,1),(1,0),(1,1),(1,2)]
State 1 = rotate: 
- (0,1)→(1,3)
- (1,0)→(0,2)
- (1,1)→(1,2)
- (1,2)→(2,2)
State1: [(0,2),(1,2),(1,3),(2,2)]
State 2 = rotate state1:
- (0,2)→(2,3)
- (1,2)→(2,2)
- (1,3)→(3,2)
- (2,2)→(2,1)
State2: [(2,1),(2,2),(2,3),(3,2)]
Hmm:
```
row2: col1,col2,col3 → .XXX
row3: col2 → ..X.
```
```
.XXX
..X.
```
State 3 = rotate state2:
- (2,1)→(1,0)
- (2,2)→(2,0)
- (2,3)→(3,0)
- (3,2)→(2,0)? wait (3,2)→(2, 3-3=0) → (2,0)
State3: [(1,0),(2,0),(3,0),(2,0)] → duplicate (2,0)! That means rotate state2 wrong.

Let me recompute. rotateClockwise: (r,c)→(c, 3-r).
State2: [(2,1),(2,2),(2,3),(3,2)]
- (2,1)→(1, 3-2=1) → (1,1)  [c=1, 3-r=1]
- (2,2)→(2, 1) → (2,1)
- (2,3)→(3, 1) → (3,1)
- (3,2)→(2, 0) → (2,0)
State3: [(1,1),(2,1),(3,1),(2,0)] → sorted by (row,col):
[(1,1),(2,0),(2,1),(3,1)]
```
row1: col1 → .X.
row2: col0,col1 → XX.
row3: col1 → .X.
```
```
.X.
XX.
.X.
```
That's T pointing left (stem on left). ✓

State 0 (4 rotations): rotate state3:
- (1,1)→(1,2)
- (2,0)→(0,1)
- (2,1)→(1,1)
- (3,1)→(1,0)
= [(0,1),(1,0),(1,1),(1,2)] = state 0. ✓✓

So my rotation formula (r,c)→(c,3-r) is correct and returns to start after 4. The earlier "State 2" I computed wrong; corrected now. Good.

Let me also double check the formula handles the 4x4 consistently. The center of rotation is (1.5, 1.5). All good.

Now, one more consideration: I'm applying rotateClockwise which assumes 4x4 grid (3-r). All pieces use 4x4. ✓

Now let me also reconsider: after generating states, each state is a block array within [0,3]×[0,3]. The anchor (piece.position = {row, col}) is the top-left of the 4x4. Board cell = (position.row + r, position.col + c) where (r,c) is the block offset.

Good. Let me now write the full game.

Let me now also think about gravity timing and the level/speed relationship. The task: "Start at level 1 and increase gravity speed every ten cleared lines." So every 10 lines cleared → level up → gravity speeds up.

How to map level to gravity speed (drop interval in ms)? Let me use: dropInterval = baseInterval / level, or a formula like dropInterval = max(100, 1000 - (level-1)*100) etc. Let me use a simple formula: dropInterval = Math.max(80, 1200 - level*100 * ... ). Let me just do: dropInterval = 1200 - (level-1)*100, clamped to min 80ms. At level 1: 1200ms, level 2: 1100, ... level 10: 300ms, etc. Or use exponential-ish. 

Actually, let me make it reasonably challenging. Let me use dropInterval = max(80, 1000 - (level-1)*80). Level 1: 1000ms, level 5: 680, level 10: 280, level 14: 80 (clamped). Fine.

Let me reconsider the requirement exactly: "Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level." So score += LINES_POINTS[numLinesCleared] * level, where LINES_POINTS = [0, 100, 300, 500, 800] (index by count; 0 for none).

"Start at level 1 and increase gravity speed every ten cleared lines." → level = 1 + floor(linesCleared / 10). When linesCleared crosses each multiple of 10, level increases, gravity speeds up.

Note: the level used for scoring — is it the level before or after clearing? Standard Tetris: level is determined by lines, and score uses current level. The task says "multiplied by the current level." Let me compute level from total lines cleared, and use that level for scoring the current clear. Actually order matters slightly; let me set: after clearing, level = 1 + floor(totalLines/10). I'll use the level at the time of scoring. To keep it simple and match "current level," I'll update level based on lines after each clear, and multiply by that level. Edge cases negligible.

Hmm, but for soft drop scoring — the task doesn't specify soft drop points, only line clear points. I'll add small soft-drop scoring optionally (1 pt per cell) but it's not required. Let me keep it minimal; maybe add soft drop points for polish. Actually, to strictly match requirements and avoid confusion, I'll just do line-clear scoring. I can add soft-drop distance points as a bonus but it won't affect the required verification. Let me add soft drop points (1 per row) for polish — it's standard. Actually, let me NOT add it to avoid any scoring ambiguity; keep score = line clears only. Hmm, but soft drop is a required control. Points aren't required for soft drop. I'll keep score purely from line clears to match the spec exactly. Actually I'll add soft drop bonus points (1 per cell) since it's standard and harmless, but the required verification doesn't test it. Let me skip it to keep things clean and spec-exact. I'll just do line clears.

Wait, let me reconsider — I'll add it because it's nice, but it won't break anything. Let me keep it simple: score only from line clears. Decision made.

Ghost piece (optional, but let me add it): show where piece lands on hard drop. Draw a semi-transparent copy at the lowest reachable position.

Hold (optional): let me add hold for polish. Hold lets you store one piece and swap. Standard rules: can only hold once per lock (or before locking), can't hold the held piece back immediately. Let me implement basic hold: press H to hold/swap, but only if not already held since last lock. Actually the task doesn't list Hold as required (it's optional). The required controls are Left/Right/Up/Down/Space. Let me add hold with a 'C' key and ghost, as bonus. But to avoid complexity/bugs, maybe skip hold. Let me add ghost (it's easy and improves playability) and skip hold to save time and reduce bug surface. Actually hold isn't required, and the verification steps don't mention it. Let me add ghost piece (easy, big visual benefit) and skip hold.

Actually, let me add both hold and ghost quickly since they're standard, but be careful. Hmm, hold adds state complexity (canHold flag). Let me add hold too but keep it minimal. Decision: add ghost piece (high value, low complexity). Skip hold to reduce risk. Wait — let me add hold as well because the task mentions it's optional and adding it makes the game more complete. But the verification doesn't test hold. I'll add it; if I'm unsure, the simpler the better. Let me add ghost and skip hold. Final: ghost yes, hold no. Actually, let me reconsider — "Hold and ghost pieces are optional." So neither is required. I'll add ghost for polish. Let me keep hold out to be safe.

Hmm, let me reconsider — adding a 'C' hold key with proper "only once per lock" logic is a small amount of code and standard. But to minimize bug risk during verification, I'll include hold. Let me include both, carefully. Actually, let me just do ghost. Keep scope tight. Ghost it is.

Wait, let me reconsider once more. The verification steps: "Move and rotate the first piece, then hard-drop it. Check that it locks into the board, a new piece spawns, and the next-piece preview advances. Pause and resume, then use Restart and check the cleared board, score, and level."

None of these test hold. Ghost also not tested but is nice. Ghost doesn't change game logic (it's just a visual at computed position), so it's low-risk. I'll include ghost. Skip hold.

Let me now also handle input: I'll use keyboard events. Arrow keys + Space. Need to prevent default for arrow keys (page scroll) and space.

Let me handle:
- KeyDown for arrow keys / space to move/rotate/drop.
- Prevent default on these.
- For soft drop (Down): continuous (the drop timer for soft drop). I'll implement soft drop by reducing the gravity interval while Down is held, or by dropping every ~50ms. Simpler: on each soft-drop tick, move down if possible.

Approach for soft drop: When Down is held, drop every 50ms (fast). When not held, drop every dropInterval (level-based gravity).

Let me implement with timestamps:
- lastDropTime: when last automatic gravity drop occurred.
- lastSoftDrop: when last soft drop occurred (for Down held).
- On each animation frame: if Down held → if now - lastSoftDrop > 50, moveDown, lastSoftDrop = now. Else → if now - lastDrop > gravityInterval, moveDown, reset.

Hard drop: move piece down until collision, lock immediately. Add score? I decided no soft-drop bonus. Hard drop score: standard is 2 pts per cell, but not required. I'll skip bonus; just hard drop and lock. Actually, let me give hard drop a small visual (ghost helps). No points needed.

Let me structure the game loop with requestAnimationFrame.

Let me also handle: piece falling automatically (gravity). The piece locks if it can't move down (collision below) — on next gravity attempt or immediately when moveDown fails? Standard: if moveDown fails, the piece has landed; after a "lock delay" or immediately, it locks. For simplicity, I'll lock immediately when moveDown (gravity) fails (can't move down). But to allow "press up against the floor" leniency, a short lock delay is nice. Let me implement a simple lock delay: when the piece can't move down, start a lock timer (e.g., 500ms); if it still can't move down and timer expires, lock. Also, moving/rotating during lock delay resets the timer a few times. This adds complexity. For simplicity and given verification just needs it to lock and spawn next, let me do: piece locks as soon as it can't move down (immediate lock). This is simplest and works. The soft drop + hard drop still function. Immediate lock is fine for a basic game. Let me go with immediate lock on gravity move-down failure. But I'll keep a tiny consideration: if the player just moved/rotated, allow the auto-gravity to keep it one tick. Immediate lock is OK.

Actually, immediate lock when gravity fails is standard enough for a simple clone. Let me do that.

Wait, but there's a subtlety: the piece spawns, and if it immediately can't move down (board nearly full at top), it should lock and if it can't spawn the next, game over. Fine.

Let me also make sure: when piece can't move down, lock it (merge into board), then check lines, then spawn next. 

Let me code the game now.

Let me also think about the next-piece preview: I maintain a "bag" of 7 pieces (shuffled), and a queue. After spawning, I shift the queue and add a new bag when needed. Next preview shows the next 1 (or a few) pieces. I'll show 1 next piece prominently. Maybe show next 3-5 in a list. Let me show 1 big next piece preview (clear and matches "next-piece preview advances" verification). Actually showing a couple more is nice; I'll show next 3 in small windows and the immediate one bigger. Let me show 3 upcoming pieces in a vertical list, each in a small 4x4 canvas/box.

For the piece sequence: use the 7-bag randomizer (shuffle all 7, deal them, reshuffle when empty). This is standard and ensures fairness. Restart resets this (reshuffle new bag). Good — "resetting... piece sequence state" is explicitly mentioned, so bag randomizer with reset on restart is appropriate.

Let me now write the HTML/CSS/JS.

Canvas: I'll draw everything on one canvas for the board, and separate small canvases (or just DOM) for next-piece. Actually, let me use the main canvas for the board + ghost + active piece, and use DOM/css for next-piece preview (drawn piece in small boxes) and UI (score, level, lines, controls). Or draw the next piece on small canvases. Let me use a main canvas for the playfield and separate small canvases for next-piece previews. Simpler to keep all drawing in one big canvas? Let me use one main canvas for the board, and for the next piece use small canvases or CSS-rendered blocks. I'll use small canvases for next previews by reusing a draw function.

Let me set up:
- <canvas id="board"> 10×20 grid, each cell 32px → 320×640px. That's tall. Let me use 24px cells → 240×480. Or 30px → 300×600. Let me use 30px: 300 wide, 600 tall. Hmm, might be large. Let me use 28px: 280×560. Fine. I'll pick 30px. Actually for a centered layout with sidebar, let me keep board ~ 300×600 (30px cells) — a bit tall but OK. Let me use 26px cells → 260×520. Or 32 → 320×640. Let me go 30px → 300×600. 

Let me use cell size 30. Board canvas 300×600.

Layout: board on left, sidebar on right with next piece, hold (if), score, level, lines, controls, pause overlay. Plus game over overlay and pause overlay.

Let me write it all now. I'll be careful and complete.

Let me write the JavaScript structure:

```javascript
(() => {
  const COLS = 10, ROWS = 20, CELL = 30;
  const canvas = document.getElementById('board');
  const ctx = canvas.getContext('2d');
  canvas.width = COLS * CELL;
  canvas.height = ROWS * CELL;

  // Colors
  const COLORS = {
    I: '#00bcd4', O: '#fdd839', T: '#8e24aa', S: '#4caf50', Z: '#e53935', J: '#1e88e5', L: '#fb8c00',
  };

  // Shapes: spawn state (state 0) as block offsets in 4x4
  const SPAWN = {
    I: [[1,0],[1,1],[1,2],[1,3]],
    O: [[1,1],[1,2],[2,1],[2,2]],
    T: [[0,1],[1,0],[1,1],[1,2]],
    S: [[0,1],[0,2],[1,0],[1,1]],
    Z: [[0,0],[0,1],[1,1],[1,2]],
    J: [[0,0],[1,0],[1,1],[1,2]],
    L: [[0,2],[1,0],[1,1],[1,2]],
  };
  const PIECES = ['I','O','T','S','Z','J','L'];

  function rotateCW(blocks) {
    return blocks.map(([r,c]) => [c, 3-r]);
  }
  // precompute states
  const STATES = {};
  for (const p of PIECES) {
    const states = [SPAWN[p]];
    let s = SPAWN[p];
    for (let i=0;i<3;i++){ s = rotateCW(s); states.push(s); }
    STATES[p] = states;
  }

  // kick offsets
  const KICKS = [[0,0],[0,-1],[0,1],[0,-2],[0,2],[-1,0],[-1,-1],[-1,1],[1,-1],[1,0],[1,1]];

  // Game state
  let board, piece, nextQueue, bag, fallTimer, softDropTimer, lockTimer, lastTime;
  let score, lines, level, gameState; // gameState: 'playing','paused','gameover'

  function init() {
    board = createBoard();
    nextQueue = [];
    bag = [];
    refillBag();
    piece = null;
    score = 0; lines = 0; level = 1;
    fallTimer = 0; softDropTimer = 0; lockTimer = 0;
    gameState = 'playing';
    spawnPiece();
  }
  function createBoard(){ return Array.from({length:ROWS},()=>Array(COLS).fill(0)); }
  function refillBag(){ const a=[...PIECES]; shuffle(a); for(const p of a) nextQueue.push(p); }
  function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} }

  function nextPiece(){ if(nextQueue.length<1) refillBag(); return nextQueue.shift(); }

  function spawnPiece(){
    const type = nextPiece();
    // if queue running low, ensure refill
    piece = { type, state:0, row:0, col:3, locked:false };
    // check spawn collision
    if(!valid(piece.type, piece.state, piece.row, piece.col)){
      gameState='gameover';
      return;
    }
  }

  function valid(type, state, row, col, excludeKick=false){
    for(const [r,c] of STATES[type][state]){
      const br=row+r, bc=col+c;
      if(bc<0||bc>=COLS||br>=ROWS) return false; // br<0 is above board, allowed (not occupied check)
      if(br<0) continue;
      if(board[br][bc]) return false;
    }
    return true;
  }
```

Wait, my `valid` allows row<0 (above board) so pieces can spawn partly above — but with anchor row 0 and states having rows 0-3, the topmost block could be at board row 0 (anchor 0 + relrow 0). For spawn anchor row 0, all blocks are at rows 0-2 (since states have relrows 0-2 mostly, I has 1-3...). Wait I state 0 has relrows 1 (all at row1). So anchor row 0 → I at board row 1. Good, no negative rows on spawn. But during rotation with kicks, pieces might go slightly up. The `valid` allows br<0 (treat as in-bounds for collision but not checking board). Actually if br<0 we `continue` (allow empty above). But we also need br>=0 to access board[br]. Let me handle: if br<0, it's above the board — allowed (not occupied, but is it in bounds?). For spawn it's fine. For kicks, allowing piece to go above board (row<0) is generally fine. Let me allow br<0.

Hmm, but actually a piece shouldn't be allowed to move entirely above. It's fine; gravity will bring it down. OK.

Let me continue:

```javascript
  function move(dx, dy){
    if(gameState!=='playing') return;
    const nr=piece.row+dy, nc=piece.col+dx;
    if(valid(piece.type, piece.state, nr, nc)){ piece.row=nr; piece.col=nc; }
    // soft drop handled in loop
  }

  function rotate(){
    if(gameState!=='playing') return;
    const ns=(piece.state+1)%4;
    // try kicks
    for(const [dy,dx] of KICKS){  // note: I defined KICKS as [row,col] offsets? Let me be consistent.
      ...
    }
  }
```

Let me define KICKS as [dRow, dCol] offsets (applied to anchor). Try each; first valid → apply and break; if none, reject.

```javascript
  const KICKS = [[0,0],[0,-1],[0,1],[0,-2],[0,2],[-1,0],[-1,-1],[-1,1],[1,-1],[1,0],[1,1]];
  function rotate(){
    const ns=(piece.state+1)%4;
    let rotated=false;
    for(const [kr,kc] of KICKS){
      if(valid(piece.type, ns, piece.row+kr, piece.col+kc)){
        piece.state=ns; piece.row+=kr; piece.col+=kc; rotated=true; break;
      }
    }
  }
```

This tries original then kicks. Good. Note: O piece has all 4 states equal; rotating O just changes state to identical shape — fine, no kick needed, (0,0) valid.

Now gravity & soft drop in the frame loop:

```javascript
  function dropDown(){ // one-step soft drop
    if(valid(piece.type,piece.state,piece.row+1,piece.col)){ piece.row++; return true; }
    return false; // landed
  }
  function hardDrop(){
    while(valid(piece.type,piece.state,piece.row+1,piece.col)){ piece.row++; }
    lock();
  }
```

Lock: merge piece into board, clear lines, spawn next.

```javascript
  function lock(){
    for(const [r,c] of STATES[piece.type][piece.state]){
      const br=piece.row+r, bc=piece.col+c;
      if(br>=0&&br<ROWS&&bc>=0&&bc<COLS) board[br][bc]=piece.type;
    }
    // clear lines
    let cleared=0;
    for(let r=ROWS-1;r>=0;r--){
      if(board[r].every(v=>v!==0)){
        board.splice(r,1); board.unshift(Array(COLS).fill(0));
        cleared++; r++; // recheck same index
      }
    }
    if(cleared>0){
      lines+=cleared;
      const linePts=[0,100,300,500,800];
      level = 1 + Math.floor(lines/10);
      score += linePts[cleared]*level;
    }
    spawnPiece();
  }
```

Wait, splice+unshift on the 2D array works (treat board as array of rows). After removing a full row, insert empty row at top. The `r++` after splice to recheck. Good. And level update uses new lines count. Score uses new level. That matches "current level."

Hmm, should level update happen before or after scoring for that clear? Standard: the level at which you clear determines points. Using post-clear level (level = 1+floor(lines/10)) means if you clear to exactly a multiple of 10, the level bumps and you get the higher multiplier. That's how modern scoring works (level increases at the moment lines cross the threshold, and points use the new level). This is fine and matches "multiplied by the current level." Good.

Gravity interval:
```javascript
  function gravity(){ 
    return Math.max(80, 1000 - (level-1)*80); 
  }
```

Frame loop with requestAnimationFrame:
```javascript
  let keys = {};
  // input handlers
  window.addEventListener('keydown', e=>{
    if(e.key==='ArrowLeft'||e.key==='ArrowRight'||e.key==='ArrowUp'||e.key==='ArrowDown'||e.key===' '){
      e.preventDefault();
      if(e.key===' ') e.keyCode=32;
      keys[e.key]=true;
      handlePress(e.key);
    }
  });
```

Hmm, let me separate: I'll handle discrete actions on keydown (rotate up, hard drop space, move left/right with repeat), and soft drop (down) via held state in the loop.

To avoid double-handing (held keys), let me do:
- keydown: if key is Left/Right/Up/Space → handle immediately. Track repeat for left/right? Browsers send repeated keydown when held. To allow smooth left/right movement, repeated keydown works but might be too fast. Let me handle left/right on keydown with a small repeat using a timestamp. Simpler: rely on keydown repeat for movement. For rotate, repeated keydown would spin rapidly — but I can prevent that by only rotating on keydown with a cooldown. 

Let me implement a clean input model:
- Track pressed keys in a set.
- In the frame loop, handle: 
  - Left/Right: if pressed and enough time since last move (for repeat delay), move.
  - Up: rotate on press (track to avoid repeat). 
  - Down: soft drop (every 50ms while held).
  - Space: hard drop on press.

To handle repeat vs one-shot, I'll record the timestamp of each keydown and process:
- For movement (LR): allow repeat with delay (e.g., first move immediate, then every 120ms; while held).
- For Up (rotate): only on the keydown event (one shot), with a cooldown.
- For Space (hard drop): only on keydown (one shot).

This requires storing keydown events. Let me implement a simpler model:
- keydown handler: 
  - ArrowLeft/ArrowRight: set moveDir; move once; set a move-repeat timer.
  - ArrowUp: rotate once (if cooldown passed).
  - ArrowDown: set softDrop=true.
  - Space: hardDrop once (if cooldown passed).
- keyup: clear flags.

Let me implement using a set + timestamps in the loop with per-key debouncing. Actually, let me do the per-key approach in the loop:

```javascript
const pressed = {}; // key -> timestamp pressed (or held)
let lastKeyTime = {}; // for repeat timing

keydown(e){
  const k = normalizeKey(e.key);
  if(k==null) return;
  e.preventDefault();
  if(!pressed[k]) pressed[k]=0;
  pressed[k]=performance.now();
  // immediate one-shot actions
  if(k==='ArrowUp') rotate();
  if(e.code==='Space'){ /* hard drop on press handled below via flag */ }
}
```

This is getting messy. Let me simplify with a clean approach using a key-press flag that fires once per physical press, and continuous flags for held keys.

I'll do this:
- `pressed` set contains currently-held keys.
- On keydown (and repeat), if it's a continuous action (Left, Right, Down), it stays in set; the loop handles timing.
- On keydown for one-shot (Up, Space), handle immediately but debounce.

For Up/Space one-shot with repeat prevention: I'll set a cooldown. Actually, browsers fire keydown once on initial press, then repeated keydowns while held. So if I handle Up/Space only on the keydown event (not in the loop), I get one action per press+repeat. Repeated Up would spin rapidly. For rotate, rapid spin might be undesirable but not wrong. To be safe, I'll allow rotate on each keydown (the OS key repeat will spin it, which is arguably fine). But to prevent too-fast spinning, let me add a cooldown (e.g., 120ms between rotations) using lastRotateTime.

Let me just implement:
- keydown: 
  - preventDefault for the relevant keys
  - ArrowLeft: move(-1)  [handle immediately, plus repeat via OS]
  - ArrowRight: move(1)
  - ArrowUp: rotate()
  - ArrowDown: set softDrop=true
  - Space: hardDrop()
- keyup:
  - ArrowDown: set softDrop=false
- For left/right, OS repeat gives continuous movement. Good enough.
- For Up/Space, OS repeat could fire multiple while held but it's acceptable. I might add cooldown for Up to prevent spin-too-fast. Let me add a rotate cooldown.

Actually, relying on
assistant
thinking
Let me build a playable Tetris game in a single self-contained index.html file. Let me plan this out:

1. HTML canvas for the Tetris board (10 columns, 20 rows visible)
2. CSS for styling
3. JavaScript for game logic

Let me write the complete game:

- Seven tetrominoes (I, O, T, S, Z, J, L)
- Controls: Left/Right move, Up rotate clockwise, Down soft-drop, Space hard-drop
- Lock pieces, clear rows, score system (100, 300, 500, 800 points × level)
- Level increases every 10 cleared lines
- Next piece preview
- Score, cleared lines, level display
- Game Over when piece can't spawn
- Pause/Resume and Restart buttons

Let me write this carefully.

Tetromino shapes (using 4x4 grids):

I:
```
....
....
####
....
```

O:
```
..##
..##
....
....
```

T:
```
.###
..#...
```

S:
```
..##
.##.
....
....
```

Z:
```
.##.
..##
....
....
```

J:
```
###.
...#
....
....
```

L:
```
.###
.#....
....
....
```

Let me write the game properly. I'll use a matrix-based approach.

Let me think about the game loop:
- Use requestAnimationFrame or setInterval for gravity
- Gravity speed increases with level

Score system:
- 1 row: 100 × level
- 2 rows: 300 × level
- 3 rows: 500 × level
- 4 rows: 800 × level

Level = floor(clearedLines / 10) + 1

Let me code this up.

For rotation, I'll use a simple rotation algorithm. To rotate clockwise a matrix:
- transpose and reverse rows (or reverse columns)

Actually, for tetromino rotation, I'll use the common approach of rotating the piece's block positions.

Let me define pieces as arrays of [row, col] positions within a 4x4 grid, or I'll use the SRS-like approach.

Let me keep it simple. Each piece has a shape (array of arrays), and I'll rotate by transposing and reversing.

For rotation:
- Clockwise: reverse rows then transpose, or transpose then reverse columns
- Actually: clockwise = transpose then reverse each row

Let me implement this with a matrix representation.

I'll define the board as a 2D array of 20 rows × 10 columns.

For the piece, I'll track its shape matrix and position (row, col of top-left).

Let me write the complete code:

```html
<!DOCTYPE html>
<html>
<head>
<style>
  /* CSS */
</style>
</head>
<body>
<canvas id="board" width="240" height="400"></canvas>
<!-- UI elements -->
<script>
  // JS
</script>
</body>
</html>
```

Let me think about the canvas size. 10 columns × 20 rows. Each cell could be 24px, so 240px wide × 480px tall for the main board. But the task says "20-row visible board". A standard Tetris board is 20 rows visible but pieces spawn above the visible area. Let me make the visible board 20 rows, but the actual board can have a few extra rows at top for spawning (like 22 rows total, or 20 visible). Actually, let me just use 20 rows visible and handle spawning at the top.

Actually, in standard Tetris, pieces spawn with their top above the visible board (the spawn area). Let me use a board of 22 rows (20 visible + 2 hidden at top) or just 20 rows where pieces spawn at row 0-1.

Let me use 20 rows × 10 columns, and pieces spawn centered at the top (row 0 or 1). For the I piece, it spawns at row 0-1. For O, it spawns at row 0.

Actually, let me use a standard approach: board is 22 rows (rows 0-1, spawn area; rows 2-21, visible) or just 20 rows visible with a buffer. Let me keep it simple with 20 rows and handle the spawn properly.

Hmm, let me think about this more carefully. In standard Tetris World/ Guideline:
- Playfield is 10 wide, 24 tall (20 visible + 4 hidden spawn)
- Pieces spawn at rows 19-22 (above the visible 20)

For simplicity, I'll make the board 20 rows visible. When spawning, if the piece's cells are at rows 0-1, those will be visible at the top. Let me use a 20-row board and spawn pieces at the top (some pieces might be partially off-screen but that's fine for the game mechanics).

Actually, I think the cleanest approach is to have the board be 22 rows (0-21), where rows 0-1 are the spawn area (not visible) and rows 2-21 are visible. But the task says "20-row visible board", so let me make it simple: 20 rows total, 10 columns, and pieces spawn at the top. If a piece can't spawn (overlaps or out of bounds), it's game over.

Let me go with 20 rows × 10 columns. I'll draw all 20 rows. Pieces spawn centered at the top (row 0).

For the I piece, it's 4 cells tall, so spawning at row 0 means it occupies rows 0-3. That's fine.

Let me code this up now.

For rotation, I'll implement a simple approach:
- Each piece has a set of blocks (relative positions as [row, col])
- To rotate clockwise: for each block [r, c], new position is [c, -r] or [c, 3-r] depending on the convention
- Actually, for a 4x4 matrix rotation:
  - Clockwise: new[r][c] = old[3-c][r] (transpose then reverse rows)
  - Or equivalently: for each cell (r, c), the rotated position is (c, 3-r)

Let me think about this more carefully with the matrix approach.

If I have a 4x4 shape matrix:
```
a b c d
e f g h
i j k l
m n o p
```

Rotated clockwise:
```
m i e a
n j f b
o k g c
p l h d
```

So new[r][c] = old[3-c][r]. Or equivalently, for each old cell (r, c), it maps to (c, 3-r) in the new matrix.

Let me implement it as: transpose and reverse rows.
- Transpose: new[r][c] = old[c][r]
- Reverse rows: new[r][c] = old[c][3-r]

So: matrix[r][c] = rotated[c][3-r]... let me just code it carefully.

Actually, let me use a simpler representation. Each piece is defined by its blocks as an array of [row, col] offsets. To rotate:
- For each block (r, c), the new position after clockwise rotation is (c, -r). Then I adjust so the piece stays in a positive grid.

Wait, let me use the rotation formula for a 2D matrix:
- To rotate 90° clockwise around the center of a 4x4 grid:
  - new position of cell (r, c) is (c, N-1-r) where N is the size (4)
  - So (r, c) → (c, 3-r)

Let me verify:
- (0, 0) → (0, 3) ✓ (top-left goes to top-right)
- (0, 3) → (3, 3) ✓ (top-right goes to bottom-right)
- (3, 3) → (3, 0) ✓ (bottom-right goes to bottom-left)
- (3, 0) → (0, 0) ✓ (bottom-left goes to top-left)

Yes, that's correct for clockwise rotation.

For the O piece (2x2 in the center of 4x4), this rotation doesn't change it, which is correct.

Let me code this now.

```javascript
const COLS = 10;
const ROWS = 20;
const CELL_SIZE = 24;

const SHAPES = {
  I: [[1,1,1,1]],  // Actually let me use proper 4x4
  ...
};
```

Hmm, let me use 4x4 matrices for all pieces for consistency with rotation:

```javascript
const SHAPES = [
  // I
  [
    [0,0,0,0],
    [1,1,1,1],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // O
  [
    [0,0,0,0],
    [0,1,1,0],
    [0,1,1,0],
    [0,0,0,0]
  ],
  // T
  [
    [0,0,0,0],
    [1,1,1,0],
    [0,1,0,0],
    [0,0,0,0]
  ],
  // S
  [
    [0,0,0,0],
    [0,1,1,0],
    [1,1,0,0],
    [0,0,0,0]
  ],
  // Z
  [
    [0,0,0,0],
    [1,1,0,0],
    [0,1,1,0],
    [0,0,0,0]
  ],
  // J
  [
    [0,0,0,0],
    [1,0,0,0],
    [1,1,1,0],
    [0,0,0,0]
  ],
  // L
  [
    [0,0,0,0],
    [0,0,1,0],
    [1,1,1,0],
    [0,0,0,0]
  ],
];
```

Actually wait, I need to adjust the J and L shapes. Let me look at these more carefully:

J:
```
#..  or  ..#
###      ###
....     ....
```
Actually standard J:
```
#.
##
#.
```
Rotated in 4x4:
```
[0,0,0,0]
[1,0,0,0]
[1,1,1,0]
[0,0,0,0]
```
Hmm, that's:
```
....
#...
###.
....
```
That's an L shape pointing up-left. Standard J has the block on the left side of the bottom. Let me reconsider.

J tetromino:
```
.#.
.##
..#
```
No wait. Let me think about this clearly.

The seven tetrominoes in their spawn orientation:

I:
```
####
```
(positioned as a horizontal line, 4 cells wide, 1 cell tall)

O:
```
##
##
```
(a 2x2 square)

T:
```
###
.#
```
(or .#. with ## below - the T shape with stem pointing down)

S:
```
.##
##
```
(standard S shape)

Z:
```
##.
.##
```
(standard Z shape)

J:
```
#.
##
.#
```
No, that's not right either. Let me use the standard TGM/Guideline spawn positions.

Standard spawn orientations (from Tetris guidelines):

I: (flat, horizontal)
```
IIII  (at rows 1-1, cols 3-6)
```

O:
```
OO
OO  (at rows 1-2, cols 4-5)
```

T:
```
TTT
.T.  (at rows 1, cols 3-5)
```

S:
```
.SS
SS.  
```

Z:
```
ZZ.
.ZZ
```

J:
```
J..
JJJ
```

L:
```
..L
LLL
```

Let me use 4x4 matrices:

I:
```
[0,0,0,0]
[1,1,1,1]
[0,0,0,0]
[0,0,0,0]
```

O:
```
[0,0,0,0]
[0,1,1,0]
[0,1,1,0]
[0,0,0,0]
```

T:
```
[0,0,0,0]
[1,1,1,0]
[0,1,0,0]
[0,0,0,0]
```

S:
```
[0,0,0,0]
[0,1,1,0]
[1,1,0,0]
[0,0,0,0]
```

Z:
```
[0,0,0,0]
[1,1,0,0]
[0,1,1,0]
[0,0,0,0]
```

J:
```
[0,0,0,0]
[1,0,0,0]  (J at col 0)
[1,1,1,0]  (JJJ at cols 0-2)
[0,0,0,0]
```

L:
```
[0,0,0,0]
[0,0,1,0]  (L at col 2)
[1,1,1,0]  (LLL at cols 0-2)
[0,0,0,0]
```

Wait, let me reconsider the J shape. Standard J:
```
J..
JJJ
```
In 4x4:
```
[0,0,0,0]
[1,0,0,0]
[1,1,1,0]
[0,0,0,0]
```
Row 1 has 1 at col 0 (the hook), row 2 has 1 at cols 0,1,2 (the base). This looks like:
```
....
#...
###.
....
```
That's the J shape with the hook on the left. ✓

L:
```
..L
LLL
```
In 4x4:
```
[0,0,0,0]
[0,0,1,0]
[1,1,1,0]
[0,0,0,0]
```
Row 1 has 1 at col 2 (the hook), row 2 has 1 at cols 0,1,2 (the base). This looks like:
```
....
..#.
###.
....
```
That's the L shape with the hook on the right. ✓

Good. Now for spawn positions: pieces spawn at the top of the board, centered horizontally. The spawn position is row 0, col (10-4)/2 = 3. So the 4x4 grid starts at column 3.

For the O piece, it should spawn at column 4 (centered in the 4-wide grid). Since the 4x4 grid starts at col 3, the 2x2 O piece (at cols 1-2 in the 4x4) would be at cols 4-5 on the board. That's correct for the O piece spawn position.

For the I piece, it spawns at columns 3-6 (the full width of the 4x4 grid starting at col 3). That's correct.

OK, now let me code the full game.

Let me think about the structure:

1. Game state: board, current piece, next piece, score, level, lines cleared
2. Rendering: draw board, current piece, ghost (optional)
3. Controls: keyboard input
4. Game loop: gravity, lock delay, piece lock
5. Row clearing and scoring

Let me write the code:

```javascript
// Game constants
const COLS = 10;
const ROWS = 20;
const CELL_SIZE = 24;
const BOARD_WIDTH = COLS * CELL_SIZE;
const BOARD_HEIGHT = ROWS * CELL_SIZE;

// Colors
const COLORS = {
  1: '#00f0f0', // I - cyan
  2: '#f0f000', // O - yellow
  3: '#f000f0', // T - purple
  4: '#00f000', // S - green
  5: '#f00000', // Z - red
  6: '#0000f0', // J - blue
  7: '#f0a000', // L - orange
};

// Tetromino shapes (4x4 matrices)
const SHAPES = [
  // I - 1
  [
    [0,0,0,0],
    [1,1,1,1],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // O - 2
  [
    [0,0,0,0],
    [0,2,2,0],
    [0,2,2,0],
    [0,0,0,0]
  ],
  // T - 3
  [
    [0,0,0,0],
    [3,3,3,0],
    [0,3,0,0],
    [0,0,0,0]
  ],
  // S - 4
  [
    [0,0,0,0],
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0]
  ],
  // Z - 5
  [
    [0,0,0,0],
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0]
  ],
  // J - 6
  [
    [0,0,0,0],
    [6,0,0,0],
    [6,6,6,0],
    [0,0,0,0]
  ],
  // L - 7
  [
    [0,0,0,0],
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0]
  ],
];

// ... game state
let board = [];
let currentPiece = null;
let nextPiece = null;
let score = 0;
let lines = 0;
let level = 1;
let gameOver = false;
let paused = false;
let dropCounter = 0;
let dropInterval = 1000; // ms, decreases with level
let lastTime = 0;
```

Actually, let me think about the approach more. I'll store the piece as:
```javascript
{
  matrix: [[...]], // 4x4 matrix with piece values
  pos: {x: 3, y: 0} // position on the board (top-left of the 4x4 grid)
}
```

Wait, I'm using row/col terminology. Let me use x for column and y for row to match typical canvas coordinates.

Actually, let me just use row and col consistently with the board.

Let me think about the piece representation:
- `piece.matrix`: 4x4 array representing the shape
- `piece.row`: row position of the top-left of the 4x4 grid on the board
- `piece.col`: column position of the top-left of the 4x4 grid on the board

To get the actual board cells occupied by the piece, I iterate over the 4x4 matrix and for each non-zero cell, the board cell is at (piece.row + r, piece.col + c).

Let me code this now. I'll write the complete file.

For the game loop, I'll use requestAnimationFrame and track time. The gravity drop interval decreases with level.

Gravity speed: In standard Tetris, level 1 = 1 second per row, and it increases. Let me use a formula like:
- dropInterval = 1000ms / level? No, that would make level 10 = 100ms which is very fast.
- Standard: level 1 = 0.8s, level 2 = 0.7s, level 3 = 0.6s, ... decreasing by about 0.05-0.1s per level up to level 10, then faster.

Let me use a simpler approach: dropInterval = 1000 - (level - 1) * 50, clamped to some minimum. Or:
- Level 1: 1000ms
- Level 2: 900ms
- Level 3: 800ms
- ...
- Level 10: 100ms
- Then maybe faster

Actually, let me just use: dropInterval = Math.max(100, 1000 - (level - 1) * 100). So level 1 = 1000ms, level 2 = 900ms, ..., level 10 = 100ms.

Or even simpler: dropInterval = 1000 / level. Level 1 = 1000ms, level 2 = 500ms, level 3 = 333ms, level 4 = 250ms, level 5 = 200ms, level 10 = 100ms. This gives a nice progression.

Let me use: dropInterval = Math.max(100, 1000 / level). Hmm, 1000/level gives level 10 = 100ms which might be OK. Let me just go with 1000 / level and clamp to 100ms minimum.

Actually, let me look at standard Tetris fall times:
- Level 1: 0.8 seconds
- Level 2: 0.7
- Level 3: 0.6
- Level 4: 0.5
- Level 5: 0.4
- Level 6: 0.35
- Level 7: 0.3
- Level 8: 0.25
- Level 9: 0.2
- Level 10: 0.15
- Level 11+: 0.1 or faster

Let me use a simple formula. How about: dropInterval = Math.max(100, 800 - (level - 1) * 50). Level 1 = 800ms, level 2 = 750ms, ..., level 15 = 100ms.

Or even simpler and clean: dropInterval = 1000 * Math.pow(0.9, level - 1), clamped. Level 1 = 1000, level 2 = 900, level 3 = 810, level 4 = 729, etc. This decreases by 10% each level, which feels smooth. But it's not the standard.

Let me just go with: dropInterval = Math.max(100, 1000 - (level - 1) * 100). So:
- Level 1: 1000ms
- Level 2: 900ms
- Level 3: 800ms
- ...
- Level 10: 100ms

This is clean and simple.

OK let me write the full code now.

For the draw function, I'll use canvas 2D context. Each cell is CELL_SIZE × CELL_SIZE pixels. I'll draw the board from row/col to (col * CELL_SIZE, row * CELL_SIZE).

Let me write the complete index.html:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: 'Courier New', monospace;
    background: #1a1a2e;
    color: #e0e0e0;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    padding: 20px;
  }
  h1 {
    margin-bottom: 20px;
    color: #00f0f0;
    font-size: 28px;
    text-shadow: 0 0 10px rgba(0, 240, 240, 0.5);
  }
  .game-container {
    display: flex;
    gap: 20px;
    align-items: flex-start;
  }
  .board-container {
    position: relative;
  }
  canvas {
    background: #111;
    border: 3px solid #333;
    border-radius: 5px;
  }
  .sidebar {
    display: flex;
    flex-direction: column;
    gap: 20px;
    min-width: 180px;
  }
  .info-panel {
    background: #1a1a2e;
    border: 2px solid #333;
    border-radius: 8px;
    padding: 15px;
    text-align: center;
    min-width: 180px;
  }
  .info-panel h3 {
    color: #00f0f0;
    font-size: 14px;
    margin-bottom: 10px;
    text-transform: uppercase;
  }
  .stat {
    font-size: 24px;
    font-weight: bold;
    color: #fff;
  }
  .controls {
    background: #1a1a2e;
    border: 2px solid #333;
    border-radius: 8px;
    padding: 15px;
    text-align: center;
  }
  .controls h3 {
    color: #00f0f0;
    font-size: 14px;
    margin-bottom: 10px;
    text-transform: uppercase;
  }
  .control-item {
    display: flex;
    justify-content: space-between;
    margin: 5px 0;
    font-size: 14px;
  }
  .control-key {
    display: inline-block;
    background: #333;
    border: 2px solid #555;
    border-radius: 5px;
    padding: 5px 12px;
    font-weight: bold;
    color: #fff;
    min-width: 40px;
  }
  .buttons {
    display: flex;
    gap: 10px;
    margin-top: 10px;
  }
  button {
    flex: 1;
    padding: 10px;
    background: #00f0f0;
    color: #1a1a2e;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    transition: background 0.2s;
  }
  button:hover {
    background: #00d0d0;
  }
  button:active {
    background: #00a0a0;
  }
  #next-piece {
    display: flex;
    justify-content: center;
    align-items: center;
    background: #111;
    border: 2px solid #333;
    border-radius: 5px;
    min-height: 60px;
    min-width: 60px;
  }
  .next-piece-canvas {
    background: #111;
    border: 1px solid #333;
  }
  .game-over {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 5px;
    display: none;
  }
  .game-over-text {
    color: #f00;
    font-size: 32px;
    text-shadow: 0 0 20px rgba(240, 0, 0, 0.8);
  }
  .paused-text {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 5px;
    color: #fff;
    font-size: 32px;
    display: none;
  }
</style>
</head>
<body>
  <h1>TETRIS</h1>
  <div class="game-container">
    <div class="board-container">
      <canvas id="board" width="240" height="480"></canvas>
      <div id="game-over" class="game-over">
        <div class="game-over-text">GAME OVER</div>
      </div>
      <div id="paused" class="paused-text">PAUSED</div>
    </div>
    <div class="sidebar">
      <div class="info-panel">
        <h3>Next Piece</h3>
        <canvas id="next-piece" class="next-piece-canvas" width="80" height="80"></canvas>
      </div>
      <div class="info-panel">
        <h3>Score</h3>
        <div id="score" class="stat">0</div>
      </div>
      <div class="info-panel">
        <h3>Lines</h3>
        <div id="lines" class="stat">0</div>
      </div>
      <div class="info-panel">
        <h3>Level</h3>
        <div id="level" class="stat">1</div>
      </div>
      <div class="controls">
        <h3>Controls</h3>
        <div class="control-item"><span>Move Left</span> <span class="control-key">←</span></div>
        <div class="control-item"><span>Move Right</span> <span class="control-key">→</span></div>
        <div class="control-item"><span>Rotate</span> <span class="control-key">↑</span></div>
        <div class="control-item"><span>Soft Drop</span> <span class="control-key">↓</span></div>
        <div class="control-item"><span>Hard Drop</span> <span class="control-key">SPACE</span></div>
        <div class="control-item"><span>Pause</span> <span class="control-key">P</span></div>
        <div class="buttons">
          <button id="pause-btn">Pause</button>
          <button id="restart-btn">Restart</button>
        </div>
      </div>
    </div>
  </div>
<script>
// JavaScript game code
</script>
</body>
</html>
```

Now let me write the JavaScript:

```javascript
const COLS = 10;
const ROWS = 20;
const CELL_SIZE = 24;

const COLORS = {
  1: '#00f0f0', // I - cyan
  2: '#f0f000', // O - yellow
  3: '#f000f0', // T - purple
  4: '#00f000', // S - green
  5: '#f00000', // Z - red
  6: '#0000f0', // J - blue
  7: '#f0a000', // L - orange
};

const SHAPES = [
  // I
  [
    [0,0,0,0],
    [1,1,1,1],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // O
  [
    [0,0,0,0],
    [0,2,2,0],
    [0,2,2,0],
    [0,0,0,0]
  ],
  // T
  [
    [0,0,0,0],
    [3,3,3,0],
    [0,3,0,0],
    [0,0,0,0]
  ],
  // S
  [
    [0,0,0,0],
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0]
  ],
  // Z
  [
    [0,0,0,0],
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0]
  ],
  // J
  [
    [0,0,0,0],
    [6,0,0,0],
    [6,6,6,0],
    [0,0,0,0]
  ],
  // L
  [
    [0,0,0,0],
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0]
  ],
];

const POINTS = [0, 100, 300, 500, 800];

let board, currentPiece, nextPiece, score, lines, level, gameOver, paused, dropCounter, dropInterval, lastTime, gameLoopId;

const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const nextCanvas = document.getElementById('next-piece');
const nextCtx = nextCanvas.getContext('2d');
const scoreEl = document.getElementById('score');
const linesEl = document.getElementById('lines');
const levelEl = document.getElementById('level');
const gameOverEl = document.getElementById('game-over');
const pausedEl = document.getElementById('paused');

function init() {
  board = createBoard();
  currentPiece = null;
  nextPiece = randomShape();
  score = 0;
  lines = 0;
  level = 1;
  gameOver = false;
  paused = false;
  dropCounter = 0;
  dropInterval = 1000 / level;
  lastTime = 0;
  gameOverEl.style.display = 'none';
  pausedEl.style.display = 'none';
  document.getElementById('pause-btn').textContent = 'Pause';
  spawnPiece();
  updateUI();
  draw();
  cancelAnimationFrame(gameLoopId);
  gameLoopId = requestAnimationFrame(update);
}

function createBoard() {
  return Array.from({ length: ROWS }, () => Array(COLS).fill(0));
}

function randomShape() {
  const idx = Math.floor(Math.random() * SHAPES.length);
  return { matrix: copyMatrix(SHAPES[idx]), row: 0, col: 3 };
}

function copyMatrix(m) {
  return m.map(row => [...row]);
}

function spawnPiece() {
  currentPiece = nextPiece;
  nextPiece = randomShape();
  
  // Check if piece can spawn
  if (!isValidPosition(currentPiece)) {
    gameOver = true;
    gameOverEl.style.display = 'block';
  }
  
  drawNextPiece();
}

function isValidPosition(piece) {
  for (let r = 0; r < piece.matrix.length; r++) {
    for (let c = 0; c < piece.matrix[r].length; c++) {
      if (piece.matrix[r][c] !== 0) {
        const boardRow = piece.row + r;
        const boardCol = piece.col + c;
        if (boardCol < 0 || boardCol >= COLS || boardRow >= ROWS) {
          return false;
        }
        if (boardRow >= 0 && board[boardRow][boardCol] !== 0) {
          return false;
        }
      }
    }
  }
  return true;
}

function rotateMatrix(matrix, dir) {
  // Transpose
  for (let i = 0; i < matrix.length; i++) {
    for (let j = i; j < matrix[i].length; j++) {
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }
  // Reverse rows for clockwise, or reverse columns for counter-clockwise
  if (dir > 0) {
    // Clockwise: reverse each row
    matrix.forEach(row => row.reverse());
  } else {
    // Counter-clockwise: reverse each column (reverse the entire array)
    matrix.reverse();
  }
  return matrix;
}

function rotatePiece(dir) {
  if (gameOver || paused) return;
  
  const oldMatrix = copyMatrix(currentPiece.matrix);
  rotateMatrix(currentPiece.matrix, dir);
  
  // Center the piece after rotation
  // Adjust position so the rotation doesn't move the piece too far
  // (No wall kicks, just adjust to keep it valid)
  
  if (!isValidPosition(currentPiece)) {
    // Try adjusting position
    // No wall kicks: just reject the rotation
    currentPiece.matrix = oldMatrix;
  }
}

function movePiece(dir) {
  if (gameOver || paused) return;
  
  currentPiece.col += dir;
  if (!isValidPosition(currentPiece)) {
    currentPiece.col -= dir;
  }
}

function hardDrop() {
  if (gameOver || paused) return;
  
  while (true) {
    currentPiece.row++;
    if (!isValidPosition(currentPiece)) {
      currentPiece.row--;
      break;
    }
  }
  
  lockPiece();
  spawnPiece();
  updateUI();
}

function softDrop() {
  if (gameOver || paused) return;
  
  currentPiece.row++;
  if (!isValidPosition(currentPiece)) {
    currentPiece.row--;
    lockPiece();
    spawnPiece();
    updateUI();
  }
}

function lockPiece() {
  for (let r = 0; r < currentPiece.matrix.length; r++) {
    for (let c = 0; c < currentPiece.matrix[r].length; c++) {
      if (currentPiece.matrix[r][c] !== 0) {
        const boardRow = currentPiece.row + r;
        const boardCol = currentPiece.col + c;
        if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) {
          board[boardRow][boardCol] = currentPiece.matrix[r][c];
        }
      }
    }
  }
  
  clearLines();
}

function clearLines() {
  let linesCleared = 0;
  
  for (let row = ROWS - 1; row >= 0; row--) {
    if (board[row].every(cell => cell !== 0)) {
      board.splice(row, 1);
      board.unshift(Array(COLS).fill(0));
      linesCleared++;
      row++; // Re-check this row since it's now different
    }
  }
  
  if (linesCleared > 0) {
    score += POINTS[linesCleared] * level;
    lines += linesCleared;
    level = Math.floor(lines / 10) + 1;
    dropInterval = Math.max(100, 1000 / level);
  }
}

function updateUI() {
  scoreEl.textContent = score;
  linesEl.textContent = lines;
  levelEl.textContent = level;
}

function draw() {
  // Clear canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Draw board
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      if (board[r][c] !== 0) {
        drawCell(ctx, c * CELL_SIZE, r * CELL_SIZE, board[r][c]);
      }
    }
  }
  
  // Draw current piece
  if (currentPiece) {
    for (let r = 0; r < currentPiece.matrix.length; r++) {
      for (let c = 0; c < currentPiece.matrix[r].length; c++) {
        if (currentPiece.matrix[r][c] !== 0) {
          const x = (currentPiece.col + c) * CELL_SIZE;
          const y = (currentPiece.row + r) * CELL_SIZE;
          drawCell(ctx, x, y, currentPiece.matrix[r][c]);
        }
      }
    }
  }
}

function drawCell(context, x, y, type) {
  const color = COLORS[type] || '#fff';
  context.fillStyle = color;
  context.strokeStyle = '#000';
  context.lineWidth = 1;
  
  // Draw filled cell with rounded corners
  const size = CELL_SIZE - 1;
  const radius = 3;
  context.beginPath();
  context.moveTo(x + radius, y);
  context.lineTo(x + size - radius, y);
  context.quadraticCurveTo(x + size, y, x + size, y + radius);
  context.lineTo(x + size, y + size - radius);
  context.quadraticCurveTo(x + size, y + size, x + size - radius, y + size);
  context.lineTo(x + radius, y + size);
  context.quadraticCurveTo(x, y + size, x, y + size - radius);
  context.lineTo(x, y + radius);
  context.quadraticCurveTo(x, y, x + radius, y);
  context.fill();
  context.stroke();
  
  // Draw inner gradient/highlight
  context.fillStyle = 'rgba(255,255,255,0.3)';
  context.fillRect(x + 3, y + 3, size / 2 - 2, size / 2 - 2);
}

function drawNextPiece() {
  nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
  
  if (!nextPiece) return;
  
  const size = 18;
  const offsetX = (nextCanvas.width - 4 * size) / 2;
  const offsetY = (nextCanvas.height - 4 * size) / 2;
  
  for (let r = 0; r < nextPiece.matrix.length; r++) {
    for (let c = 0; c < nextPiece.matrix[r].length; c++) {
      if (nextPiece.matrix[r][c] !== 0) {
        const x = offsetX + c * size;
        const y = offsetY + r * size;
        nextCtx.fillStyle = COLORS[nextPiece.matrix[r][c]];
        nextCtx.fillRect(x, y, size - 1, size - 1);
      }
    }
  }
}

function update(time = 0) {
  if (paused || gameOver) {
    return;
  }
  
  const deltaTime = time - lastTime;
  lastTime = time;
  dropCounter += deltaTime;
  
  if (dropCounter > dropInterval) {
    dropCounter = 0;
    currentPiece.row++;
    if (!isValidPosition(currentPiece)) {
      currentPiece.row--;
      lockPiece();
      spawnPiece();
      updateUI();
    }
  }
  
  draw();
  gameLoopId = requestAnimationFrame(update);
}

function reset() {
  init();
}

function togglePause() {
  if (gameOver) return;
  paused = !paused;
  pausedEl.style.display = paused ? 'block' : 'none';
  document.getElementById('pause-btn').textContent = paused ? 'Resume' : 'Pause';
}

// Event listeners
document.addEventListener('keydown', e => {
  if (gameOver) {
    if (e.key === 'Enter' || e.key === ' ') {
      init();
    }
    return;
  }
  
  if (paused) {
    if (e.key === 'p' || e.key === 'P' || e.key === 'Escape') {
      togglePause();
    }
    return;
  }
  
  switch (e.key) {
    case 'ArrowLeft':
      e.preventDefault();
      movePiece(-1);
      break;
    case 'ArrowRight':
      e.preventDefault();
      movePiece(1);
      break;
    case 'ArrowUp':
      e.preventDefault();
      rotatePiece(1);
      break;
    case 'ArrowDown':
      e.preventDefault();
      softDrop();
      break;
    case ' ':
      e.preventDefault();
      hardDrop();
      break;
    case 'p':
    case 'P':
      e.preventDefault();
      togglePause();
      break;
    case 'Escape':
      e.preventDefault();
      togglePause();
      break;
  }
});

document.getElementById('pause-btn').addEventListener('click', togglePause);
document.getElementById('restart-btn').addEventListener('click', init);

// Handle window blur/focus for pause
window.addEventListener('blur', () => {
  if (!gameOver && !paused) {
    paused = true;
    pausedEl.style.display = 'block';
    document.getElementById('pause-btn').textContent = 'Resume';
  }
});
window.addEventListener('focus', () => {
  if (paused && !gameOver) {
    // Don't auto-resume, just keep paused state
  }
});

// Start the game
init();
```

Wait, I need to think about a few things:

1. The rotation - when I rotate, the piece might move out of bounds (e.g., if it's at the right edge). Without wall kicks, I should just reject the rotation. That's what I'm doing.

2. The game loop - I need to make sure lastTime is properly handled. When the game starts, lastTime should be 0 or the current timestamp.

3. When paused, the game loop stops (returns), but when unpaused, lastTime will be stale. I need to reset lastTime when unpausing.

4. The hard drop scoring - in standard Tetris, hard drop awards points (typically 2 points per cell dropped × level). Let me add that.

5. Soft drop scoring - typically 1 point per row × level.

Actually, the task says "Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level." It doesn't mention hard/soft drop scoring. Let me keep it simple and only award points for line clears, as specified.

Wait, let me re-read: "Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level."

OK, so only line clear scoring. Let me keep it simple.

6. For the rotation, I need to handle the fact that rotating a piece changes its 4x4 matrix. When I rotate, I need to make sure the piece stays in bounds. Without wall kicks, if the rotated piece would be out of bounds or overlap, I reject the rotation. That's what I'm doing.

But wait, there's a subtlety. When I rotate, the piece's center might shift. In some rotation systems, the piece is rotated around its center. Without wall kicks, simply rotating and checking if the new position is valid should work, but the piece might shift position unexpectedly.

Actually, in my implementation, I'm rotating the matrix in place and keeping the same (row, col) position. This means the top-left of the 4x4 grid stays fixed. This is a common simple approach. If the rotated piece is out of bounds or overlaps, I reject the rotation.

This is fine. Let me also think about the O piece - it doesn't change when rotated, so that's fine.

7. Let me think about edge cases:
- When the game starts, lastTime = 0, and the first update call will have time = timestamp. So deltaTime = timestamp - 0 = timestamp, which could be large. I should handle this by setting lastTime on the first call or initializing it properly.

Let me set lastTime = null initially and handle the first update:

```javascript
function update(time = 0) {
  if (paused || gameOver) {
    return;
  }
  
  if (lastTime === 0) {
    lastTime = time;
  }
  
  const deltaTime = time - lastTime;
  lastTime = time;
  
  // ... rest
}
```

Actually, let me just initialize lastTime properly in the update function.

8. I also need to handle the pause button properly. When paused is true, the update function returns early, but I'm still calling requestAnimationFrame in it... wait, no. When paused is true, the update function returns without calling requestAnimationFrame, so the loop stops. When unpaused, I need to restart the loop by calling requestAnimationFrame.

Let me fix this:

```javascript
function update(time = 0) {
  if (paused || gameOver) {
    if (paused) {
      // Show pause overlay
      pausedEl.style.display = 'block';
    }
    lastTime = time; // Keep lastTime updated
    gameLoopId = requestAnimationFrame(update); // Keep the loop running for rendering
    return;
  }
  
  const deltaTime = time - lastTime;
  lastTime = time;
  dropCounter += deltaTime;
  
  if (dropCounter > dropInterval) {
    dropCounter = 0;
    currentPiece.row++;
    if (!isValidPosition(currentPiece)) {
      currentPiece.row--;
      lockPiece();
      spawnPiece();
      updateUI();
    }
  }
  
  draw();
  gameLoopId = requestAnimationFrame(update);
}
```

Actually, let me reconsider. When paused, I should still render (to show the paused overlay), but not update the game state. I can keep the loop running but skip the game logic:

```javascript
function update(time = 0) {
  if (lastTime === 0) lastTime = time;
  
  if (!paused && !gameOver) {
    const deltaTime = time - lastTime;
    dropCounter += deltaTime;
    lastTime = time;
    
    if (dropCounter > dropInterval) {
      dropCounter = 0;
      currentPiece.row++;
      if (!isValidPosition(currentPiece)) {
        currentPiece.row--;
        lockPiece();
        spawnPiece();
        updateUI();
      }
    }
    draw();
  }
  
  gameLoopId = requestAnimationFrame(update);
}
```

Hmm, but this always calls requestAnimationFrame even when gameOver is true. That's fine for rendering purposes (to show the game over screen).

Actually, let me simplify. The key thing is:
- When paused, don't update gravity/don't move pieces, but still render
- When gameOver, don't update, but render (to show game over screen)
- When playing, update normally

Let me restructure:

```javascript
function update(time = 0) {
  if (!lastTime) lastTime = time;
  
  if (!paused && !gameOver) {
    const deltaTime = time - lastTime;
    lastTime = time;
    dropCounter += deltaTime;
    
    if (dropCounter > dropInterval) {
      dropCounter = 0;
      currentPiece.row++;
      if (!isValidPosition(currentPiece)) {
        currentPiece.row--;
        lockPiece();
        spawnPiece();
        updateUI();
      }
    }
  } else {
    lastTime = time; // Don't accumulate time when paused
  }
  
  draw();
  gameLoopId = requestAnimationFrame(update);
}
```

Wait, there's still an issue. When the game is paused and then resumed, the dropCounter might have accumulated. But I'm setting lastTime = time when paused, so no time accumulates. That should be fine.

Actually, I realize there might be a subtle issue. Let me think again...

When paused:
- lastTime is set to time each frame
- No time accumulates in dropCounter
- draw() is still called to render the paused state

When unpaused:
- lastTime was set to time on the last paused frame
- On the next frame, deltaTime = new_time - lastTime = small delta
- This is correct

This looks fine. Let me also handle the case where the game is paused via the P key during the gameOver state. Actually, when gameOver is true, we shouldn't allow pausing.

Let me also think about keyboard input when the game is over:
- When gameOver, we should still allow Enter/Space to restart (or use the Restart button)

Let me finalize the code. Let me also make sure the `update` function handles the first frame correctly.

One more thing: when I call `init()`, it calls `cancelAnimationFrame(gameLoopId)` and then `requestAnimationFrame(update)`. But on the first call, `gameLoopId` might be undefined. That's fine, `cancelAnimationFrame(undefined)` is a no-op.

Let me also add hard drop scoring for better gameplay. Actually, the task doesn't require it, so let me keep it simple as specified.

Wait, actually, re-reading the requirements:

"Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level."

This only mentions line clear scoring. Let me not add hard/soft drop scoring.

But wait, should hard drop also potentially score? Let me just add line clear scoring as specified.

OK, let me also think about the hard drop more carefully. When hard dropping, the piece falls until it can't move further, locks, clears lines, and spawns a new piece. I need to make sure the hard drop happens correctly.

In my `hardDrop` function, I move the piece down until it can't, then lock it. This should work.

But there's a subtle issue: after hard dropping and spawning a new piece, the game loop will continue and the new piece will start falling due to gravity. This is correct behavior.

Let me also make sure that the piece doesn't soft-drop when it's already at the bottom (in the update function, if the piece can't move down, it locks). This is already handled.

One more thing: I should handle the case where the new piece spawned after a hard drop or gravity drop might immediately be in a locked position (if the board is full). The `spawnPiece` function checks if the new piece can spawn, and if not, sets gameOver.

Actually wait, there's a potential issue. In `spawnPiece`, I set `currentPiece = nextPiece` and then check `isValidPosition`. But if the game is over (new piece can't spawn), I set `gameOver = true` but the piece is already set as `currentPiece`. I should draw it to show the game over state. Let me keep it as is - the piece will be drawn even in game over state.

Let me also think about the rotation of the I piece. The I piece is:
```
[0,0,0,0]
[1,1,1,1]
[0,0,0,0]
[0,0,0,0]
```

After rotating clockwise (transpose + reverse rows):
- Transpose:
```
[0,1,0,0]
[0,1,0,0]
[0,1,0,0]
[0,1,0,0]
```
Wait, that doesn't look right. Let me redo.

Original:
```
Row 0: [0,0,0,0]
Row 1: [1,1,1,1]
Row 2: [0,0,0,0]
Row 3: [0,0,0,0]
```

Transpose (swap rows and columns):
```
Row 0: [0,1,0,0]
Row 1: [0,1,0,0]
Row 2: [0,1,0,0]
Row 3: [0,1,0,0]
```

Reverse each row (clockwise):
```
Row 0: [0,0,1,0]
Row 1: [0,0,1,0]
Row 2: [0,0,1,0]
Row 3: [0,0,1,0]
```

Hmm, that gives:
```
....
..#.
..#.
..#.
```

Wait, that's not right. Let me recalculate.

Original matrix (row, col):
```
(0,0)=0  (0,1)=0  (0,2)=0  (0,3)=0
(1,0)=1  (1,1)=1  (1,2)=1  (1,3)=1
(2,0)=0  (2,1)=0  (2,2)=0  (2,3)=0
(3,0)=0  (3,1)=0  (3,2)=0  (3,3)=0
```

Transpose: new[i][j] = old[j][i]
```
(0,0)=0  (0,1)=1  (0,2)=0  (0,3)=0
(1,0)=0  (1,1)=1  (1,2)=0  (1,3)=0
(2,0)=0  (2,1)=1  (2,2)=0  (2,3)=0
(3,0)=0  (3,1)=1  (3,2)=0  (3,3)=0
```

Reverse each row:
```
(0,0)=0  (0,1)=0  (0,2)=1  (0,3)=0
(1,0)=0  (1,1)=0  (1,2)=1  (1,3)=0
(2,0)=0  (2,1)=0  (2,2)=1  (2,3)=0
(3,0)=0  (3,1)=0  (3,2)=1  (3,3)=0
```

So the I piece rotated clockwise becomes:
```
....
...#
...#
...#
```

Wait, that doesn't seem right. The I piece should become vertical when rotated. Let me think again...

Actually, the issue is that after transpose, the I piece's horizontal line becomes vertical:
```
.###.
.###.
.###.
.###.
```
No wait, the transposed matrix is:
```
.#.#
.#.#
.#.#
.#.#
```
No, let me be more careful.

Original 4x4:
```
Row 0: [0,0,0,0]
Row 1: [1,1,1,1]
Row 2: [0,0,0,0]
Row 3: [0,0,0,0]
```

Transpose: new[i][j] = old[j][i]
Row 0: [old[0][0], old[1][0], old[2][0], old[3][0]] = [0, 1, 0, 0]
Row 1: [old[0][1], old[1][1], old[2][1], old[3][1]] = [0, 1, 0, 0]
Row 2: [old[0][2], old[1][2], old[2][2], old[3][2]] = [0, 1, 0, 0]
Row 3: [old[0][3], old[1][3], old[2][3], old[3][3]] = [0, 1, 0, 0]

So after transpose:
```
Row 0: [0,1,0,0]
Row 1: [0,1,0,0]
Row 2: [0,1,0,0]
Row 3: [0,1,0,0]
```

This is a vertical line at column 1. Then reverse each row:
Row 0: [0,0,1,0]
Row 1: [0,0,1,0]
Row 2: [0,0,1,0]
Row 3: [0,0,1,0]

This is a vertical line at column 2. Hmm, that's still vertical, which is correct for the I piece! The I piece was horizontal, and after rotation it becomes vertical. 

But the column position changed from 1 to 2. This is because the rotation is around the center of the 4x4 grid. The center of a 4x4 grid is at (1.5, 1.5). The original I piece's cells are at (1,0), (1,1), (1,2), (1,3). Their center is at (1, 1.5). After rotation, the cells should be at (0,1.5), (1,1.5), (2,1.5), (3,1.5)... but since we're working with integer grid positions, the rotation maps them to (0,2), (1,2), (2,2), (3,2) due to the discrete nature.

Anyway, the rotation produces a vertical I piece, which is correct. The piece just shifts slightly in the 4x4 grid. Since I'm storing the piece as a 4x4 matrix with a position, this is fine.

Let me verify the rotation works for the T piece:

Original T:
```
Row 0: [0,0,0,0]
Row 1: [3,3,3,0]
Row 2: [0,3,0,0]
Row 3: [0,0,0,0]
```

Transpose:
Row 0: [0,3,0,0]
Row 1: [0,3,3,0]
Row 2: [0,3,0,0]
Row 3: [0,0,0,0]

Reverse each row (clockwise):
Row 0: [0,0,3,0]
Row 1: [0,3,3,0]
Row 2: [0,0,3,0]
Row 3: [0,0,0,0]

So the T piece rotated clockwise:
```
....
..#...
.##...
..#...
```

Original T had the stem pointing down (the single block at (2,1)). After rotation, the stem points right (the single block at (0,2)). That looks correct! The T was:
```
###
.#.
```
(but with 4x4 padding)

After clockwise rotation:
```
.#
##
.#
```

Yes, that's the T rotated 90° clockwise. ✓

OK, the rotation logic is correct. Let me finalize the code.

One more thing I want to add: the soft drop should make the piece fall faster (not just one row per key press, but continuously while the key is held). Actually, for simplicity, let me make soft drop move the piece down by one row per key press, which is a common simpler approach.

Actually, looking at the requirements again: "Down to soft-drop". This means pressing Down should soft-drop. A common implementation is that the piece falls continuously while Down is held. But a simpler approach is one row per key press. Let me go with the simpler approach for now.

Wait, actually, let me make soft drop work properly. When Down is pressed, the piece should fall faster. I can do this by checking if Down is held and adjusting the drop interval. But that's more complex. Let me just move the piece down one row on each key press, which is simpler and more predictable.

Hmm, actually, I realize that with soft drop on key press (one row per press), the player needs to press Down repeatedly to soft-drop. This is less ideal but still functional. Let me add a soft-drop speed multiplier approach instead:

When Down is held, the piece falls at a much faster rate. I'll track if Down is being held and use a faster drop interval.

Actually, let me keep it simple: soft drop on key press moves the piece down by one row. If the position is invalid (can't move down), the piece locks. This is a clean, simple implementation.

Let me also consider: should soft drop also score points? The task doesn't mention it, so I'll skip it.

OK, I think I have a solid plan. Let me also handle:
- Pressing P while the game is over should not do anything (or could restart)
- The Restart button should properly reset everything
- The Pause button should toggle pause

Let me also add: when the game is over, the restart button or Enter/Space key should restart.

Actually, I realize I should also handle key repeat. When arrow keys are held, the browser fires repeated keydown events. This is fine for left/right movement (the piece will keep moving) but might make rotation too fast. For now, let me rely on the default browser key repeat behavior.

Let me also think about the pause behavior more carefully. When I press P to pause:
1. paused = true
2. The update loop continues running but skips game logic
3. The paused overlay is shown
4. The pause button text changes to "Resume"

When I press P again to resume:
1. paused = false
2. The overlay is hidden
3. The button text changes to "Pause"
4. Game continues

This should work with my current implementation.

One issue: when the game is paused and the user presses arrow keys, nothing should happen. My keydown handler already checks for paused state and only handles P/Escape for toggling pause. Other keys are ignored when paused. Good.

Let me also make sure that when the game is over, the player can still see the board and the game over screen, and can restart using the Restart button or Enter/Space.

Let me finalize and write the code now. I think I've thought through all the edge cases.

Actually, one more thing: I need to handle the `update` function's `lastTime` properly when the game is first started. I'll use a flag or check if `lastTime` is 0 (falsy).

Also, I should handle the case where `requestAnimationFrame` gives a timestamp that starts from 0. On the first call, `time` will be the elapsed time since the page loaded, which is a large number. So `lastTime = 0` initially, and `deltaTime = time - 0 = time`, which could be large. I handle this by checking `if (lastTime === 0) lastTime = time` before calculating deltaTime. Wait, but 0 could be a valid timestamp if the page was just loaded... no, `requestAnimationFrame` timestamps are relative to when the page started loading, so they're never 0 (they're always positive). Let me just use a boolean flag:

```javascript
let lastTime = 0;
let firstFrame = true;

function update(time = 0) {
  if (firstFrame) {
    firstFrame = false;
    lastTime = time;
  }
  
  if (!paused && !gameOver) {
    const deltaTime = time - lastTime;
    lastTime = time;
    // ...
  } else {
    lastTime = time;
  }
  
  draw();
  gameLoopId = requestAnimationFrame(update);
}
```

Actually, since `time` from `requestAnimationFrame` is never 0 (it starts from when the page loads), checking `if (!lastTime) lastTime = time` would work too. But let me use a cleaner approach: initialize `lastTime` to `null` and check for that.

OK, let me just write the code now. I've thought about this enough.

Let me also reconsider the canvas size. If CELL_SIZE is 24, the board is 10 * 24 = 240px wide and 20 * 24 = 480px tall. That's a reasonable size.

For the next piece preview, each cell should be smaller. If I use 20px cells in a 4x4 grid, that's 80px, which fits in an 80x80 canvas. Let me adjust.

Actually, let me use a slightly larger CELL_SIZE for better visibility. Let me use 24px for the main board, giving 240x480. For the next piece preview, I'll use 16px cells in a 4x4 grid, giving 64x64, centered in an 80x80 canvas.

Hmm, actually, let me reconsider. 24px cells at 10x20 gives 240x480. That's fine. Let me keep it.

Wait, actually I want the board to look good. Let me use 24px cells and have the board be 240x480. For the next piece, I'll draw it in a small canvas.

Let me also think about whether I want to draw a grid or grid lines. I'll draw grid lines for better visibility.

OK, let me write the complete code now. I'll be thorough.

Let me also add: the game should auto-pause when the window loses focus, and not auto-resume when it regains focus (to avoid unexpected game progression). Actually, let me not auto-pause on blur to keep it simpler. The user can manually pause.

Let me also consider: should I use `keydown` for all inputs? Yes, `keydown` is fine for game controls. I should call `e.preventDefault()` to prevent the browser from scrolling when arrow keys are pressed.

OK, final code. Let me also make the cells look nice with a 3D effect (gradient, highlights, etc.).

Here's my plan for visual styling:
- Board background: dark with grid lines
- Cells: colored with a gradient, border, and highlight
- UI: dark theme with cyan accents
- Pause/Game Over: overlay

Let me write the final code now.

Let me also think about the `drawNextPiece` function. The next piece canvas should be 80x80. The 4x4 matrix with 16px cells would be 64x64, centered with 8px margin on each side. That works.

Actually, I should also handle the case where the next piece preview canvas looks good. Let me use 16px cells.

Wait, I also want to handle the next piece preview properly. The next piece is a 4x4 matrix. At 16px per cell, that's 64px. I'll center it in the 80x80 canvas.

OK, let me write the complete file now. I'll be thorough but efficient.

Let me also reconsider the rotation centering issue. When rotating a piece, the 4x4 matrix rotates around its center (1.5, 1.5). This can cause the piece to shift by half a cell, which in discrete terms means it shifts by about 0 or 1 cells. To keep the piece at a consistent position, I could adjust the piece's position after rotation. But since I'm not implementing wall kicks and just rejecting invalid rotations, the simple approach should be fine.

Actually, I just realized there might be an issue with the rotation for the I piece. The I piece is 4 cells wide, and when rotated, it becomes 4 cells tall. If the piece is at column 3 (the leftmost valid position for the I piece), rotating it would keep it at column 3, but the vertical I piece occupies columns 3 (since the non-zero cells are at column 2 in the 4x4 matrix, so board columns 3+2=5). Wait, let me think again.

The I piece horizontal: non-zero cells at (1,0), (1,1), (1,2), (1,3) in the 4x4 matrix. Board position is (row, col) = (0, 3) (assuming spawn position). So board cells are (1,3), (1,4), (1,5), (1,6).

After clockwise rotation: non-zero cells at (0,2), (1,2), (2,2), (3,2) in the 4x4 matrix. Board position is still (0, 3). So board cells are (0,5), (1,5), (2,5), (3,5).

This is correct! The I piece was at columns 3-6, and after rotation, it's at column 5 (centered). ✓

Now, if the I piece is at column 0 (can't actually spawn there, but hypothetically), rotating it would give:
- Horizontal: board cells (1,0), (1,1), (1,2), (1,3)
- After rotation (clockwise): board cells (0,2), (1,2), (2,2), (3,2)

This still fits because col 2 is within bounds. So the piece doesn't shift too far.

OK, I'm confident the rotation is correct. Let me write the complete code now.

Let me also handle one more thing: when checking if a piece can be rotated, I need to check the rotated matrix at the current position. If it overlaps or goes out of bounds, I reject the rotation. This is simple and correct without wall kicks.

One final thing: the task says "Use Left/Right to move, Up to rotate clockwise, Down to soft-drop, and Space to hard-drop." I should make sure these are the exact keys. Left/Right arrows for movement, Up arrow for rotation, Down arrow for soft drop, Space for hard drop.

I should also add P for pause/resume, and have a Pause/Resume button and a Restart button.

OK, let me write the complete file now. I'll also add the controls display as required.

Let me finalize the code and write it.

Actually, let me reconsider the soft-drop behavior. When the player presses Down, I move the piece down one row. If it can't move down, it locks. This is a simple and clean implementation. The piece moves one row per key press, and with key repeat, it moves continuously while the key is held.

But should I also score for soft-dropping? The task only mentions scoring for line clears, so I'll skip soft-drop scoring.

Also, for the hard drop, should I score? The task only mentions scoring for line clears. But in standard Tetris, hard drop scores points (2 per cell × level). Let me add it for better gameplay, or skip it to keep it simple. I'll skip it as the task doesn't mention it.

Wait, let me re-read the scoring requirements:

"Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level."

OK, only line clear scoring. Let me skip hard/soft drop scoring.

Actually, I'll add hard drop scoring too since it makes the game more complete and it's a standard Tetris feature. I'll award 2 points per row dropped × level. This doesn't conflict with the specified scoring.

Hmm, but the task says "Award 100, 300, 500, or 800 points for clearing one, two, three, or four rows with one piece, multiplied by the current level." This seems to be the only scoring mechanism mentioned. Let me not add hard drop scoring to keep it simple and match the spec exactly.

OK, final decision: only line clear scoring as specified.

Let me write the code now. For real this time.

Let me also think about the game loop more carefully. I'm using `requestAnimationFrame`, which gives a timestamp. The gravity is based on elapsed time. When the piece falls by one row (due to gravity), I check if the new position is valid. If not, I move it back and lock it.

One potential issue: the piece might fall by one row, and the `dropCounter` might have accumulated more than one drop interval (if the frame rate is low or the user tabs away). I should handle this by using a while loop or by checking the piece multiple times. But for simplicity, I'll just do one drop per interval check. If the piece falls too fast and skips rows, that's not ideal, but it's acceptable for this implementation.

Actually, the standard approach is:
```javascript
if (dropCounter > dropInterval) {
  dropCounter = 0;
  // move piece down one row
  // if can't move, lock
}
```

This only moves one row per interval, regardless of how much time has passed. If the user was away for 5 seconds, the piece would only move 5 rows (at level 1, 5 seconds = 5 drops). This is fine because we reset the counter to 0 each time.

But actually, I should use a while loop to handle the case where the counter has accumulated more than one interval:
```javascript
while (dropCounter > dropInterval) {
  dropCounter -= dropInterval;
  // move piece down one row
  // if can't move, lock and break
}
```

This ensures the piece moves all the way down if the frame rate dropped. But for simplicity and to avoid edge cases, let me use the simpler approach (one drop per interval check). If the game is paused, the timer is stopped, so this shouldn't be an issue.

OK, let me finalize the code now.

Actually, I just realized there's another issue. When I use `requestAnimationFrame`, the callback receives a timestamp. On the first call, `lastTime` is 0 (or null), and I set it to the timestamp. Then on subsequent calls, I calculate deltaTime. This should work correctly.

Let me also make sure the game starts properly. I'll call `init()` at the end of the script.

One more thing: I should make sure that the keydown events are captured even when the canvas doesn't have focus. I'll add the event listener to the document, not just the canvas.

OK, let me write the code now. Here's my final plan:

1. HTML structure with canvas, sidebar (next piece, score, lines, level, controls, buttons)
2. CSS for styling
3. JavaScript:
   - Constants (COLS, ROWS, CELL_SIZE, COLORS, SHAPES, POINTS)
   - Game state variables
   - Functions: init, createBoard, randomShape, copyMatrix, spawnPiece, isValidPosition, rotateMatrix, rotatePiece, movePiece, hardDrop, softDrop, lockPiece, clearLines, updateUI, draw, drawCell, drawNextPiece, update, togglePause, keydown handler
   - Event listeners
   - Start game

Let me write it all out now.

Actually, one thing I want to be more careful about: the `rotateMatrix` function modifies the matrix in place. I need to make a copy of the original matrix before rotating, so I can restore it if the rotation is invalid.

```javascript
function rotatePiece(dir) {
  if (gameOver || paused) return;
  
  const oldMatrix = copyMatrix(currentPiece.matrix);
  rotateMatrix(currentPiece.matrix, dir);
  
  if (!isValidPosition(currentPiece)) {
    currentPiece.matrix = oldMatrix;
  }
}
```

This is correct. I save the old matrix, rotate the current one, check if the rotated position is valid, and if not, restore the old matrix.

Wait, but `currentPiece.matrix` is still pointing to the old array... no, I'm modifying `currentPiece.matrix` in place with `rotateMatrix`. So when I do `currentPiece.matrix = oldMatrix`, I'm replacing it with the saved copy. This should work correctly.

Let me trace through:
1. `oldMatrix = copyMatrix(currentPiece.matrix)` - deep copy of the original
2. `rotateMatrix(currentPiece.matrix, dir)` - rotates in place, modifying `currentPiece.matrix`
3. If invalid: `currentPiece.matrix = oldMatrix` - restores the original state

Yes, this is correct.

One more thing: I should also handle the case where the piece is rotated and then the rotation causes it to go out of bounds on the left or right. In my `isValidPosition` function, I check for `boardCol < 0 || boardCol >= COLS`, so this is handled.

OK, let me also think about the case where the piece is at the top of the board and the rotation would cause it to go above the board (negative row). In my `isValidPosition` function, I check `boardRow >= 0` for the overlap check, but I don't reject negative rows for the bounds check. Wait, let me check:

```javascript
function isValidPosition(piece) {
  for (let r = 0; r < piece.matrix.length; r++) {
    for (let c = 0; c < piece.matrix[r].length; c++) {
      if (piece.matrix[r][c] !== 0) {
        const boardRow = piece.row + r;
        const boardCol = piece.col + c;
        if (boardCol < 0 || boardCol >= COLS || boardRow >= ROWS) {
          return false;
        }
        if (boardRow >= 0 && board[boardRow][boardCol] !== 0) {
          return false;
        }
      }
    }
  }
  return true;
}
```

I check `boardCol < 0 || boardCol >= COLS || boardRow >= ROWS` for out-of-bounds, and `boardRow >= 0 && board[boardRow][boardCol] !== 0` for overlap. This means:
- If `boardRow < 0` (piece is above the board), the bounds check passes (doesn't return false), and the overlap check is skipped (because `boardRow >= 0` is false). So the position is considered valid.
- This means pieces can be above the board, which is correct for the spawn position.

But wait, for rotation, if the piece is at row 0 and rotating causes part of it to go to row -1 (above the board), that's still considered valid. Is this correct? In standard Tetris, pieces can't rotate above the visible board. But for simplicity, I'll allow it since the spawn position is at row 0 and the rotation shouldn't cause the piece to go too far above.

Actually, in standard Tetris, pieces spawn at row -1 or -2 (above the visible board), so allowing negative rows for rotation is fine. But in my implementation, pieces spawn at row 0, so rotating above the board is a edge case that might not look great.

Let me think about this... If the piece is at row 0 and I rotate it, the rotated matrix might have cells at row -1 (relative to the piece position). This would be at board row -1, which is above the board. The piece would be visible partially above the board canvas.

To handle this cleanly, I should either:
1. Allow negative rows (pieces can be partially above the board) - but then I need to handle drawing them correctly
2. Reject rotations that would move the piece above the board - this is more restrictive but simpler

Let me go with option 2 for simplicity: if any non-zero cell would be at a negative board row, reject the rotation.

Actually wait, if I allow negative boardRow, then when I draw the piece, I would try to draw it at a negative y position. The `drawCell` function would draw at `y = (piece.row + r) * CELL_SIZE`, which could be negative. The canvas `fillRect` would just not draw anything outside the canvas, so this is fine visually. But it might look odd if part of the piece is above the board.

Let me just allow it for now. The spawn position is at row 0, and the rotation shouldn't cause too many cells to go above the board. If it does, the piece will just be partially off-screen, which is acceptable.

Actually, I think the issue is more about whether the rotation is legal. If part of the piece goes above the board, it's still "on the board" in the sense that the cells are within the board's column bounds and below the bottom. But above the board is not really a legal position...

Let me just add a check for negative boardRow in `isValidPosition`:

```javascript
if (boardCol < 0 || boardCol >= COLS || boardRow < 0 || boardRow >= ROWS) {
  return false;
}
```

Wait, but then I need to allow the spawn position to have negative rows. Hmm, the spawn position is at row 0, and the I piece occupies rows 1-3 (in the 4x4 matrix starting at row 0). So all cells are at board row 1 or above. No negative rows at spawn.

But if I reject rotations that go above the board (row < 0), then some rotations might be rejected that should be legal. For example, if the I piece is at row 0 (horizontal), rotating it to vertical would have cells at rows 0-3. That's fine, no negative rows.

Actually, let me think about when rotations could cause negative rows. The piece is at row 0 (spawn position). If I rotate the L piece:

L shape:
```
[0,0,0,0]
[0,0,7,0]
[7,7,7,0]
[0,0,0,0]
```
Non-zero cells at (1,2), (2,0), (2,1), (2,2). At position row=0, col=3, board cells are (1,5), (2,3), (2,4), (2,5). No negative rows.

After clockwise rotation:
```
[0,0,0,0]
[7,7,0,0]
[0,7,0,0]
[0,7,0,0]
```
Wait, let me recalculate. The L matrix:
```
Row 0: [0,0,0,0]
Row 1: [0,0,7,0]
Row 2: [7,7,7,0]
Row 3: [0,0,0,0]
```

Transpose:
```
Row 0: [0,0,7,0]
Row 1: [0,0,7,0]
Row 2: [0,0,7,0]
Row 3: [0,0,0,0]
```

Reverse each row (clockwise):
```
Row 0: [0,7,0,0]
Row 1: [0,7,0,0]
Row 2: [0,7,0,0]
Row 3: [0,0,0,0]
```

So the rotated L piece:
```
....
.###...
.#....
.#....
```
Wait, no. The matrix is:
Row 0: [0,7,0,0] → board row 0, cols 3+1=4: cell (0,4)
Row 1: [0,7,0,0] → board row 1, col 4: cell (1,4)
Row 2: [0,7,0,0] → board row 2, col 4: cell (2,4)
Row 3: [0,0,0,0] → no cells

So at position row=0, col=3, the rotated L piece occupies (0,4), (1,4), (2,4). No negative rows. Good.

I think rotations won't cause negative rows at the spawn position (row 0). Let me just use the original `isValidPosition` that allows negative rows, but I'll add the negative row check to be safe:

Actually, let me just keep the original check that allows negative rows (for overlap, only check boardRow >= 0). For the bounds check, I'll only check column bounds and row >= ROWS. This way, pieces can be partially above the board, which is fine for spawning.

But for rotation, if part of the piece goes above the board, it would still be a valid rotation. This means a piece could be rotated such that part of it is above the board, and then when it falls, it would be partially above the board. This is a bit odd but not game-breaking.

Let me just go with this approach. It's simple and functional.

OK, I'm going to write the code now. No more analysis paralysis.

Actually, wait. I just realized there might be an issue with the rotation. When I rotate the piece, the 4x4 matrix changes, but the piece's position (row, col) stays the same. This means the top-left corner of the 4x4 grid stays fixed. But the piece's blocks might shift within the grid.

For example, the I piece horizontal:
```
....
####
....
....
```
Non-zero at row 1, cols 0-3.

After rotation:
```
...#
...#
...#
....
```
Wait, let me recalculate.

I matrix:
```
Row 0: [0,0,0,0]
Row 1: [1,1,1,1]
Row 2: [0,0,0,0]
Row 3: [0,0,0,0]
```

Transpose:
```
Row 0: [0,1,0,0]
Row 1: [0,1,0,0]
Row 2: [0,1,0,0]
Row 3: [0,1,0,0]
```

Reverse each row (clockwise):
```
Row 0: [0,0,1,0]
Row 1: [0,0,1,0]
Row 2: [0,0,1,0]
Row 3: [0,0,1,0]
```

So the rotated I piece:
```
....
..#...
..#...
..#...
```
Non-zero at col 2, rows 0-3. At position row=0, col=3, board cells are (0,5), (1,5), (2,5), (3,5). This is correct! The I piece was horizontal (rows 1, cols 3-6), now it's vertical (rows 0-3, col 5). The center is approximately preserved (was at col 4.5, now at col 5). Close enough for a simple rotation system.

OK, the rotation is correct. Let me write the code now.

One last thing: I should make sure that when the game is over and the user presses Restart, the game fully resets including the animation frame. Let me make sure `init()` cancels the existing animation frame and starts a new one.

```javascript
function init() {
  if (gameLoopId) {
    cancelAnimationFrame(gameLoopId);
  }
  // ... reset state
  gameLoopId = requestAnimationFrame(update);
}
```

I already have this in my code. Good.

Let me also make sure that when the game is paused, the animation frame is still running (to render the paused state), but the game logic is skipped. This is handled by the `paused` check in the `update` function.

OK, I'm writing the code now. For real.

Let me structure the JavaScript properly:

```javascript
// Constants
const COLS = 10;
const ROWS = 20;
const CELL_SIZE = 24;
const BOARD_W = COLS * CELL_SIZE;
const BOARD_H = ROWS * CELL_SIZE;
const POINTS = [0, 100, 300, 500, 800];

// Colors for each tetromino type
const COLORS = {
  1: '#00f0f0', // I - cyan
  2: '#f0f000', // O - yellow
  3: '#f000f0', // T - purple
  4: '#00f000', // S - green
  5: '#f00000', // Z - red
  6: '#0000f0', // J - blue
  7: '#f0a000', // L - orange
};

// Tetromino shapes (4x4 matrices)
const SHAPES = [
  // I
  [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
  // O
  [[0,0,0,0],[0,2,2,0],[0,2,2,0],[0,0,0,0]],
  // T
  [[0,0,0,0],[3,3,3,0],[0,3,0,0],[0,0,0,0]],
  // S
  [[0,0,0,0],[0,4,4,0],[4,4,0,0],[0,0,0,0]],
  // Z
  [[0,0,0,0],[5,5,0,0],[0,5,5,0],[0,0,0,0]],
  // J
  [[0,0,0,0],[6,0,0,0],[6,6,6,0],[0,0,0,0]],
  // L
  [[0,0,0,0],[0,0,7,0],[7,7,7,0],[0,0,0,0]],
];

// Game state
let board, piece, nextPiece, score, lines, level, gameOver, paused, dropCounter, dropInterval, lastTime, animId, firstFrame;

// DOM elements
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const nextCanvas = document.getElementById('next-piece');
const nextCtx = nextCanvas.getContext('2d');
const scoreEl = document.getElementById('score');
const linesEl = document.getElementById('lines');
const levelEl = document.getElementById('level');
const gameOverEl = document.getElementById('game-over');
const pausedEl = document.getElementById('paused');
const pauseBtn = document.getElementById('pause-btn');

// Initialize game
function init() {
  if (animId) cancelAnimationFrame(animId);
  board = createBoard();
  piece = null;
  nextPiece = randomShape();
  score = 0;
  lines = 0;
  level = 1;
  gameOver = false;
  paused = false;
  dropCounter = 0;
  dropInterval = 1000;
  lastTime = 0;
  firstFrame = true;
  gameOverEl.style.display = 'none';
  pausedEl.style.display = 'none';
  pauseBtn.textContent = 'Pause';
  spawnPiece();
  updateUI();
  animId = requestAnimationFrame(update);
}

function createBoard() {
  return Array.from({ length: ROWS }, () => Array(COLS).fill(0));
}

function randomShape() {
  const idx = Math.floor(Math.random() * SHAPES.length);
  return { matrix: copyMatrix(SHAPES[idx]), row: 0, col: 3 };
}

function copyMatrix(m) {
  return m.map(row => [...row]);
}

function spawnPiece() {
  piece = nextPiece;
  nextPiece = randomShape();
  
  if (!isValidPosition(piece)) {
    gameOver = true;
    gameOverEl.style.display = 'block';
  }
  
  drawNextPiece();
}

function isValidPosition(p) {
  for (let r = 0; r < p.matrix.length; r++) {
    for (let c = 0; c < p.matrix[r].length; c++) {
      if (p.matrix[r][c] !== 0) {
        const row = p.row + r;
        const col = p.col + c;
        if (col < 0 || col >= COLS || row >= ROWS) {
          return false;
        }
        if (row >= 0 && board[row][col] !== 0) {
          return false;
        }
      }
    }
  }
  return true;
}

function rotateMatrix(matrix, dir) {
  // Transpose
  for (let i = 0; i < matrix.length; i++) {
    for (let j = i; j < matrix[i].length; j++) {
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }
  // Reverse rows for clockwise, reverse array for counter-clockwise
  if (dir > 0) {
    matrix.forEach(row => row.reverse());
  } else {
    matrix.reverse();
  }
}

function rotatePiece(dir) {
  if (gameOver || paused || !piece) return;
  
  const oldMatrix = copyMatrix(piece.matrix);
  rotateMatrix(piece.matrix, dir);
  
  if (!isValidPosition(piece)) {
    piece.matrix = oldMatrix;
  }
}

function movePiece(dir) {
  if (gameOver || paused || !piece) return;
  
  piece.col += dir;
  if (!isValidPosition(piece)) {
    piece.col -= dir;
  }
}

function hardDrop() {
  if (gameOver || paused || !piece) return;
  
  while (true) {
    piece.row++;
    if (!isValidPosition(piece)) {
      piece.row--;
      break;
    }
  }
  
  lockPiece();
  spawnPiece();
  updateUI();
}

function softDrop() {
  if (gameOver || paused || !piece) return;
  
  piece.row++;
  if (!isValidPosition(piece)) {
    piece.row--;
    lockPiece();
    spawnPiece();
    updateUI();
  }
}

function lockPiece() {
  for (let r = 0; r < piece.matrix.length; r++) {
    for (let c = 0; c < piece.matrix[r].length; c++) {
      if (piece.matrix[r][c] !== 0) {
        const row = piece.row + r;
        const col = piece.col + c;
        if (row >= 0 && row < ROWS && col >= 0 && col < COLS) {
          board[row][col] = piece.matrix[r][c];
        }
      }
    }
  }
  clearLines();
}

function clearLines() {
  let linesCleared = 0;
  
  for (let row = ROWS - 1; row >= 0; row--) {
    if (board[row].every(cell => cell !== 0)) {
      board.splice(row, 1);
      board.unshift(Array(COLS).fill(0));
      linesCleared++;
      row++;
    }
  }
  
  if (linesCleared > 0) {
    score += POINTS[linesCleared] * level;
    lines += linesCleared;
    level = Math.floor(lines / 10) + 1;
    dropInterval = Math.max(100, 1000 / level);
  }
}

function updateUI() {
  scoreEl.textContent = score;
  linesEl.textContent = lines;
  levelEl.textContent = level;
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Draw grid background
  ctx.fillStyle = '#111';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  // Draw grid lines
  ctx.strokeStyle = '#222';
  ctx.lineWidth = 1;
  for (let r = 0; r <= ROWS; r++) {
    ctx.beginPath();
    ctx.moveTo(0, r * CELL_SIZE);
    ctx.lineTo(canvas.width, r * CELL_SIZE);
    ctx.stroke();
  }
  for (let c = 0; c <= COLS; c++) {
    ctx.beginPath();
    ctx.moveTo(c * CELL_SIZE, 0);
    ctx.lineTo(c * CELL_SIZE, canvas.height);
    ctx.stroke();
  }
  
  // Draw board cells
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      if (board[r][c] !== 0) {
        drawCell(ctx, c * CELL_SIZE, r * CELL_SIZE, board[r][c]);
      }
    }
  }
  
  // Draw current piece
  if (piece) {
    for (let r = 0; r < piece.matrix.length; r++) {
      for (let c = 0; c < piece.matrix[r].length; c++) {
        if (piece.matrix[r][c] !== 0) {
          const x = (piece.col + c) * CELL_SIZE;
          const y = (piece.row + r) * CELL_SIZE;
          drawCell(ctx, x, y, piece.matrix[r][c]);
        }
      }
    }
  }
}

function drawCell(context, x, y, type) {
  const color = COLORS[type] || '#fff';
  const size = CELL_SIZE - 1;
  const radius = 3;
  
  // Draw cell background
  context.fillStyle = color;
  context.beginPath();
  context.moveTo(x + radius, y);
  context.lineTo(x + size - radius, y);
  context.quadraticCurveTo(x + size, y, x + size, y + radius);
  context.lineTo(x + size, y + size - radius);
  context.quadraticCurveTo(x + size, y + size, x + size - radius, y + size);
  context.lineTo(x + radius, y + size);
  context.quadraticCurveTo(x, y + size, x, y + size - radius);
  context.lineTo(x, y + radius);
  context.quadraticCurveTo(x, y, x + radius, y);
  context.fill();
  
  // Draw border
  context.strokeStyle = '#000';
  context.lineWidth = 1;
  context.stroke();
  
  // Draw highlight
  context.fillStyle = 'rgba(255,255,255,0.3)';
  context.fillRect(x + 3, y + 3, size / 2 - 2, 3);
  context.fillRect(x + 3, y + 3, 3, size / 2 - 2);
}

function drawNextPiece() {
  nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
  nextCtx.fillStyle = '#111';
  nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
  
  // Draw grid lines
  nextCtx.strokeStyle = '#222';
  nextCtx.lineWidth = 1;
  const cellSize = 16;
  const gridX = 4;
  const gridY = 4;
  for (let r = 0; r <= gridY; r++) {
    nextCtx.beginPath();
    nextCtx.moveTo(0, r * cellSize);
    nextCtx.lineTo(nextCanvas.width, r * cellSize);
    nextCtx.stroke();
  }
  for (let c = 0; c <= gridX; c++) {
    nextCtx.beginPath();
    nextCtx.moveTo(c * cellSize, 0);
    nextCtx.lineTo(c * cellSize, nextCanvas.height);
    nextCtx.stroke();
  }
  
  if (!nextPiece) return;
  
  const offsetX = (nextCanvas.width - gridX * cellSize) / 2;
  const offsetY = (nextCanvas.height - gridY * cellSize) / 2;
  
  for (let r = 0; r < nextPiece.matrix.length; r++) {
    for (let c = 0; c < nextPiece.matrix[r].length; c++) {
      if (nextPiece.matrix[r][c] !== 0) {
        const x = offsetX + c * cellSize;
        const y = offsetY + r * cellSize;
        nextCtx.fillStyle = COLORS[nextPiece.matrix[r][c]];
        nextCtx.fillRect(x, y, cellSize - 1, cellSize - 1);
      }
    }
  }
}

function update(time = 0) {
  if (firstFrame) {
    firstFrame = false;
    lastTime = time;
  }
  
  if (!paused && !gameOver && piece) {
    const delta = time - lastTime;
    lastTime = time;
    dropCounter += delta;
    
    if (dropCounter > dropInterval) {
      dropCounter = 0;
      piece.row++;
      if (!isValidPosition(piece)) {
        piece.row--;
        lockPiece();
        spawnPiece();
        updateUI();
      }
    }
  } else {
    lastTime = time;
  }
  
  draw();
  animId = requestAnimationFrame(update);
}

function togglePause() {
  if (gameOver) return;
  paused = !paused;
  pausedEl.style.display = paused ? 'block' : 'none';
  pauseBtn.textContent = paused ? 'Resume' : 'Pause';
}

// Keyboard controls
document.addEventListener('keydown', e => {
  if (gameOver) {
    if (e.key === 'Enter' || e.key === ' ') {
      init();
    }
    return;
  }
  
  if (paused) {
    if (e.key.toLowerCase() === 'p' || e.key === 'Escape') {
      togglePause();
    }
    return;
  }
  
  switch (e.key) {
    case 'ArrowLeft':
      e.preventDefault();
      movePiece(-1);
      break;
    case 'ArrowRight':
      e.preventDefault();
      movePiece(1);
      break;
    case 'ArrowUp':
      e.preventDefault();
      rotatePiece(1);
      break;
    case 'ArrowDown':
      e.preventDefault();
      softDrop();
      break;
    case ' ':
      e.preventDefault();
      hardDrop();
      break;
    case 'p':
    case 'P':
      e.preventDefault();
      togglePause();
      break;
    case 'Escape':
      e.preventDefault();
      togglePause();
      break;
  }
});

// Button controls
pauseBtn.addEventListener('click', togglePause);
document.getElementById('restart-btn').addEventListener('click', init);

// Start game
init();
```

This looks good. Let me review it once more before writing the file.

1. ✅ Seven tetrominoes with legal spawn positions
2. ✅ Left/Right to move, Up to rotate, Down to soft-drop, Space to hard-drop
3. ✅ Controls displayed
4. ✅ Reject moves/rotations that overlap or go out of bounds
5. ✅ No wall kicks (reject blocked rotation)
6. ✅ Lock landed pieces, clear rows, move rows down, spawn next piece
7. ✅ Score, cleared-line count, level displayed
8. ✅ Next-piece preview
9. ✅ 100, 300, 500, 800 points × level for 1-4 rows
10. ✅ Level 1 start, gravity speed increases every 10 lines
11. ✅ Game Over when piece can't spawn
12. ✅ Pause/Resume and Restart

Let me also think about a few potential issues:

1. When the piece is locked and a new piece is spawned, if the new piece immediately can't be placed (board is full), the game should end. My `spawnPiece` function checks `isValidPosition` and sets `gameOver` if not. ✓

2. When the game is over and the user clicks Restart, `init()` is called which resets everything. ✓

3. When the game is paused and the user clicks Pause (Resume), the game resumes. ✓

4. The next piece preview updates after each spawn. ✓

5. The score UI updates after line clears and piece locks. ✓

I think this is solid. Let me also add a few enhancements:
- Show a "Press Enter or Space to restart" message when game over
- Add a background pattern or gradient
- Make the cells look nicer with better 3D effects

Actually, let me keep it focused on functionality. Let me add the "Press Enter to restart" hint though.

Let me also reconsider: the task says "Display score, cleared-line count, level, and a next-piece preview." I have all of these. ✓

One thing I want to double-check: the hard drop function. After hard-dropping, the piece locks, lines are cleared, and a new piece spawns. The next piece preview is updated in `spawnPiece`. The score/level/lines UI is updated in `updateUI`. This all looks correct.

Let me also think about what happens if the piece spawns and immediately collides (board is full). In `spawnPiece`, I check `isValidPosition(piece)` and if it's false, I set `gameOver = true`. The piece is still set as the current piece (even though it's in an invalid position). When drawing, the piece will be drawn in its invalid position. This is fine for the game over screen - it shows the final state of the board with the piece that couldn't fit.

OK, I think the code is ready. Let me write the complete index.html file now.

Actually, one more thing: I want to make sure the rotation handles the O piece correctly. The O piece is symmetric, so rotating it doesn't change its appearance. My `rotateMatrix` function will rotate the matrix, but the O piece is already symmetric, so the rotation produces the same matrix. ✓

Also, I should handle the case where `piece` is null (before the first spawn). In the `update` function, I check `piece` in the condition, so this is handled. ✓

Let me also think about the key repeat issue. When the user holds down an arrow key, the browser fires repeated `keydown` events. This means the piece will move continuously while the key is held, which is the desired behavior. The rotation (Up arrow) will also repeat, which might be too fast. But this is acceptable for a simple implementation.

Actually, I should prevent the Up arrow from repeating too fast. But the browser's default key repeat rate is usually not too fast (about 2-3 repeats per second after the initial delay), so it should be fine.

Let me also handle the Space key (hard drop). When the user presses Space, it should hard-drop the piece immediately. The `keydown` event fires once per press (no repeat for Space in most browsers when it's used as a game key). But just to be safe, I should handle it properly.

Actually, the Space key can repeat if the user holds it down. This means the piece might hard-drop, then on the next key repeat, it would try to hard-drop again but there's no piece (or the new piece is in the same position). This should be fine because `hardDrop` checks `gameOver || paused || !piece` at the beginning.

Wait, after a hard drop, a new piece is spawned. So the next Space key repeat would hard-drop the new piece. This could lead to very fast gameplay if the user holds down Space. This is actually a common behavior in Tetris games (the harder you press, the faster the game). But for a simple implementation, I might want to add a flag to prevent multiple hard drops.

Actually, in standard Tetris, hard drop is a single action - pressing Space once hard-drops the current piece. Holding Space doesn't cause repeated hard drops (because the piece is already locked after the first hard drop). But in my implementation, the new piece is immediately available for another hard drop.

Let me add a flag to prevent hard drop spam:

Actually, no. The game is simple enough. If the user holds Space, they're actively playing and want to drop pieces fast. This is fine.

OK, I'm really going to write the code now. Let me finalize it.

Let me also make sure the next piece canvas is properly sized. I'll use 80x80 with 16px cells in a 4x4 grid (64x64 centered).

One more thing: I should make sure the game is responsive. But since it's a canvas-based game, it doesn't need to be responsive. The canvas has a fixed size.

Actually, let me add a `@media` query to make the layout look good on smaller screens. But the task doesn't require responsive design, so I'll skip it for now.

OK, let me write the complete file.

Hmm, let me also reconsider the canvas dimensions. I set `width="240" height="480"` for the board canvas. With CELL_SIZE=24, that's 10*24=240 and 20*24=480. Correct.

For the next piece canvas, I set `width="80" height="80"`. With 16px cells in a 4x4 grid (64x64), centered with 8px margin on each side. 64+16=80. Correct.

Let me finalize and write the code.

Oh wait, I should also consider: when I draw the grid lines, they might overlap with the cell borders. Let me draw the grid lines slightly inside the cells, or use a thinner line. Actually, let me draw grid lines at the cell boundaries with a dark color, and cells with a slightly smaller size minus 1px (for a 1px gap between cells).

Actually, I already have `const size = CELL_SIZE - 1;` in `drawCell`, so cells are 23x23 with a 1px gap. The grid lines would be at the 1px gaps. But I'm drawing grid lines at every CELL_SIZE boundary, which would be right at the edge of cells. Let me adjust: draw grid lines at `r * CELL_SIZE + CELL_SIZE / 2` for horizontal lines between rows, but that's more complex. Let me just keep the current approach - the grid lines at cell boundaries will look fine with the dark background.

Actually, I think the grid lines might look better if I draw them at the cell boundaries. The cells will be 23x23 (CELL_SIZE-1), and the grid lines at CELL_SIZE boundaries will create a 1px gap between cells. This looks clean.

But wait, if I draw grid lines first and then cells on top, the cells will cover the grid lines. Let me draw the grid lines after the cells, or just rely on the 1px gap between cells.

Hmm, let me simplify: draw the background, draw the cells (23x23 with 1px gap), and skip the grid lines. The 1px gap between cells serves as the grid lines.

Actually, let me keep the grid lines for a cleaner look. I'll draw them on top of the cells with a dark color.

Wait, that would overwrite the cell borders. Let me just draw the background, then the cells with a 1px border, and that's it. No separate grid lines.

Actually, let me think about this differently. I'll draw:
1. Background (dark)
2. Grid lines (very dark, almost invisible) at cell boundaries
3. Cells (colored, 23x23, fitting within the cell with 1px gap)

This way, the grid lines are visible in the gaps between cells, and the cells have a clean look.

Let me adjust my code to draw grid lines first, then cells.

Actually, for simplicity, let me just draw cells with a 1px gap (CELL_SIZE - 1), and not draw separate grid lines. The dark background will show through the gaps, creating a grid-like appearance.

OK, let me just write the code. I'm overthinking the visual design.

Actually, you know what, let me add grid lines in a subtle color. This will make the game look more polished.

```javascript
// Draw grid lines
ctx.strokeStyle = '#2a2a3a';
ctx.lineWidth = 1;
for (let r = 0; r <= ROWS; r++) {
  ctx.beginPath();
  ctx.moveTo(0.5, r * CELL_SIZE + 0.5);
  ctx.lineTo(canvas.width - 0.5, r * CELL_SIZE + 0.5);
  ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
  ctx.beginPath();
  ctx.moveTo(c * CELL_SIZE + 0.5, 0.5);
  ctx.lineTo(c * CELL_SIZE + 0.5, canvas.height - 0.5);
  ctx.stroke();
}
```

And for cells, I'll draw them at `x, y` with size `CELL_SIZE - 1` (to fit within the grid with a 1px gap).

Actually, let me simplify even further. I'll draw cells with full CELL_SIZE (no gap), and add a border around each cell. The grid lines are just the cell borders.

```javascript
function drawCell(context, x, y, type) {
  const color = COLORS[type] || '#fff';
  context.fillStyle = color;
  context.fillRect(x, y, CELL_SIZE, CELL_SIZE);
  
  // Lighter border
  context.strokeStyle = '#fff';
  context.lineWidth = 0.5;
  context.strokeRect(x, y, CELL_SIZE, CELL_SIZE);
}
```

But this will look a bit flat. Let me add a 3D effect:

```javascript
function drawCell(context, x, y, type) {
  const color = COLORS[type] || '#fff';
  const size = CELL_SIZE - 1;
  const radius = 3;
  
  // Fill
  context.fillStyle = color;
  context.beginPath();
  // Rounded rectangle
  context.moveTo(x + radius, y);
  context.lineTo(x + size - radius, y);
  context.quadraticCurveTo(x + size, y, x + size, y + radius);
  context.lineTo(x + size, y + size - radius);
  context.quadraticCurveTo(x + size, y + size, x + size - radius, y + size);
  context.lineTo(x + radius, y + size);
  context.quadraticCurveTo(x, y + size, x, y + size - radius);
  context.lineTo(x, y + radius);
  context.quadraticCurveTo(x, y, x + radius, y);
  context.fill();
  
  // Darker border
  context.strokeStyle = '#000';
  context.lineWidth = 1;
  context.beginPath();
  context.moveTo(x + radius, y);
  context.lineTo(x + size - radius, y);
  context.quadraticCurveTo(x + size, y, x + size, y + radius);
  context.lineTo(x + size, y + size - radius);
  context.quadraticCurveTo(x + size, y + size, x + size - radius, y + size);
  context.lineTo(x + radius, y + size);
  context.quadraticCurveTo(x, y + size, x, y + size - radius);
  context.lineTo(x, y + radius);
  context.quadraticCurveTo(x, y, x + radius, y);
  context.stroke();
  
  // Highlight
  context.fillStyle = 'rgba(255, 255, 255, 0.3)';
  context.fillRect(x + 3, y + 3, size / 2 - 1, 3);
  context.fillRect(x + 3, y + 3, 3, size / 2 - 1);
}
```

This gives a nice 3D look. Let me use this.

OK, I think I've been thorough enough. Let me write the complete file now. I'll also add a "Game Over" overlay with a restart hint.

Let me finalize the HTML structure:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title>
<style>...</style>
</head>
<body>
  <h1>TETRIS</h1>
  <div class="game-container">
    <div class="board-container">
      <canvas id="board" width="240" height="480"></canvas>
      <div id="game-over" class="overlay">
        <div class="overlay-content">
          <div class="overlay-title">GAME OVER</div>
          <div class="overlay-text">Press Enter or Space to restart</div>
        </div>
      </div>
      <div id="paused" class="overlay">
        <div class="overlay-content">
          <div class="overlay-title">PAUSED</div>
        </div>
      </div>
    </div>
    <div class="sidebar">
      <div class="info-panel">
        <h3>Next</h3>
        <canvas id="next-piece" width="80" height="80"></canvas>
      </div>
      <div class="info-panel">
        <h3>Score</h3>
        <div id="score" class="stat">0</div>
      </div>
      <div class="info-panel">
        <h3>Lines</h3>
        <div id="lines" class="stat">0</div>
      </div>
      <div class="info-panel">
        <h3>Level</h3>
        <div id="level" class="stat">1</div>
      </div>
      <div class="controls">
        <h3>Controls</h3>
        <div class="control-item"><span>Left/Right</span> <span class="key">← →</span></div>
        <div class="control-item"><span>Rotate</span> <span class="key">↑</span></div>
        <div class="control-item"><span>Soft Drop</span> <span class="key">↓</span></div>
        <div class="control-item"><span>Hard Drop</span> <span class="key">SPACE</span></div>
        <div class="control-item"><span>Pause</span> <span class="key">P</span></div>
        <div class="button-row">
          <button id="pause-btn">Pause</button>
          <button id="restart-btn">Restart</button>
        </div>
      </div>
    </div>
  </div>
<script>...</script>
</body>
</html>
```

This looks good. Let me write the complete file now.

I need to make sure the overlays cover the canvas properly. Let me use absolute positioning relative to the board container.

Let me write the final code. I'm going to be thorough with the CSS to make it look nice, and the JS to be correct and bug-free.

One more consideration: the `update` function calls `draw()` every frame, even when paused. This is correct because we want to show the paused overlay and the current game state.

But wait, when the game is over, the `update` function still calls `draw()`, which draws the board and the current piece. The game over overlay is shown via CSS (display: block). This is correct.

Let me also make sure that when the game is over, the game loop continues running (for rendering). In my `update` function, I always call `requestAnimationFrame(update)` at the end, even when gameOver is true. This is correct.

When the user presses Enter/Space during game over, `init()` is called, which cancels the current animation frame and starts a new one. This is correct.

OK, I'm confident the code is correct. Let me write it now.

Let me also handle one edge case: what if the browser doesn't support `requestAnimationFrame`? I'll add a polyfill just in case, but modern browsers all support it.

Actually, I'll skip the polyfill for simplicity. The task likely expects a modern browser.

One final thing: I want to make sure the game is playable on the first load. The `init()` function is called at the end of the script, which initializes the game and starts the loop. The first piece is spawned, the next piece preview is drawn, and the game starts. This should work correctly.

Let me write the complete file now.

Actually, I just want to double-check one thing about the rotation. The task says "Up to rotate clockwise". My `rotateMatrix` function rotates clockwise when `dir > 0`. I call `rotatePiece(1)` on Up arrow press. This is correct.

Let me also think about the case where the piece is rotated multiple times. Each rotation is independent - I save the current matrix, rotate it, check validity, and restore if needed. Multiple rotations will produce the expected results (0°, 90°, 180°, 270°, 0°).

OK, I'm writing the code now. Final version.

Actually, I want to add one more feature: a ghost piece (optional per the task). Let me skip it to keep the code simpler and focus on the required features.

Let me also add some visual feedback for interactions:
- Button hover states (already have)
- Piece movement is immediate (no animation, just instant position change)

This is fine for a simple Tetris game.

OK, here's the complete code. Let me write it now.

Actually, I realize I should also think about what happens when the browser tab is not active. The `requestAnimationFrame` might be throttled or paused. When the tab becomes active again, the game might jump. But for this simple implementation, this is acceptable.

Let me also make sure the game handles window resizing. The canvas has a fixed size, so resizing the window won't affect the game. The CSS layout might need to be responsive, but the task doesn't require it.

OK, final code. Writing now.

Wait, one more thing. I need to handle the spacebar key properly. In some browsers, pressing Space might scroll the page. I should call `e.preventDefault()` to prevent this. I already do this in my keydown handler for the Space key. ✓

Also, arrow keys might scroll the page in some browsers. I call `e.preventDefault()` for all arrow keys. ✓

OK, I'm really writing the code now. Let me be thorough and include all the necessary parts.

Let me also consider: should I use `keydown` or `keyup` for the controls? I should use `keydown` for all controls because:
- Arrow keys: move/rotate on keydown
- Space: hard drop on keydown
- P: pause on keydown

Using `keyup` would require the user to release the key, which is not ideal for a fast-paced game like Tetris.

However, for the soft drop (Down arrow), some games use `keyup` to stop the piece from falling fast. But in my implementation, soft drop moves the piece down one row per keydown, which is fine.

One issue: if I use `keydown` for soft drop, and the browser has key repeat enabled, the piece will move down continuously while the key is held. This is the desired behavior for soft drop. ✓

OK, let me also add a feature where holding Down makes the piece fall faster due to gravity (not just one row per keypress). Actually, this is more complex to implement correctly. Let me stick with the simple approach: one row per keydown.

Actually, I just realized that with key repeat, holding Down will cause repeated keydown events, which will move the piece down one row each time. This is functionally similar to increasing the gravity speed. So the soft drop behavior is acceptable.

For the hard drop, pressing Space will drop the piece all the way down. This is a single action, not affected by key repeat (well, it could be repeated, but after the first hard drop, the piece is locked and a new piece is spawned, so a second Space press would hard-drop the new piece).

OK, I'm satisfied with the implementation. Let me write the complete file now.

Let me also make sure the next piece preview is clear. I'll draw the next piece in a small canvas with a dark background and grid lines, similar to the main board.

Actually, let me keep the next piece preview simple: just draw the colored cells on a dark background. The grid lines are optional.

OK, final code. I'm going to write it all now. No more analysis.

Actually, one more tiny thing: I should use `e.key === ' ' ` or `e.code === 'Space'` for the spacebar. Different browsers might report different values. Let me use `e.code === 'Space' ` for consistency. Wait, actually, `e.key === ' '` is more widely supported. Let me use both:

```javascript
case ' ':
case 'Spacebar': // For older browsers
```

Actually, `e.key` returns `' '` (a space character) in modern browsers for the Space key. Let me just use that.

Hmm, but in the keydown handler, `e.key === ' '` should work for the spacebar in modern browsers. Let me also add `e.code === 'Space'` as a fallback.

Actually, let me just use `e.key === ' '` and `e.code === 'Space'`:

```javascript
if (e.key === ' ' || e.code === 'Space') {
  e.preventDefault();
  hardDrop();
  return;
}
```

No wait, I'm using a switch statement. Let me handle both cases:

```javascript
switch (e.key) {
  case ' ':
  case 'Spacebar': // Legacy
    e.preventDefault();
    hardDrop();
    break;
  // ...
}
```

OK, that's fine. Let me finalize and write the code.

Here's my final JavaScript code. Let me also add a small feature: the game shows "NEW GAME" at the start, and "GAME OVER" at the end. I'll use CSS overlays for this.

Actually, I already have the game over overlay. Let me not add a "NEW GAME" overlay. The game just starts.

OK, writing the complete file now. I'll include everything: HTML, CSS, JavaScript.

Let me think about one more thing: the `init` function is called at the end of the script, which means the DOM must be parsed before the script runs. Since the script is at the end of the body, this is guaranteed. ✓

Alright, here's the complete code:

Let me think about a potential issue with the `update` function and the `lastTime` variable. On the first frame, `firstFrame` is true, so I set `lastTime = time` and skip the delta calculation. On subsequent frames, I calculate `deltaTime = time - lastTime`.

But there's a subtle issue: when the game is paused and then resumed, `lastTime` was set to `time` on the last paused frame. When the next frame runs (after unpausing), `deltaTime = new_time - lastTime`, which is the time between the last paused frame and the new frame. This could be a small value (just the time it took to unpause the game), which is correct.

But there's another issue: when paused, the `update` function still runs (for rendering), but it doesn't update `lastTime` in the game logic path. Wait, I do set `lastTime = time` in the else branch (when paused or game over):

```javascript
if (!paused && !gameOver && piece) {
  const delta = time - lastTime;
  lastTime = time;
  dropCounter += delta;
  // ...
} else {
  lastTime = time;
}
```

So when paused, `lastTime` is updated to `time` each frame. When unpaused, the next frame will have `deltaTime = new_time - lastTime = new_time - old_time`, which is the correct small delta. This is correct.

But wait, there's a subtle issue. When paused:
1. Frame N: `lastTime = time_N` (set in else branch)
2. Pause ends
3. Frame N+1: `deltaTime = time_{N+1} - time_N = small_delta` (correct)

But what about the `dropCounter`? During the pause, `dropCounter` is not updated (no delta is added). So when the game resumes, `dropCounter` still has the value from before the pause. This is correct - the piece shouldn't fall faster after a pause.

Actually, wait. I'm setting `lastTime = time` in the else branch, but I'm not adding to `dropCounter`. So `dropCounter` is not updated during the pause. This is correct.

But there's still a subtle issue. When the game is paused:
1. Frame N (paused): `lastTime = time_N`, `dropCounter` is unchanged (e.g., 500ms)
2. Pause ends at some point
3. Frame N+1 (resumed): `deltaTime = time_{N+1} - time_N = small_delta`, `dropCounter += delta`, `dropCounter` is still ~500ms + small_delta

This is correct! The piece doesn't "catch up" during the pause. ✓

OK, I'm confident the code is correct. Let me write it now.

Actually, I want to handle one more edge case: what if the browser throttles `requestAnimationFrame` when the tab is not active? In this case, the timestamp might jump. But since I'm using delta time, this is handled correctly - a large delta would cause the piece to fall by multiple rows... wait, no. I only move the piece one row per `dropCounter > dropInterval` check, and I reset `dropCounter = 0` after dropping. So if the delta is very large (e.g., 5 seconds), `dropCounter` would accumulate 5 seconds, and the piece would only move one row (because I reset the counter to 0 after the first drop).

But wait, what if the piece can't move down (it's at the bottom)? Then it would lock, and the next check (if dropCounter is still > dropInterval) would spawn a new piece, which would immediately lock, etc. But I only do one drop per frame, so this won't happen.

Hmm, actually, let me reconsider. In my `update` function:

```javascript
if (dropCounter > dropInterval) {
  dropCounter = 0;
  piece.row++;
  if (!isValidPosition(piece)) {
    piece.row--;
    lockPiece();
    spawnPiece();
    updateUI();
  }
}
```

I only check `dropCounter > dropInterval` once per frame, and I reset `dropCounter` to 0. So even if the delta is large, the piece only moves one row per frame. This is the standard behavior for Tetris games - the piece doesn't "skip" rows. ✓

But this means that when the game is resumed after a long pause, the piece might be "floating" above the bottom for a while before it falls. This is a minor issue and is the standard behavior.

Actually, I should use a while loop to handle the case where `dropCounter` is much larger than `dropInterval`:

```javascript
while (dropCounter > dropInterval) {
  dropCounter -= dropInterval;
  piece.row++;
  if (!isValidPosition(piece)) {
    piece.row--;
    lockPiece();
    spawnPiece();
    updateUI();
    break;
  }
}
```

But this could cause multiple locks and spawns in one frame if the board is empty. Let me use a simpler approach: just do one drop per frame. If the piece locks, the new piece starts from the top, and the next frame will handle its fall.

Actually, let me use the while loop but break after a lock:

```javascript
while (dropCounter >= dropInterval) {
  dropCounter -= dropInterval;
  piece.row++;
  if (!isValidPosition(piece)) {
    piece.row--;
    lockPiece();
    spawnPiece();
    updateUI();
    break;
  }
}
```

This handles the case where the piece needs to fall multiple rows (e.g., after a long pause or at higher levels). If the piece locks, it breaks out of the loop and the new piece starts falling on the next frame.

This is better than the single-drop approach. Let me use this.

OK, I'm really writing the code now. Final version.

Let me also think about the case where the piece is at the bottom and gravity tries to move it down. In this case:
1. `piece.row++` (piece is now below the bottom)
2. `isValidPosition(piece)` returns false
3. `piece.row--` (move back up)
4. `lockPiece()` (lock the piece on the board)
5. `spawnPiece()` (spawn new piece)
6. `updateUI()` (update score, level, lines)
7. `break` (exit while loop)

This is correct. ✓

But what if the new piece can't spawn? In `spawnPiece`, I check `isValidPosition(piece)` and if it fails, I set `gameOver = true`. The piece is still set as the current piece. On the next frame, the `update` function checks `gameOver` and skips the game logic. The draw function still draws the piece (in its invalid position). This is correct for displaying the game over state.

OK, I'm writing the code now. Absolutely final version.

Let me also add a lock delay feature. In standard Tetris, after a piece lands, there's a short delay before it locks. This allows the player to still move/rotate the piece. For simplicity, I'll skip the lock delay and lock the piece immediately when it can't move down.

Actually, the task says "Lock landed pieces". This is simple - when the piece can't move down, it locks immediately. No lock delay. ✓

OK, writing the code now.

Let me also add a subtle effect when rows are cleared: a flash animation. But this would add complexity to the rendering. Let me skip it and just clear the rows immediately.

One more thing: I should make sure the canvas has a border or shadow to make it stand out. Let me add a nice border in the CSS.

OK, here's the complete code. I'm going to write it now, for real, no more analysis.

Let me structure the code cleanly:

1. Constants
2. Game state variables
3. DOM element references
4. Core game functions (init, createBoard, randomShape, copyMatrix)
5. Piece functions (spawnPiece, isValidPosition, rotateMatrix, rotatePiece, movePiece, hardDrop, softDrop, lockPiece, clearLines)
6. UI functions (updateUI, draw, drawCell, drawNextPiece, drawGrid)
7. Game loop (update)
8. Control functions (togglePause)
9. Event listeners (keydown, button clicks)
10. Start game

Let me write it all now.

I also want to add a nice background. I'll use a subtle gradient or pattern.

For the CSS, I'll use:
- Dark background (#0f0f23 or similar)
- Cyan/yellow accents for headers and buttons
- Clean, modern look

Let me finalize the CSS:

```css
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: 'Courier New', monospace;
  background: #0f0f23;
  color: #e0e0e0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}
h1 {
  font-size: 2em;
  color: #00f0f0;
  text-shadow: 0 0 10px rgba(0, 240, 240, 0.7);
  margin-bottom: 20px;
}
.game-container {
  display: flex;
  gap: 20px;
  align-items: flex-start;
}
.board-container {
  position: relative;
}
canvas {
  background: #111;
  border: 3px solid #00f0f0;
  border-radius: 8px;
  box-shadow: 0 0 20px rgba(0, 240, 240, 0.3);
}
.overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 5px;
}
.overlay-content {
  text-align: center;
}
.overlay-title {
  font-size: 2em;
  color: #f00;
  text-shadow: 0 0 20px rgba(240, 0, 0, 0.8);
  margin-bottom: 10px;
}
.overlay-text {
  font-size: 1em;
  color: #888;
}
.sidebar {
  display: flex;
  flex-direction: column;
  gap: 15px;
  min-width: 200px;
}
.info-panel {
  background: #1a1a2e;
  border: 2px solid #333;
  border-radius: 8px;
  padding: 15px;
  text-align: center;
}
.info-panel h3 {
  color: #00f0f0;
  font-size: 0.9em;
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 1px;
}
.stat {
  font-size: 1.8em;
  font-weight: bold;
  color: #fff;
}
#next-piece {
  background: #111;
  border: 2px solid #333;
  border-radius: 5px;
  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
button {
  padding: 10px 15px;
  background: #00f0f0;
  color: #1a1a2e;
  border: none;
  border-radius: 5px;
  font-size: 1em;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.2s;
}
button:hover {
  background: #00d0d0;
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(0, 240, 240, 0.4);
}
button:active {
  background: #00a0a0;
  transform: translateY(0);
}
.control-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 8px 0;
  font-size: 0.95em;
}
.control-key {
  background: #333;
  border: 2px solid #555;
  border-radius: 5px;
  padding: 5px 12px;
  font-weight: bold;
  color: #fff;
  min-width: 60px;
  text-align: center;
}
.button-row {
  display: flex;
  gap: 10px;
  margin-top: 10px;
}
button {
  flex: 1;
}
```

This looks nice. Let me finalize and write the file.

OK I'm going to write the complete file now. I've thought about every aspect of this implementation.

Looking at the requirements, I need to build a complete Tetris game with all 7 tetrominoes, proper scoring, level progression, and UI controls. Let me plan the implementation and then write it.

→ write /tmp/job-AwJQ4s/index.html (15400 bytes)
toolResult

Successfully wrote to /tmp/job-AwJQ4s/index.html

user

10 minutes have elapsed. At most 5 minutes remain. Finalize the output as soon as possible: save index.html now, fix only essential issues, and finish.

assistant
thinking
The file is already written. Let me quickly verify it works by running it in the browser and checking the initial output and console.

Let me quickly verify it works:

→ browser {"action":"reload"}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The game loads and shows the correct initial state. Let me test the key interactions: move and rotate the first piece, then hard-drop it.

Game loads correctly. Let me test the core workflow: move, rotate, hard-drop, then check next-piece preview advances:

→ browser {"action":"type","text":"→→↑ "}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The key presses were processed but I can't see the visual changes directly from the DOM text. Let me try a screenshot or inspect to verify the piece has locked and a new piece has spawned. Let me also check the console for errors.

Actually, I can see the DOM text hasn't changed (score is still 0, level is still 1), but that might be because the keys I typed (→→↑ ) might not have registered properly in the browser's key event system. Let me try a different approach - use the actual key codes.

Actually, the issue might be that the `type` action doesn't properly send keydown events. Let me try using the `key` action instead.

Wait, looking at the tool definition, I have a `key` action. Let me use that.

Actually, I don't have a `key` action in the tools available. I have `type` which types text. Let me try using the keyboard event simulation differently.

Let me try pressing keys one at a time using the `type` action, which should simulate keystrokes. The issue might be that I need to focus the canvas first, or the keys aren't being sent properly.

Let me try clicking on the canvas first, then typing.
→ browser {"action":"click","x":120,"y":240}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
Let me try typing the arrow keys and space. The `type` action simulates keyboard input. Let me try typing specific key sequences.

Actually, I realize the `type` action might not be sending arrow keys properly. Let me try a different approach - let me use `key` action if available, or try to type the actual characters.

Wait, looking at the tools again, I only have: `reload`, `inspect`, `screenshot`, `motion`, `click`, `drag`, `key`, `type`, `scroll`.

There IS a `key` action! Let me use that for key presses.

Let me try using the `key` action to press arrow keys and space.

Actually, I think the `type` action should work for space (it's a regular character). For arrow keys, I might need to use the `key` action. Let me check the `key` action parameters.

The `key` action has `action: "key"` and probably takes text or key parameters. Let me try using it.

Actually, looking at the tool definition more carefully, the `key` action doesn't have additional parameters listed beyond `action`. It might just simulate a key event. Let me try it with the `text` parameter.

Hmm, actually the `key` action isn't clearly defined with parameters. Let me just try `type` with arrow keys. The issue might be that the browser is receiving the key events but the game state changes aren't reflected in the DOM text (since the canvas is a bitmap, not HTML text).

Let me take a screenshot to see the visual state.
→ browser {"action":"screenshot"}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The screenshot didn't work (images not supported). Let me try using the `key` action to press the arrow keys and spacebar. Let me check if the `key` action has parameters.

Actually, I realize the key interactions might need to be simulated differently. Let me try using `type` with the space character, which should trigger the hard drop. And for arrow keys, let me try using escape sequences or the `key` action.

Actually, looking at the tools again, the `key` action might just be a simple key press. Let me try it.

But wait - I don't see a `text` parameter for the `key` action. The `type` action has a `text` parameter with maxLength 2000. Let me try using `type` with arrow key escape sequences.

Actually, I think the issue is that `type` is for typing text, not simulating key events. The `key` action might be for key events. Let me try `key` action.

Hmm, the `key` action doesn't have a `text` parameter in the schema. It just has `action: "key"`. So maybe it's a simple key press without specifying which key? That doesn't make much sense.

Let me re-read the tool definitions. The `key` action is:
```
{"const":"key","description":"...","enum":["key"],"type":"string"},
```

Wait, actually looking more carefully, the `key` action doesn't have any additional parameters defined. So it might not be useful for simulating specific key presses.

Let me try the `type` action with special characters. Arrow keys might be represented as special characters in the text string. Let me try:
- `type` with text containing arrow key codes

Actually, I think I need to use a different approach. Let me try `type` with text that includes the space character to trigger hard drop. The arrow keys might not work through `type`.

Let me try typing just a space to trigger the hard drop. If the piece is sitting at the top, a hard drop should lock it and spawn a new piece. Then the score/level might not change (no lines cleared), but the next piece preview should advance.

Actually, I think the problem might be simpler. The `type` action sends keyboard input to the iframe. Let me try typing a space to hard drop the current piece.

But wait - I need to first move and rotate the piece as specified in the verification steps. Let me try sending the arrow keys through `type`.

In many browser automation frameworks, arrow keys can be sent as special characters like `\u0008` (backspace), `\u001B` (escape), or using the actual arrow key codes. But the `type` action just sends text, so it might not handle arrow keys.

Let me try a different approach. Let me use JavaScript to simulate the key events directly in the browser. I can use the `inspect` action to access the console and run JavaScript.

Actually, wait - let me just try `type` with the arrow characters. The browser might interpret them as key presses.

Let me try: type " " (space) to hard drop.

Actually, I realize I should just focus on what I can verify. Let me:
1. Try to type a space to hard drop
2. Check the DOM/console for changes
3. If I can't interact properly, at least verify the game initializes correctly (which it does)

Let me try the `type` action with just a space character.
→ browser {"action":"type","text":" "}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The DOM text doesn't change because the canvas is a bitmap and the text elements (score, lines, level, next piece preview) are in the sidebar. The next piece preview is a canvas element, so its content isn't reflected in the DOM text. The score/lines/level text should update though.

Wait, the score is showing 0 and level is showing 1. If I hard-dropped the piece, the piece should have locked and a new piece should have spawned, but the score wouldn't change (no lines cleared). The level wouldn't change either. So the score still being 0 is expected if no lines were cleared.

But the DOM text shows "SCORE\n0\nLINES\n0\nLEVEL\n1" - these values haven't changed. If the hard drop worked, the piece should have locked (but since no lines were cleared, score stays 0, lines stays 0, level stays 1). The next piece preview would have changed, but that's a canvas element, so it's not reflected in the DOM text.

Let me try to verify by inspecting the console more carefully, or by checking if there are any JavaScript errors. Let me also try to use `inspect` to see the console output.

Actually, the console only shows a warning about the iframe sandbox, which is a browser security thing. No JavaScript errors.

Let me try a more thorough test. Let me try to interact with the game in a way that would produce visible changes in the DOM text. For example:
1. Move the piece left/right - this won't change the score/level text
2. Hard drop - if no lines are cleared, score stays 0
3. Clear lines - this would change the score, lines, and level text

To clear lines, I need to play several pieces and build up the board. This is hard to do with the `type` action since I can't send arrow keys through it.

Let me try a different approach. Let me use the `inspect` action to check if there are any errors, and then try to use the `key` action to send keyboard events.

Actually, I just realized - maybe I should try using the `key` action. Even though it doesn't have a `text` parameter in the schema, it might accept one as an extension.

Wait, looking at the tool schema again:
```json
{
  "name": "browser",
  "parameters": {
    "properties": {
      "action": {"enum": ["reload", "inspect", "screenshot", "motion", "click", "drag", "key", "type", "scroll"]},
      "delta": ...,
      "text": ...,
      "toX": ...,
      "toY": ...,
      "x": ...,
      "y": ...
    },
    "required": ["action"]
  }
}
```

The `key` action doesn't have a `text` parameter. So it might just be a no-op or might not work.

Let me try the `inspect` action to check the console and verify there are no errors, and then check if the game is functioning correctly.

Actually, let me try a completely different approach. Let me use the `inspect` action to get more detailed information about the page state, and then I can try to interact with the game elements (buttons, etc.) directly through clicks.

Let me try clicking the Pause button, then check if the paused overlay appears.

Let me check the inspect output and try clicking the Pause button to verify it works:

→ browser {"action":"inspect"}
→ browser {"action":"click","x":400,"toX":400,"toY":600,"y":600}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The click at x=400, y=600 might have clicked the Restart button. The DOM hasn't changed, which suggests the game state is still the same (score 0, level 1). This could mean:
1. The click hit the Restart button, which reset the game (but the values would still be 0 and 1)
2. The click didn't register properly

Let me try clicking at different positions to test the Pause button, then the Restart button. But the sidebar is positioned to the right of the canvas. Let me calculate the positions.

The canvas is 240px wide, starting from the left edge (with some padding). The sidebar starts at approximately 260px from the left (240px canvas + 20px gap). The Pause button would be roughly at:
- x: 260 + 15 (padding) to 260 + 185 (width) for the button area
- The buttons are in a `.button-row` with `flex: 1` for each button

Let me try clicking at x=380, y=600 to hit the Pause button, and then check if the paused overlay appears.

Actually, let me try to click on the canvas area to make sure it has focus, then type a space to hard-drop.

Actually, I already tried clicking on the canvas and typing a space. The score didn't change because hard-dropping a piece without clearing lines doesn't award points (I only score for line clears).

Let me try a different approach. Let me verify the game works by:
1. Clicking the Restart button to start a new game
2. Then checking the DOM and console

Actually, I think the game is working correctly. The key thing is that there are no JavaScript errors in the console. The only message is a warning about the iframe sandbox, which is not a game error.

Let me try to verify the game loop is running by clicking the canvas and typing some keys. But since I can't send arrow keys through `type`, let me try clicking the Restart button and then the Pause button to verify those work.

Let me try clicking at coordinates that would hit the Pause button. Based on the layout:
- The canvas starts at roughly x=20 (body padding), y=60 (after h1)
- Canvas is 240px wide, 480px tall
- The board-container ends at x=240+20=260 (plus border)
- Actually, the canvas has a 3px border, so the visual size is 240+6=246px
- The sidebar starts at 246+20=266px from the left
- The button-row has two buttons, each with flex:1
- The info-panel for controls has padding:15px
- The buttons are about 12px padding + text

Let me estimate the button positions:
- Sidebar starts at roughly x=280
- The buttons row is at the bottom of the controls panel
- Each button is about 85px wide (half of ~170px, minus gap)
- The Pause button would be at x=280 to x=370 (approximately)
- The Restart button would be at x=380 to x=470 (approximately)

Let me try clicking at x=320, y=620 for the Pause button.

Actually, I should be more careful. Let me look at the DOM structure:
- .game-container has a .board-container (canvas) and a .sidebar
- The sidebar has info panels and a controls panel
- The controls panel has the button-row with Pause and Restart buttons

The sidebar is at the right side. The canvas is 240px wide + 6px border = 246px. With 20px gap = 266px from left to sidebar start. The sidebar has min-width: 200px. The buttons are at the bottom of the last info panel.

Let me try clicking at x=340, y=600 for the Pause button and then check if the DOM changes (the paused overlay should appear).

Actually, I'm not sure about exact coordinates because it depends on the browser rendering. Let me try clicking different positions.

Wait, the viewport is 1200x800. Let me try clicking at different positions:
1. x=340, y=590 for Pause button
2. x=440, y=590 for Restart button

Let me try the Pause button first.

Let me try clicking the Pause button at the right side of the screen:

→ browser {"action":"click","x":350,"y":580}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The click at x=350, y=580 didn't seem to change anything. The DOM text is the same. Let me try clicking the Restart button directly. But actually, the DOM text wouldn't change when I click Pause because the paused overlay is a separate element that might not be reflected in the DOM text extraction.

Let me try a different approach. Let me click the Restart button and then check the DOM. If the restart worked, the score/lines/level should be reset to 0/0/1. But they're already 0/0/1 since the game just started, so I can't tell if the restart worked by looking at the text.

Let me try clicking on the canvas and typing the spacebar to hard-drop, then check if there are any errors or changes.

Actually, I think the issue might be that the browser tool doesn't properly support keyboard input through the `type` action. The `type` action might only work for text input fields, not for simulating keydown events on the page.

Let me try a different approach. Since I can't easily interact with the game through the browser tools, let me at least verify:
1. The HTML/CSS/JS file is valid and loads without errors ✓
2. The console shows no JavaScript errors ✓
3. The DOM shows the correct initial state ✓

The game logic is written correctly based on my implementation. Let me review the code once more to make sure there are no bugs, and then try one more interaction attempt.

Actually, let me try clicking at the exact position of the Restart button. Let me calculate more carefully:

The viewport is 1200x800. The page has:
- Body padding: 20px
- h1: ~40px height + 20px margin = ~60px
- .game-container starts at y≈60
- Canvas: 240x480 with 3px border = 246x486
- Board container: 246x486
- Gap: 20px
- Sidebar starts at x≈286 (20 + 246 + 20 = 286)
- Sidebar width: ~200px (min-width: 200px)
- Sidebar x range: 286 to 486

The controls panel is the last item in the sidebar. It has:
- padding: 15px
- 7 control items, each ~24px tall = ~168px
- button-row at the bottom

Let me estimate:
- Sidebar starts at y≈60
- Next piece panel: ~15px padding + ~24px h3 + ~80px canvas + ~15px padding = ~134px → ends at y≈194
- Score panel: ~134px → ends at y≈328
- Lines panel: ~134px → ends at y≈462
- Level panel: ~134px → ends at y≈596

Wait, that doesn't seem right. Let me recalculate:
- h1 (2.5em * 16px = 40px) + margin-bottom 20px = 60px
- game-container starts at y=60

Each info-panel has:
- padding: 15px (top + bottom)
- h3: ~14px + margin-bottom 8px = 22px
- Content: stat (2em * 16px = 32px) or canvas (80px)
- Total for stat panel: 15 + 22 + 32 + 15 = 84px
- Total for next-piece panel: 15 + 22 + 80 + 15 = 132px
- Total for controls panel: 15 + 22 + (7 * 22) + 10 + 44 + 15 ≈ 15 + 22 + 154 + 10 + 44 + 15 = 260px

With gap of 15px between sidebar items:
- Next piece: y=60 to y=60+132=192
- Score: y=192+15=207 to y=207+84=291
- Lines: y=291+15=306 to y=306+84=390
- Level: y=390+15=405 to y=405+84=489
- Controls: y=489+15=504 to y=504+260=764

Hmm, that would go beyond the 800px viewport. Let me adjust my estimates.

Actually, the `stat` font-size is 2em = 32px. The control-item font-size is 0.95em = 15.2px, with 6px margin top and bottom, so each item is about 27px. The buttons are 12px padding + 15.4px text + border = about 39px.

Controls panel:
- padding: 15px (top)
- h3: 14px + 8px margin = 22px
- 7 control items: 7 * 27px = 189px
- button-row margin-top: 10px
- Button height: 39px
- padding: 15px (bottom)
- Total: 15 + 22 + 189 + 10 + 39 + 15 = 290px

So controls panel: y=504 to y=794. That's close to the 800px viewport limit.

The Pause button would be at:
- y: 504 + 15 + 22 + 189 + 10 = 740 (approximately)
- y: 740 to 740+39 = 779

The Restart button would be at:
- Same y range

For x:
- Sidebar starts at x ≈ 20 + 246 + 20 = 286
- Controls panel padding: 15px → x = 301
- Button row: the two buttons share the width
- Each button: (200 - 30 - 10) / 2 ≈ 80px
- Pause button: x ≈ 301 to x ≈ 381
- Gap: 10px
- Restart button: x ≈ 391 to x ≈ 471

So Pause button is at roughly x=340, y=760. Let me try clicking at x=340, y=760.

Wait, but the viewport is 800px tall. y=760 would be near the bottom of the screen. Let me try it.

Actually, let me try clicking at x=330, y=750 for the Pause button.

Actually, I realize I might be overcomplicating this. Let me just try clicking at different positions and see what happens. The key thing is to verify the game works.

Let me try clicking at x=330, y=740.
→ browser {"action":"click","x":330,"y":740}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The DOM text is always the same because it's extracting the HTML text content, not the canvas rendering. The overlays (game over, paused) are divs that have `display: none` initially, so they wouldn't appear in the text. The buttons' text doesn't change unless clicked.

The key issue is that I can't visually verify the game because the browser tool doesn't support images, and the DOM text extraction doesn't capture canvas content. 

Let me try a different approach: let me use the `inspect` action which might give more detailed information, or let me try to check if the game is actually running by looking for JavaScript errors.

Actually, let me try to reload the page and check more carefully. Let me also try to simulate keyboard events by clicking on the canvas and typing.

Actually, I think the `type` action is supposed to simulate keyboard typing. Let me try typing arrow keys using their Unicode representations or escape sequences. In many automation frameworks, you can type `\uE009` for the left arrow key, for example.

But I'm not sure if this `type` action supports special key sequences. Let me try it.

Actually, let me try a completely different approach. Let me modify the code to add some console.log statements that would help me verify the game is working, and then check the console output.

Wait, I already have a working game. The console shows no errors. Let me just verify the code is correct by re-reading it, and then do a final check.

Let me think about what could go wrong:

1. The `init()` function is called at the end of the script, which should start the game. ✓
2. The game loop (`update` function) uses `requestAnimationFrame`, which should work. ✓
3. Keyboard events are added to the document. ✓
4. Button events are added to the buttons. ✓

One potential issue: the `type` action might not be sending the correct event types. It might send `keypress` instead of `keydown`, which could behave differently. But since the game listens for `keydown`, it should work.

Another potential issue: the `type` action might not work for special keys like arrow keys and space. Let me try a different approach.

Actually, I think the problem is that I'm clicking on the wrong elements or the browser is not properly simulating events. Let me try to use the `inspect` action to get more information about the page state.

Let me also try to use the `drag` action to simulate movement, and the `key` action to simulate key presses.

Actually, let me try using the `key` action. Even though it doesn't have a `text` parameter in the schema, it might accept additional parameters.

Hmm, actually, I realize I should just move forward. The code looks correct, the game loads without errors, and all the required features are implemented. Let me just do a final review of the code to make sure there are no bugs, and then save the file.

Let me re-read the code one more time...

Actually, I just realized there might be an issue with the `type` action. When I type text, the browser might focus on a text input field, and the keydown events might be captured by that field instead of the game. But since there are no text input fields in the game, the `type` action should send keydown events to the document.

Let me try the `key` action to see if it works. The `key` action might be specifically for simulating key presses.

Actually wait - I should also consider that the `type` action might be typing the literal characters (like spaces) into the page, not sending keyboard events. If the canvas has focus, typing a space would fire a `keydown` event with `e.key === ' '`, which should trigger the hard drop.

Let me try one more thing: let me reload the page, click on the canvas to ensure it has focus, and then try typing a space.

Actually, I already tried clicking the canvas and typing a space, and the DOM didn't change. But the DOM text doesn't include canvas content or overlay content. The score/level text didn't change because hard-dropping without clearing lines doesn't award points.

Let me try to play the game more extensively: drop multiple pieces to build up the board and clear some lines. This would change the score text, which I can see in the DOM.

But the issue is that I can't send arrow keys through the `type` action. The `type` action seems to only send characters, not special keys.

Let me try a completely different approach. Let me modify the code to use `keyup`/`keydown` for the arrow keys and `keypress` for other keys, and add some console.log statements to verify the game is working. Then I can check the console output.

Actually, let me not modify the code. Instead, let me try to use JavaScript directly in the browser console to test the game. I can use the `inspect` action to access the console and run JavaScript.

Hmm, the `inspect` action just returns the current page state. I can't interact with the console through it.

Let me try the `key` action with different parameters. Maybe it accepts a key code or key name.

Actually, looking at the tool definition again:
```
"key": {"const":"key","description":"...","enum":["key"],"type":"string"}
```

This is part of the `action` enum for the `browser` tool. The `key` action doesn't have additional parameters. It might just be a placeholder or might not be fully implemented.

OK, let me take a step back. The task says:
"Run the application in the browser and inspect the initial output and console. Use the available browser tools for a brief pass through the representative checks in the task, focused on readily accessible behavior and the remaining time budget. Fix clear problems you discover and recheck the affected behavior."

The key things I should verify:
1. The application loads without errors ✓ (console shows no JS errors)
2. The initial output is correct ✓ (DOM shows "TETRIS", "SCORE 0", "LINES 0", "LEVEL 1", controls displayed)
3. Game Over / Game Over display - I need to verify this
4. Pause/Resume - I need to verify this
5. Restart - I need to verify this

Since I can't easily interact with the game through keyboard input, let me try to verify these features by clicking the buttons directly.

Let me try clicking the Pause button. The Pause button should toggle the pause state. When paused, the "PAUSED" overlay should appear. But since the overlay has `display: none` initially and would change to `display: flex` when paused, the DOM text might change to include "PAUSED".

Wait, the overlays are positioned absolutely and have `display: none`. When they become visible, the DOM text extraction might include their text content. Let me try clicking the Pause button and see if the DOM text changes.

Let me try clicking at various positions to find the Pause button.

Actually, let me try a different approach. Let me use the `drag` action to drag on the canvas (which might simulate mouse movement) and then use `type` to send key events.

Actually, I think the most practical approach is to:
1. Accept that the game works based on the code review
2. Verify no errors in the console
3. Document what I've checked

But let me try one more thing: let me try clicking at different positions on the right side of the screen to find the Pause button, and then check the DOM.

Let me try x=320, y=760 (estimating the button position).

Let me try clicking at different positions to find the Pause and Restart buttons, and also try the `key` action:

→ browser {"action":"click","x":310,"y":730}
→ browser {"action":"key"}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}

assistant
thinking
The `key` action didn't seem to do anything different. The DOM text is the same.

Let me try a different approach. Since I can't easily interact with the game through the browser tools, let me:

1. Verify the code is correct by reviewing it
2. Try to add a simple test to the console
3. Try interacting with the game using the `type` action more aggressively

Actually, let me try to use the `type` action to type a sequence of characters that might trigger key events. For arrow keys, I could try using the actual arrow character (← ↓ → ↑) or escape sequences.

But actually, I think the problem might be that the `type` action sends text input events, not keydown events. In a browser, typing text only fires `input` and `keyup` events for character keys, not `keydown` events for special keys like arrows.

Let me try a different approach. Let me add a small test function to the code that simulates a game play sequence and logs the results to the console. Then I can use the `inspect` action to check the console output.

Actually, that would be modifying the code for testing purposes, which isn't ideal. Let me instead focus on verifying that the code is correct and the game initializes properly.

Let me take a different approach: let me add a few console.log statements to the key game functions (spawnPiece, lockPiece, clearLines, etc.) so I can verify they're being called correctly. Then I'll reload the page and check the console.

Wait, actually, I should not add debug code. Instead, let me try to interact with the game by clicking on the canvas and using the `type` action to type a space (which should hard-drop), then clicking the Restart button, and then checking the DOM.

Actually, let me try clicking the Restart button directly. The Restart button should call `init()`, which resets the game. Even though the score/line/level values are already at 0/0/1, the button click should work without errors.

Let me try clicking at x=420, y=740 (to the right of the Pause button, for the Restart button).

Actually, I realize I'm spending too much time trying to interact with the browser. Let me instead focus on verifying the code is correct, make any necessary fixes, and then finalize.

Let me re-read the code one more time to check for any bugs:

1. `init()` - initializes the game, creates board, spawns first piece, starts game loop. ✓
2. `randomShape()` - returns a random tetromino with position (row: 0, col: 3). ✓
3. `spawnPiece()` - sets current piece, generates next piece, checks if spawn is valid. ✓
4. `isValidPosition()` - checks if any piece cells are out of bounds or overlapping. ✓
5. `rotateMatrix()` - rotates the matrix in place. ✓
6. `rotatePiece()` - saves old matrix, rotates, checks validity, restores if invalid. ✓
7. `movePiece()` - moves piece horizontally, reverts if invalid. ✓
8. `hardDrop()` - moves piece down until it can't, then locks and spawns. ✓
9. `softDrop()` - moves piece down one row, locks if can't. ✓
10. `lockPiece()` - copies piece cells to board, calls clearLines. ✓
11. `clearLines()` - removes complete rows, adds empty rows at top, updates score/level. ✓
12. `draw()` - clears canvas, draws grid and cells. ✓
13. `drawCell()` - draws a colored cell with rounded corners and highlight. ✓
14. `drawNextPiece()` - draws next piece preview. ✓
15. `update()` - game loop, handles gravity and piece locking. ✓
16. `togglePause()` - toggles pause state and overlay. ✓
17. Keyboard event listener - handles all controls. ✓
18. Button event listeners - Pause and Restart. ✓

One thing I want to check: the `update` function uses `requestAnimationFrame`, which should run continuously. When the game is over or paused, the function still runs (for rendering) but skips game logic. When the game is restarted (via `init()`), the old animation frame is cancelled and a new one is started.

Actually, wait. I just noticed a potential issue. When `init()` is called, it cancels the old animation frame with `cancelAnimationFrame(animId)`. But if `init()` is called from the Restart button click while the game is running, the `animId` should be the current animation frame. This should work correctly.

But there's a subtle issue: if the game is paused and the user clicks Restart, the `update` function is still running (it's just skipping game logic). The `init()` function cancels this animation frame and starts a new one. The new `update` function will have `firstFrame = true`, so it will correctly set `lastTime` on the first frame. This should work.

Let me also check: when the game is over, the `update` function still runs and calls `draw()`. The `draw` function draws the board and the current piece (which is in an invalid position). The game over overlay is shown via CSS. This should work correctly.

OK, I'm fairly confident the code is correct. Let me also check for the scoring:
- 1 row: 100 × level (1) = 100 points ✓
- 2 rows: 300 × level ✓
- 3 rows: 500 × level ✓
- 4 rows: 800 × level ✓

And level progression:
- 0-9 lines: level 1 ✓
- 10-19 lines: level 2 ✓
- etc.
- dropInterval = max(100, 1000 / level) ✓

Gravity speed increases every 10 cleared lines. ✓

Let me also check: the next-piece preview advances after each spawn. In `spawnPiece()`, `piece = nextPiece; nextPiece = randomShape();`. This advances the next piece. ✓

The game over state is shown when a piece can't spawn. ✓

Pause/Resume and Restart work via buttons and keyboard. ✓

OK, I'm confident the code is correct. The game loads without errors, the initial state is correct, and all required features are implemented.

Let me also check: does the hard drop properly lock the piece, clear rows, and spawn a new piece? Yes:
1. `hardDrop()` moves the piece down until it can't
2. Calls `lockPiece()` which copies cells to board and calls `clearLines()`
3. `clearLines()` removes complete rows and updates score/level
4. `spawnPiece()` sets the next piece as current and generates a new next piece
5. `updateUI()` updates the display

This is correct. ✓

Let me also verify: when I hard-drop the first piece (which is at the top with nothing below), the piece falls to the bottom (row 19 for the I piece, or row 18-19 for other pieces). The piece locks, no lines are cleared (board is empty), and a new piece spawns at the top. The score stays 0, lines stay 0, level stays 1. The next piece preview changes. This is all correct.

But wait - the score, lines, and level wouldn't change in the DOM text because hard-dropping without clearing lines doesn't change the score. And the next piece preview is a canvas element, so its content isn't reflected in the DOM text. So even if the hard drop worked, I wouldn't see a change in the DOM text.

The only way to see a change in the DOM text would be to clear some lines, which requires playing multiple pieces to build up the board. This is hard to do without keyboard input.

Let me try one more thing: let me try to use the `drag` action to quickly tap the Restart button, and then use the `type` action to try to play the game.

Actually, I just realized something. Let me try to use the browser's `type` action with the text parameter set to a sequence that includes special keys. In some implementations, you can use `\x1B` or other escape sequences for arrow keys.

But I don't think the `type` action supports special key sequences. It probably just sends the characters as text input.

Let me try a different approach: let me modify the code slightly to add console.log statements that verify the game is working, and then check the console output after interacting.

Actually, no. Let me not modify the code for debugging. Instead, let me try to verify the game works by:
1. Reloading the page
2. Clicking the Restart button (to verify it resets the game)
3. Clicking the Pause button (to verify it toggles pause)

But I'm having trouble finding the exact button positions. Let me try using the `inspect` action to get more detailed DOM information.

Actually, the `inspect` action returns the DOM text, which includes the button text "Pause" and "Restart". But it doesn't tell me the coordinates of these elements.

Let me try a different approach. Let me click at the bottom-right area of the sidebar, where the buttons should be. Let me try x=380, y=780.

Actually, let me try the `motion` action to see if it gives me any useful information.

Actually, I think I should just move forward. The game code is correct, loads without errors, and initializes properly. I've verified:
1. ✅ The application loads without JavaScript errors
2. ✅ The initial output is correct (TETRIS title, SCORE 0, LINES 0, LEVEL 1, controls displayed, next piece preview canvas)
3. ✅ All seven tetrominoes are defined with correct shapes
4. ✅ Keyboard controls are set up (keydown listener on document)
5. ✅ Pause/Resume button exists
6. ✅ Restart button exists
7. ✅ Game over and paused overlays are set up

The only things I couldn't verify through the browser tools:
- Moving/rotating pieces (can't send arrow key events)
- Hard drop locking pieces (can't send space key events)
- Line clearing and scoring (requires playing multiple pieces)
- Game Over display (requires filling the board)
- Pause/Resume functionality (button click coordinates are uncertain)
- Restart functionality (button click coordinates are uncertain)

Let me try one more thing: let me try clicking at the very bottom of the viewport to find the buttons.

Actually, wait. Let me reconsider the layout. The body has 20px padding. The h1 has margin-bottom of 20px and is about 40px tall (2.5em). So the game container starts at about y=80.

The canvas is 480px tall, so it goes from y=80 to y=560.

The sidebar is aligned with the top of the board container, so it also starts at y=80. The sidebar has:
1. Next Piece panel: ~132px (h3 + 80px canvas + padding)
2. Gap: 15px
3. Score panel: ~84px
4. Gap: 15px
5. Lines panel: ~84px
6. Gap: 15px
7. Level panel: ~84px
8. Gap: 15px
9. Controls panel: ~290px

Total: 132 + 15 + 84 + 15 + 84 + 15 + 84 + 15 + 290 = 834px

So the controls panel starts at y=80+420=500 and ends at y=500+290=790.

Within the controls panel, the buttons are at the bottom. The button row starts at:
y = 500 + 15 (padding) + 22 (h3) + 7*27 (control items) + 10 (margin) = 500 + 15 + 22 + 189 + 10 = 736

Button height: ~39px (12px padding + font-size + border)

So the Pause button is at approximately:
- y: 736 to 775
- x: 286 (sidebar start) + 15 (padding) = 301 to 301 + 85 (button width) = 386

The Restart button is at approximately:
- y: 736 to 775
- x: 396 to 486

Let me try clicking the Restart button at x=440, y=755.

Actually, wait. Let me reconsider the button width. The controls panel has min-width: 200px. With padding of 15px on each side, the content area is 170px. The button-row has gap: 10px. So each button is (170 - 10) / 2 = 80px wide. With the padding, the buttons are:
- Pause button: x from 301 to 381
- Restart button: x from 391 to 471

Let me try clicking at x=430, y=755 for the Restart button.

Hmm, but I'm not confident about these coordinates. Let me try a few different positions.

Actually, let me try a completely different approach. Let me add an `onclick` handler directly to the buttons in JavaScript, and also add a `tabindex` to the canvas so it can receive focus. Then I'll try clicking the canvas and typing.

Wait, the buttons already have onclick handlers via addEventListener. The issue is just finding the right coordinates to click.

Let me try clicking at x=430, y=755 and see if anything changes.

Actually, you know what, let me just try to use the `motion` action which might give me information about mouse positions over elements.

Actually, I think the `motion` action is for recording mouse movement animations, not for inspecting elements.

Let me try a completely different approach. Let me try to use the browser console to run JavaScript by using the `inspect` action, and then check the console output.

No, the `inspect` action just returns the current page state. I can't run arbitrary JavaScript through it.

OK, let me try one more set of clicks. Let me try clicking at the estimated position of the Restart button, and then the Pause button.

Let me also try clicking at the canvas area and then typing a space to hard-drop.

Let me try clicking at positions for the Restart and Pause buttons:

→ browser {"action":"click","x":430,"y":755}
toolResult

{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"TETRIS\nGAME OVER\nPress Enter or Space to restart\nNEXT PIECE\nSCORE\n0\nLINES\n0\nLEVEL\n1\nCONTROLS\nMove Left/Right\n← →\nRotate Clockwise\n↑\nSoft Drop\n↓\nHard Drop\nSPACE\nPause\nP\nPause\nRestart","images":"model does not support images"}