| author | Alan Dipert
<alan@dipert.org> 2025-11-16 17:06:20 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2025-11-16 17:06:20 UTC |
| Makefile | +15 | -0 |
| blossom-blocks/game.js | +545 | -0 |
| blossom-blocks/index.html | +49 | -0 |
| blossom-blocks/styles.css | +283 | -0 |
| fairy-finder/game.js | +289 | -0 |
| fairy-finder/index.html | +54 | -0 |
| fairy-finder/styles.css | +236 | -0 |
| index.html | +53 | -0 |
| styles.css | +132 | -0 |
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c58c96e --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: deploy clean + +DIST_ITEMS := index.html styles.css blossom-blocks fairy-finder +DEPLOY_TARGET ?= arsien23i2@dreamhost:tailrecursion.com/arcade/ +RSYNC_FLAGS ?= -av --delete --chmod=F644,D755 --exclude='.DS_Store' + +clean: + find . -name '*.DS_Store' -delete + +deploy: + @if [ -z "$(DEPLOY_TARGET)" ]; then \ + echo "DEPLOY_TARGET not set"; \ + exit 1; \ + fi + rsync $(RSYNC_FLAGS) $(DIST_ITEMS) $(DEPLOY_TARGET) diff --git a/blossom-blocks/game.js b/blossom-blocks/game.js new file mode 100644 index 0000000..e86ff6b --- /dev/null +++ b/blossom-blocks/game.js @@ -0,0 +1,545 @@ +(() => { + const size = 8; + const boardElement = document.getElementById('board'); + const piecesElement = document.getElementById('pieces'); + const scoreElement = document.getElementById('score'); + const bestScoreElement = document.getElementById('best-score'); + const newGameBtn = document.getElementById('new-game'); + const startBtn = document.getElementById('start-game'); + const startOverlay = document.getElementById('start-overlay'); + const floatingScore = document.getElementById('floating-score'); + + const bestKey = 'blossomBlocksBest'; + + const SHAPES = [ + [[0, 0]], + [[0, 0], [1, 0]], + [[0, 0], [0, 1]], + [[0, 0], [1, 0], [2, 0]], + [[0, 0], [0, 1], [0, 2]], + [[0, 0], [1, 0], [0, 1], [1, 1]], + [[0, 0], [1, 0], [0, 1]], + [[0, 0], [0, 1], [1, 1]], + [[0, 0], [1, 0], [2, 0], [3, 0]], + [[0, 0], [0, 1], [0, 2], [0, 3]], + [[0, 0], [1, 0], [2, 0], [2, 1]], + [[0, 0], [0, 1], [1, 1], [2, 1]], + [[0, 0], [1, 0], [1, 1], [1, 2]], + [[0, 0], [0, 1], [0, 2], [1, 2]], + [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]], + [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]], + [[0, 0], [1, 0], [0, 1], [0, 2]], + [[0, 0], [0, 1], [1, 1], [2, 1]], + [[0, 0], [1, 0], [1, 1], [2, 1], [2, 2]], + ]; + + const EMOJIS = ['๐ธ', '๐ท', '๐บ', '๐', '๐ฎ', '๐']; + + let board = []; + let score = 0; + let bestScore = Number(localStorage.getItem(bestKey)) || 0; + let availablePieces = []; + let selectedPieceIndex = null; + let isActive = false; + + const cells = []; + let draggingPieceIndex = null; + let dragGhost = null; + let dragTarget = null; + let dragValid = false; + let dragMoved = false; + let suppressClick = false; + const highlightedCells = []; + let dragAnchor = { row: 0, col: 0 }; + let ghostSnapped = false; + let boardMetrics = null; + + function parsePixels(value) { + if (!value) return 0; + const number = parseFloat(value); + return Number.isNaN(number) ? 0 : number; + } + + function measureBoard() { + const firstCell = cells[0]?.[0]; + if (!firstCell) return null; + const boardRect = boardElement.getBoundingClientRect(); + const cellRect = firstCell.getBoundingClientRect(); + const styles = window.getComputedStyle(boardElement); + const gapX = parsePixels(styles.columnGap || styles.gap || '0'); + const gapY = parsePixels(styles.rowGap || styles.gap || '0'); + boardMetrics = { + boardRect, + cellWidth: cellRect.width, + cellHeight: cellRect.height, + offsetX: cellRect.left - boardRect.left, + offsetY: cellRect.top - boardRect.top, + gapX, + gapY, + }; + return boardMetrics; + } + + function pointerToCell(clientX, clientY) { + if (!boardMetrics) measureBoard(); + if (!boardMetrics) return null; + const { + boardRect, offsetX, offsetY, cellWidth, cellHeight, gapX, gapY, + } = boardMetrics; + const stepX = cellWidth + gapX; + const stepY = cellHeight + gapY; + const localX = clientX - boardRect.left - offsetX; + const localY = clientY - boardRect.top - offsetY; + if (localX < -gapX / 2 || localY < -gapY / 2) return null; + const col = Math.floor((localX + gapX / 2) / stepX); + const row = Math.floor((localY + gapY / 2) / stepY); + if (row < 0 || row >= size || col < 0 || col >= size) return null; + return { row, col }; + } + + function initBoard() { + boardElement.innerHTML = ''; + cells.length = 0; + boardElement.style.gridTemplateColumns = `repeat(${size}, var(--cell-size))`; + boardElement.style.gridTemplateRows = `repeat(${size}, var(--cell-size))`; + for (let r = 0; r < size; r += 1) { + const row = []; + for (let c = 0; c < size; c += 1) { + const cell = document.createElement('button'); + cell.type = 'button'; + cell.className = 'cell'; + cell.dataset.row = r; + cell.dataset.col = c; + cell.addEventListener('click', () => handleBoardClick(r, c)); + boardElement.appendChild(cell); + row.push(cell); + } + cells.push(row); + } + } + + function createEmptyBoard() { + return Array.from({ length: size }, () => Array(size).fill(null)); + } + + function randomItem(list) { + return list[Math.floor(Math.random() * list.length)]; + } + + const createId = () => { + if (window.crypto && typeof window.crypto.randomUUID === 'function') { + return window.crypto.randomUUID(); + } + return `piece-${Date.now()}-${Math.random().toString(16).slice(2)}`; + }; + + function createPiece() { + const shape = randomItem(SHAPES); + const emoji = randomItem(EMOJIS); + return { + id: createId(), + cells: shape.map(([r, c]) => ({ r, c })), + emoji, + }; + } + + function refillPieces() { + availablePieces = [createPiece(), createPiece(), createPiece()]; + selectedPieceIndex = null; + renderPieces(); + } + + function renderPieces() { + piecesElement.innerHTML = ''; + availablePieces.forEach((piece, index) => { + if (!piece) return; + const maxRow = Math.max(...piece.cells.map((cell) => cell.r)); + const maxCol = Math.max(...piece.cells.map((cell) => cell.c)); + const pieceEl = document.createElement('div'); + pieceEl.className = 'piece'; + pieceEl.style.gridTemplateRows = `repeat(${maxRow + 1}, 28px)`; + pieceEl.style.gridTemplateColumns = `repeat(${maxCol + 1}, 28px)`; + pieceEl.addEventListener('click', () => { + if (suppressClick) return; + selectPiece(index); + }); + pieceEl.addEventListener('pointerdown', (event) => startDrag(event, index)); + if (selectedPieceIndex === index) { + pieceEl.classList.add('selected'); + } + const grid = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(false)); + piece.cells.forEach(({ r, c }) => { + grid[r][c] = true; + }); + grid.forEach((row, rowIndex) => { + row.forEach((isFilled, colIndex) => { + if (isFilled) { + const mini = document.createElement('div'); + mini.className = 'piece-cell'; + mini.textContent = piece.emoji; + mini.style.gridRow = rowIndex + 1; + mini.style.gridColumn = colIndex + 1; + mini.dataset.row = rowIndex; + mini.dataset.col = colIndex; + pieceEl.appendChild(mini); + } + }); + }); + piecesElement.appendChild(pieceEl); + }); + } + + function clearHighlights() { + while (highlightedCells.length) { + const cell = highlightedCells.pop(); + cell.classList.remove('cell--highlight'); + } + } + + function highlightPlacement(piece, baseRow, baseCol) { + clearHighlights(); + piece.cells.forEach(({ r, c }) => { + const row = baseRow + r; + const col = baseCol + c; + if (row >= 0 && row < size && col >= 0 && col < size) { + const cell = cells[row][col]; + if (cell) { + cell.classList.add('cell--highlight'); + highlightedCells.push(cell); + } + } + }); + } + + function startDrag(event, index) { + if (!isActive) return; + if (event.button !== undefined && event.button !== 0) return; + const piece = availablePieces[index]; + if (!piece) return; + measureBoard(); + const targetCell = event.target.closest('.piece-cell'); + if (targetCell) { + const cellRow = Number(targetCell.dataset.row); + const cellCol = Number(targetCell.dataset.col); + dragAnchor = { + row: Number.isNaN(cellRow) ? 0 : cellRow, + col: Number.isNaN(cellCol) ? 0 : cellCol, + }; + } else { + dragAnchor = { row: 0, col: 0 }; + } + draggingPieceIndex = index; + dragTarget = null; + dragValid = false; + dragMoved = false; + ghostSnapped = false; + selectedPieceIndex = index; + renderPieces(); + createDragGhost(piece); + followPointer(event); + document.addEventListener('pointermove', handleDragMove); + document.addEventListener('pointerup', handleDragEnd); + event.preventDefault(); + } + + function createDragGhost(piece) { + if (dragGhost) { + dragGhost.remove(); + } + const ghost = document.createElement('div'); + ghost.className = 'drag-ghost'; + const metrics = boardMetrics || measureBoard(); + const cellWidth = metrics ? metrics.cellWidth : 32; + const cellHeight = metrics ? metrics.cellHeight : 32; + if (metrics) { + ghost.style.columnGap = `${metrics.gapX}px`; + ghost.style.rowGap = `${metrics.gapY}px`; + } else { + ghost.style.gap = '6px'; + } + const maxRow = Math.max(...piece.cells.map((cell) => cell.r)); + const maxCol = Math.max(...piece.cells.map((cell) => cell.c)); + ghost.style.gridTemplateRows = `repeat(${maxRow + 1}, ${cellHeight}px)`; + ghost.style.gridTemplateColumns = `repeat(${maxCol + 1}, ${cellWidth}px)`; + const grid = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(false)); + piece.cells.forEach(({ r, c }) => { + grid[r][c] = true; + }); + grid.forEach((row, rowIndex) => { + row.forEach((isFilled, colIndex) => { + if (isFilled) { + const mini = document.createElement('div'); + mini.className = 'piece-cell'; + mini.textContent = piece.emoji; + mini.style.gridRow = rowIndex + 1; + mini.style.gridColumn = colIndex + 1; + mini.style.fontSize = `${Math.max(18, cellWidth * 0.6)}px`; + ghost.appendChild(mini); + } + }); + }); + document.body.appendChild(ghost); + dragGhost = ghost; + } + + function followPointer(event) { + if (!dragGhost) return; + ghostSnapped = false; + dragGhost.classList.remove('drag-ghost--snapped'); + dragGhost.style.left = `${event.clientX}px`; + dragGhost.style.top = `${event.clientY}px`; + } + + function snapGhostToCell(baseRow, baseCol) { + if (!dragGhost) return; + if (!boardMetrics) measureBoard(); + if (!boardMetrics) return; + const cell = cells[baseRow]?.[baseCol]; + if (!cell) return; + const rect = cell.getBoundingClientRect(); + ghostSnapped = true; + dragGhost.classList.add('drag-ghost--snapped'); + dragGhost.style.left = `${rect.left}px`; + dragGhost.style.top = `${rect.top}px`; + } + + function handleDragMove(event) { + if (!dragGhost || draggingPieceIndex === null) return; + dragMoved = true; + measureBoard(); + const piece = availablePieces[draggingPieceIndex]; + if (!piece) return; + const cell = pointerToCell(event.clientX, event.clientY); + if (cell) { + const baseRow = cell.row - dragAnchor.row; + const baseCol = cell.col - dragAnchor.col; + if (canPlacePiece(piece, baseRow, baseCol)) { + dragValid = true; + dragTarget = { row: baseRow, col: baseCol }; + highlightPlacement(piece, baseRow, baseCol); + snapGhostToCell(baseRow, baseCol); + return; + } + } + dragValid = false; + dragTarget = null; + clearHighlights(); + followPointer(event); + } + + function handleDragEnd() { + document.removeEventListener('pointermove', handleDragMove); + document.removeEventListener('pointerup', handleDragEnd); + const moved = dragMoved; + if (dragGhost) { + dragGhost.remove(); + dragGhost = null; + } + if (dragValid && dragTarget && draggingPieceIndex !== null) { + selectedPieceIndex = draggingPieceIndex; + handleBoardClick(dragTarget.row, dragTarget.col); + } + clearHighlights(); + draggingPieceIndex = null; + dragTarget = null; + dragValid = false; + dragMoved = false; + ghostSnapped = false; + if (moved) { + suppressClick = true; + requestAnimationFrame(() => { + suppressClick = false; + }); + } + dragAnchor = { row: 0, col: 0 }; + } + + function selectPiece(index) { + if (!isActive) return; + if (selectedPieceIndex === index) { + selectedPieceIndex = null; + } else { + selectedPieceIndex = index; + } + renderPieces(); + } + + function handleBoardClick(row, col) { + if (!isActive) return; + clearHighlights(); + if (selectedPieceIndex === null) return; + const piece = availablePieces[selectedPieceIndex]; + if (!piece) return; + if (!canPlacePiece(piece, row, col)) return; + placePiece(piece, row, col); + availablePieces[selectedPieceIndex] = null; + selectedPieceIndex = null; + if (availablePieces.every((p) => !p)) { + refillPieces(); + } else { + renderPieces(); + } + if (!hasAvailableMoves()) { + endGame(false); + } + } + + function canPlacePiece(piece, baseRow, baseCol) { + return piece.cells.every(({ r, c }) => { + const targetRow = baseRow + r; + const targetCol = baseCol + c; + return ( + targetRow >= 0 && + targetRow < size && + targetCol >= 0 && + targetCol < size && + !board[targetRow][targetCol] + ); + }); + } + + function placePiece(piece, baseRow, baseCol) { + let placedCells = 0; + piece.cells.forEach(({ r, c }) => { + const targetRow = baseRow + r; + const targetCol = baseCol + c; + board[targetRow][targetCol] = { emoji: piece.emoji }; + placedCells += 1; + }); + updateBoardUI(); + const clearedLines = clearLines(); + const gained = placedCells * 5 + clearedLines * 15; + updateScore(score + gained, clearedLines); + } + + function updateBoardUI() { + for (let r = 0; r < size; r += 1) { + for (let c = 0; c < size; c += 1) { + const cellData = board[r][c]; + const cellEl = cells[r][c]; + cellEl.classList.toggle('cell--filled', Boolean(cellData)); + if (cellData) { + cellEl.dataset.emoji = cellData.emoji; + } else { + cellEl.removeAttribute('data-emoji'); + } + } + } + } + + function clearLines() { + const rowsToClear = []; + const colsToClear = []; + for (let r = 0; r < size; r += 1) { + if (board[r].every(Boolean)) { + rowsToClear.push(r); + } + } + for (let c = 0; c < size; c += 1) { + let full = true; + for (let r = 0; r < size; r += 1) { + if (!board[r][c]) { + full = false; + break; + } + } + if (full) colsToClear.push(c); + } + + rowsToClear.forEach((row) => { + for (let c = 0; c < size; c += 1) { + addClearingEffect(row, c); + board[row][c] = null; + } + }); + colsToClear.forEach((col) => { + for (let r = 0; r < size; r += 1) { + addClearingEffect(r, col); + board[r][col] = null; + } + }); + if (rowsToClear.length || colsToClear.length) { + setTimeout(updateBoardUI, 200); + showFloatingBonus(rowsToClear.length + colsToClear.length); + } + return rowsToClear.length + colsToClear.length; + } + + function addClearingEffect(row, col) { + const cell = cells[row][col]; + cell.classList.add('cell--clearing'); + setTimeout(() => cell.classList.remove('cell--clearing'), 400); + } + + function showFloatingBonus(lines) { + const bonus = `+${lines * 15} โจ`; + floatingScore.textContent = bonus; + floatingScore.style.opacity = '1'; + const rect = boardElement.getBoundingClientRect(); + floatingScore.style.left = `${rect.left + rect.width / 2 + window.scrollX}px`; + floatingScore.style.top = `${rect.top + window.scrollY + 30}px`; + floatingScore.animate( + [ + { transform: 'translate(-50%, 0)', opacity: 1 }, + { transform: 'translate(-50%, -30px)', opacity: 0 } + ], + { duration: 900 } + ); + setTimeout(() => { + floatingScore.style.opacity = '0'; + }, 900); + } + + function updateScore(newScore) { + score = newScore; + scoreElement.textContent = score; + if (score > bestScore) { + bestScore = score; + bestScoreElement.textContent = bestScore; + localStorage.setItem(bestKey, String(bestScore)); + } + } + + function hasAvailableMoves() { + return availablePieces.some((piece) => { + if (!piece) return false; + for (let r = 0; r < size; r += 1) { + for (let c = 0; c < size; c += 1) { + if (canPlacePiece(piece, r, c)) { + return true; + } + } + } + return false; + }); + } + + function startGame() { + isActive = true; + score = 0; + board = createEmptyBoard(); + updateScore(0); + updateBoardUI(); + refillPieces(); + startOverlay.classList.add('hidden'); + } + + function endGame(fromNewGame) { + isActive = false; + const message = fromNewGame ? 'Ready for a fresh bouquet?' : `Garden full! Final score: ${score}`; + startOverlay.querySelector('p').textContent = message; + startOverlay.querySelector('h2').textContent = 'Blossom Blocks'; + startOverlay.classList.remove('hidden'); + } + + newGameBtn.addEventListener('click', () => { + endGame(true); + }); + + startBtn.addEventListener('click', startGame); + + bestScoreElement.textContent = bestScore; + initBoard(); + board = createEmptyBoard(); + updateBoardUI(); +})(); diff --git a/blossom-blocks/index.html b/blossom-blocks/index.html new file mode 100644 index 0000000..92f7376 --- /dev/null +++ b/blossom-blocks/index.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Blossom Blocks</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600&family=Nunito:wght@400;600&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="styles.css" /> + <script src="game.js" defer></script> +</head> +<body> + <div class="background-charms" aria-hidden="true">๐ธ ๐ท ๐ ๐บ ๐ ๐</div> + <main class="layout"> + <header class="top-panel"> + <div> + <h1>Blossom Blocks</h1> + <p class="tagline">Arrange the garden blocks and make rows of flowers! ๐ธ</p> + </div> + <div class="score-panel"> + <div class="score">Score: <span id="score">0</span> ๐ธ</div> + <div class="best">Best score: <span id="best-score">0</span> ๐</div> + <button class="btn" id="new-game">New Game</button> + </div> + </header> + + <section class="play-area"> + <div class="board" id="board" aria-label="Blossom Blocks board" role="grid"></div> + <div class="pieces" id="pieces" aria-label="Available pieces"></div> + </section> + + <aside class="instructions"> + <h2>How to play</h2> + <p>Drag pretty blocks onto the garden. Fill a row or column to clear it. When you canโt place any more blocks, the game is over. Try to beat your best score!</p> + </aside> + </main> + + <div class="overlay" id="start-overlay"> + <div class="overlay-card"> + <h2>Blossom Blocks</h2> + <p>Help Viviana tidy the flower beds. Fill rows and columns with bright blocks!</p> + <button class="btn btn-lg" id="start-game">Play</button> + </div> + </div> + + <div class="floating-score" id="floating-score" aria-hidden="true"></div> +</body> +</html> diff --git a/blossom-blocks/styles.css b/blossom-blocks/styles.css new file mode 100644 index 0000000..127f65b --- /dev/null +++ b/blossom-blocks/styles.css @@ -0,0 +1,283 @@ +:root { + --bg-start: #ffe9f2; + --bg-end: #e8f0ff; + --accent-pink: #f79ad3; + --accent-purple: #cda9ff; + --accent-mint: #a6f0d6; + --accent-blue: #a9c6ff; + --board-bg: rgba(255, 255, 255, 0.7); + --cell-size: min(48px, 7vw); + font-size: 16px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Nunito', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: radial-gradient(circle at top, var(--bg-start), var(--bg-end)); + min-height: 100vh; + color: #4a2e4f; + overflow-x: hidden; +} + +.background-charms { + position: fixed; + inset: 0; + font-size: 8rem; + opacity: 0.07; + pointer-events: none; + text-align: center; + letter-spacing: 2rem; + padding-top: 3rem; +} + +.layout { + max-width: 1100px; + margin: 0 auto; + padding: 2rem clamp(1rem, 4vw, 3rem) 4rem; + position: relative; + z-index: 1; +} + +.top-panel { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +h1, h2 { + font-family: 'Baloo 2', 'Nunito', sans-serif; + margin: 0; + color: #ff5fa2; +} + +.tagline { + margin: 0.2rem 0 0; + color: #7b588f; +} + +.score-panel { + display: flex; + flex-direction: column; + gap: 0.4rem; + background: var(--board-bg); + padding: 0.8rem 1rem; + border-radius: 1rem; + box-shadow: 0 10px 30px rgba(255, 138, 197, 0.25); +} + +.score-panel div { + font-weight: 600; +} + +.btn { + padding: 0.5rem 1.5rem; + border: none; + border-radius: 999px; + background: linear-gradient(120deg, var(--accent-pink), var(--accent-purple)); + color: white; + cursor: pointer; + font-weight: 600; + font-size: 1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 10px 20px rgba(247, 154, 211, 0.4); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 12px 24px rgba(205, 169, 255, 0.4); +} + +.btn-lg { + font-size: 1.2rem; + padding: 0.9rem 2.5rem; +} + +.play-area { + margin-top: 2rem; + display: grid; + grid-template-columns: auto minmax(220px, 1fr); + gap: 1.5rem; + align-items: start; +} + +.board { + width: min(520px, 90vw); + aspect-ratio: 1; + background: var(--board-bg); + border-radius: 24px; + padding: 1rem; + display: grid; + grid-template-columns: repeat(10, var(--cell-size)); + grid-template-rows: repeat(10, var(--cell-size)); + gap: 0.4rem; + box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.6), 0 20px 40px rgba(210, 170, 235, 0.4); +} + +.cell { + border-radius: 12px; + background: rgba(255, 255, 255, 0.5); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; + color: #ff64a5; + transition: transform 0.2s ease, box-shadow 0.3s ease, background 0.3s ease; + user-select: none; +} + +.cell--filled { + background: linear-gradient(145deg, rgba(255, 200, 230, 0.95), rgba(205, 215, 255, 0.95)); + box-shadow: 0 6px 12px rgba(255, 122, 199, 0.4); +} + +.cell--filled::after { + content: attr(data-emoji); +} + +.cell--highlight { + box-shadow: 0 0 12px rgba(255, 118, 197, 0.8); +} + +.cell--clearing { + animation: sparkle 0.6s ease; +} + +@keyframes sparkle { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.4; } + 100% { transform: scale(0.8); opacity: 0; } +} + +.pieces { + background: var(--board-bg); + border-radius: 24px; + padding: 1rem; + box-shadow: 0 15px 30px rgba(169, 198, 255, 0.4); + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.piece { + display: inline-grid; + grid-auto-rows: 28px; + grid-auto-columns: 28px; + gap: 0.2rem; + padding: 0.4rem; + border-radius: 16px; + background: rgba(255, 255, 255, 0.8); + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 2px solid transparent; + touch-action: none; +} + +.piece-cell { + width: 28px; + height: 28px; + border-radius: 8px; + background: linear-gradient(135deg, var(--accent-pink), var(--accent-blue)); + box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; +} + +.piece:hover { + transform: translateY(-3px); + box-shadow: 0 12px 20px rgba(255, 110, 189, 0.3); +} + +.piece.selected { + border-color: #ff9fd6; + box-shadow: 0 0 15px rgba(255, 159, 214, 0.6); + transform: scale(1.03); +} + +.instructions { + margin-top: 2rem; + background: rgba(255, 255, 255, 0.8); + padding: 1.5rem; + border-radius: 1.5rem; + box-shadow: 0 10px 30px rgba(169, 156, 255, 0.3); +} + +.instructions p { + margin: 0.2rem 0 0; +} + +.overlay { + position: fixed; + inset: 0; + background: rgba(246, 220, 255, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + transition: opacity 0.3s ease; +} + +.overlay.hidden { + opacity: 0; + pointer-events: none; +} + +.overlay-card { + background: white; + padding: 2rem 3rem; + border-radius: 2rem; + text-align: center; + box-shadow: 0 20px 40px rgba(199, 119, 255, 0.3); +} + +.floating-score { + position: absolute; + pointer-events: none; + color: #ff5faf; + font-weight: 700; + opacity: 0; + transform: translate(-50%, -50%); + z-index: 5; +} + +.drag-ghost { + position: fixed; + display: inline-grid; + gap: 0.25rem; + padding: 0.4rem; + border-radius: 1rem; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 15px 30px rgba(255, 126, 203, 0.4); + pointer-events: none; + z-index: 50; + transform: translate(-50%, -50%); +} + +.drag-ghost .piece-cell { + width: 100%; + height: 100%; + font-size: 1.1rem; +} + +.drag-ghost--snapped { + transform: none; +} + +@media (max-width: 900px) { + .play-area { + grid-template-columns: 1fr; + } + + .pieces { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } +} diff --git a/fairy-finder/game.js b/fairy-finder/game.js new file mode 100644 index 0000000..b6ce557 --- /dev/null +++ b/fairy-finder/game.js @@ -0,0 +1,289 @@ +(() => { + const boardElement = document.getElementById('board'); + const remainingElement = document.getElementById('remaining'); + const flagsElement = document.getElementById('flags'); + const timerElement = document.getElementById('timer'); + const difficultySelect = document.getElementById('difficulty'); + const restartButton = document.getElementById('restart'); + const overlay = document.getElementById('overlay'); + const overlayButton = document.getElementById('overlay-btn'); + const overlayMessage = overlay.querySelector('p'); + + const difficulties = { + easy: { size: 8, fairies: 10 }, + medium: { size: 10, fairies: 18 }, + hard: { size: 12, fairies: 28 }, + }; + + const game = { + boardSize: difficulties.easy.size, + fairyTotal: difficulties.easy.fairies, + board: [], + cells: [], + flagsUsed: 0, + revealedSafe: 0, + isActive: false, + timer: 0, + timerInterval: null, + }; + + function stopTimer() { + if (game.timerInterval) { + clearInterval(game.timerInterval); + game.timerInterval = null; + } + } + + function startTimer() { + if (game.timerInterval) return; + game.timerInterval = setInterval(() => { + game.timer += 1; + updateTimerDisplay(); + }, 1000); + } + + function updateTimerDisplay() { + const minutes = String(Math.floor(game.timer / 60)).padStart(2, '0'); + const seconds = String(game.timer % 60).padStart(2, '0'); + timerElement.textContent = `${minutes}:${seconds}`; + } + + function buildBoardCells() { + boardElement.innerHTML = ''; + boardElement.style.gridTemplateColumns = `repeat(${game.boardSize}, var(--cell-size))`; + boardElement.style.gridTemplateRows = `repeat(${game.boardSize}, var(--cell-size))`; + game.cells = []; + for (let r = 0; r < game.boardSize; r += 1) { + const rowElements = []; + for (let c = 0; c < game.boardSize; c += 1) { + const cellButton = document.createElement('button'); + cellButton.type = 'button'; + cellButton.className = 'cell cell--hidden'; + cellButton.dataset.skipReveal = 'false'; + cellButton.addEventListener('click', () => handleReveal(r, c, cellButton)); + cellButton.addEventListener('contextmenu', (event) => { + event.preventDefault(); + toggleFlag(r, c); + }); + let longPressTimer = null; + const clearLongPress = () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + } + }; + cellButton.addEventListener('pointerdown', (event) => { + if (event.pointerType === 'touch') { + clearLongPress(); + longPressTimer = setTimeout(() => { + cellButton.dataset.skipReveal = 'true'; + toggleFlag(r, c); + clearLongPress(); + }, 550); + } + }); + ['pointerup', 'pointerleave', 'pointercancel'].forEach((evtName) => { + cellButton.addEventListener(evtName, clearLongPress); + }); + rowElements.push(cellButton); + boardElement.appendChild(cellButton); + } + game.cells.push(rowElements); + } + } + + function createDataBoard() { + game.board = Array.from({ length: game.boardSize }, () => + Array.from({ length: game.boardSize }, () => ({ + hasFairy: false, + revealed: false, + flagged: false, + neighborCount: 0, + })) + ); + } + + function placeFairies() { + const totalCells = game.boardSize * game.boardSize; + const spots = new Set(); + while (spots.size < game.fairyTotal) { + spots.add(Math.floor(Math.random() * totalCells)); + } + spots.forEach((index) => { + const row = Math.floor(index / game.boardSize); + const col = index % game.boardSize; + game.board[row][col].hasFairy = true; + }); + } + + function calculateNeighborCounts() { + const directions = [ + [-1, -1], [-1, 0], [-1, 1], + [0, -1], [0, 1], + [1, -1], [1, 0], [1, 1], + ]; + for (let r = 0; r < game.boardSize; r += 1) { + for (let c = 0; c < game.boardSize; c += 1) { + const cell = game.board[r][c]; + if (cell.hasFairy) continue; + let count = 0; + directions.forEach(([dr, dc]) => { + const nr = r + dr; + const nc = c + dc; + if (nr >= 0 && nr < game.boardSize && nc >= 0 && nc < game.boardSize) { + if (game.board[nr][nc].hasFairy) count += 1; + } + }); + cell.neighborCount = count; + } + } + } + + function handleReveal(row, col, element) { + if (!game.isActive) return; + if (element.dataset.skipReveal === 'true') { + element.dataset.skipReveal = 'false'; + return; + } + const cell = game.board[row][col]; + if (cell.flagged || cell.revealed) return; + if (!game.timerInterval) startTimer(); + cell.revealed = true; + if (cell.hasFairy) { + revealCellElement(row, col, cell); + revealAllFairies(); + finishGame(false); + return; + } + game.revealedSafe += 1; + revealCellElement(row, col, cell); + if (cell.neighborCount === 0) { + floodReveal(row, col); + } + if (game.revealedSafe === game.boardSize * game.boardSize - game.fairyTotal) { + revealAllFairies(true); + finishGame(true); + } + } + + function floodReveal(row, col) { + const queue = [[row, col]]; + const dirs = [ + [-1, -1], [-1, 0], [-1, 1], + [0, -1], [0, 1], + [1, -1], [1, 0], [1, 1], + ]; + while (queue.length) { + const [cr, cc] = queue.shift(); + dirs.forEach(([dr, dc]) => { + const nr = cr + dr; + const nc = cc + dc; + if (nr < 0 || nr >= game.boardSize || nc < 0 || nc >= game.boardSize) return; + const neighbor = game.board[nr][nc]; + if (neighbor.revealed || neighbor.flagged || neighbor.hasFairy) return; + neighbor.revealed = true; + game.revealedSafe += 1; + revealCellElement(nr, nc, neighbor); + if (neighbor.neighborCount === 0) { + queue.push([nr, nc]); + } + }); + } + } + + function revealCellElement(row, col, cell) { + const button = game.cells[row][col]; + button.classList.remove('cell--hidden', 'cell--flagged'); + button.classList.add('cell--revealed'); + button.textContent = ''; + button.dataset.skipReveal = 'false'; + for (let i = 1; i <= 8; i += 1) { + button.classList.remove(`cell--num-${i}`); + } + if (cell.hasFairy) { + button.classList.add('cell--fairy'); + button.textContent = '๐ง'; + return; + } + if (cell.neighborCount > 0) { + button.textContent = cell.neighborCount; + button.classList.add(`cell--num-${cell.neighborCount}`); + } + } + + function toggleFlag(row, col) { + if (!game.isActive) return; + const cell = game.board[row][col]; + if (cell.revealed) return; + cell.flagged = !cell.flagged; + game.flagsUsed += cell.flagged ? 1 : -1; + const button = game.cells[row][col]; + button.textContent = ''; + if (cell.flagged) { + button.classList.add('cell--flagged'); + button.classList.remove('cell--hidden'); + } else { + button.classList.remove('cell--flagged'); + button.classList.add('cell--hidden'); + } + updateStats(); + } + + function revealAllFairies(isWin = false) { + for (let r = 0; r < game.boardSize; r += 1) { + for (let c = 0; c < game.boardSize; c += 1) { + const cell = game.board[r][c]; + if (cell.hasFairy) { + revealCellElement(r, c, cell); + } else if (!isWin && game.cells[r][c].classList.contains('cell--flagged')) { + game.cells[r][c].classList.remove('cell--flagged'); + game.cells[r][c].classList.add('cell--revealed'); + game.cells[r][c].textContent = ''; + } + } + } + } + + function finishGame(hasWon) { + game.isActive = false; + stopTimer(); + overlayMessage.textContent = hasWon + ? 'You found every fairy friend! Want to search again?' + : 'A shy fairy fluttered out! Try again for a perfect sweep?'; + overlayButton.textContent = hasWon ? 'Play again' : 'Try again'; + overlay.classList.remove('hidden'); + } + + function updateStats() { + const remaining = Math.max(game.fairyTotal - game.flagsUsed, 0); + remainingElement.textContent = remaining; + flagsElement.textContent = game.flagsUsed; + } + + function prepareGame() { + const config = difficulties[difficultySelect.value] || difficulties.easy; + game.boardSize = config.size; + game.fairyTotal = config.fairies; + game.flagsUsed = 0; + game.revealedSafe = 0; + game.isActive = true; + stopTimer(); + game.timer = 0; + updateTimerDisplay(); + buildBoardCells(); + createDataBoard(); + placeFairies(); + calculateNeighborCounts(); + updateStats(); + } + + function startNewRound() { + prepareGame(); + overlay.classList.add('hidden'); + } + + restartButton.addEventListener('click', startNewRound); + overlayButton.addEventListener('click', startNewRound); + + prepareGame(); +})(); diff --git a/fairy-finder/index.html b/fairy-finder/index.html new file mode 100644 index 0000000..6d5c482 --- /dev/null +++ b/fairy-finder/index.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Fairy Finder</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600&family=Nunito:wght@400;600&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="styles.css" /> + <script src="game.js" defer></script> +</head> +<body> + <div class="sparkle-background" aria-hidden="true">โจ ๐ โญ ๐ ๐ง โจ</div> + <main class="wrapper"> + <header class="hero"> + <div> + <h1>Fairy Finder</h1> + <p class="tagline">Viviana, can you find every magical fairy in the garden? ๐ง</p> + </div> + <div class="controls"> + <div class="stat">Fairies hiding: <span id="remaining">0</span> ๐ง</div> + <div class="stat">Flags used: <span id="flags">0</span> โจ</div> + <div class="stat">Time: <span id="timer">00:00</span> โฑ๏ธ</div> + <label class="difficulty"> + Level + <select id="difficulty"> + <option value="easy">Easy Garden</option> + <option value="medium">Twilight Trail</option> + <option value="hard">Mystic Maze</option> + </select> + </label> + <button class="btn" id="restart">New Game</button> + </div> + </header> + + <section class="main-area"> + <div class="board" id="board" aria-label="Fairy Finder board" role="grid"></div> + <aside class="instructions"> + <h2>How to play</h2> + <p>Fairies are hiding in the garden! Tap a square to peek. Numbers show how many fairies are next door. Mark fairy spots with a flag using right-click or a long press.</p> + </aside> + </section> + </main> + + <div class="overlay" id="overlay"> + <div class="overlay-card"> + <h2>Fairy Finder</h2> + <p>Search gently for shy fairies. Reveal every safe patch to win!</p> + <button class="btn btn-lg" id="overlay-btn">Play</button> + </div> + </div> +</body> +</html> diff --git a/fairy-finder/styles.css b/fairy-finder/styles.css new file mode 100644 index 0000000..5168a22 --- /dev/null +++ b/fairy-finder/styles.css @@ -0,0 +1,236 @@ +:root { + --bg-start: #f7e3ff; + --bg-end: #d6f6ff; + --accent: #f89ad1; + --accent-2: #a6c8ff; + --forest: #7bdcb5; + --text-dark: #3e2a58; + --cell-size: min(52px, 8vw); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Nunito', system-ui, sans-serif; + color: var(--text-dark); + background: radial-gradient(circle at 20% 20%, var(--bg-start), var(--bg-end)); + min-height: 100vh; + overflow-x: hidden; +} + +.sparkle-background { + position: fixed; + inset: 0; + font-size: 7rem; + opacity: 0.05; + text-align: center; + padding-top: 2rem; + pointer-events: none; + letter-spacing: 1rem; +} + +.wrapper { + max-width: 1100px; + margin: 0 auto; + padding: 2rem clamp(1rem, 5vw, 3.5rem) 4rem; + position: relative; + z-index: 1; +} + +.hero { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + justify-content: space-between; +} + +h1, h2 { + font-family: 'Baloo 2', 'Nunito', sans-serif; + margin: 0; + color: #ff6bba; +} + +.tagline { + margin: 0.2rem 0 0; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + align-items: center; + background: rgba(255, 255, 255, 0.75); + padding: 0.8rem 1rem; + border-radius: 1.2rem; + box-shadow: 0 18px 45px rgba(166, 200, 255, 0.35); +} + +.stat { + font-weight: 600; +} + +.difficulty { + display: flex; + flex-direction: column; + font-weight: 600; + color: var(--text-dark); +} + +select { + margin-top: 0.2rem; + border-radius: 999px; + border: 2px solid rgba(255, 139, 206, 0.4); + padding: 0.3rem 0.8rem; + background: white; + font-weight: 600; +} + +.btn { + padding: 0.5rem 1.4rem; + border-radius: 999px; + border: none; + background: linear-gradient(120deg, var(--accent), var(--accent-2)); + color: white; + font-weight: 600; + cursor: pointer; + box-shadow: 0 12px 25px rgba(255, 107, 186, 0.35); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 14px 28px rgba(166, 200, 255, 0.4); +} + +.btn-lg { + font-size: 1.1rem; + padding: 0.8rem 2.3rem; +} + +.main-area { + margin-top: 2rem; + display: grid; + grid-template-columns: minmax(280px, 1.1fr) minmax(220px, 0.9fr); + gap: 1.5rem; +} + +.board { + width: min(560px, 95vw); + background: rgba(255, 255, 255, 0.8); + border-radius: 24px; + padding: 1rem; + display: grid; + gap: 0.35rem; + box-shadow: inset 0 0 25px rgba(255, 255, 255, 0.7), 0 20px 40px rgba(179, 160, 255, 0.35); +} + +.cell { + width: var(--cell-size); + height: var(--cell-size); + border-radius: 14px; + border: none; + font-size: 1.2rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Baloo 2', 'Nunito', sans-serif; +} + +.cell--hidden { + background: linear-gradient(135deg, #d9f1ff, #ffe6fa); + box-shadow: 0 8px 15px rgba(122, 178, 255, 0.35); + color: transparent; +} + +.cell--hidden::after { + content: '๐ฑ'; + font-size: 1.1rem; + opacity: 0.5; +} + +.cell--revealed { + background: rgba(255, 255, 255, 0.95); + box-shadow: inset 0 0 12px rgba(255, 255, 255, 0.8); +} + +.cell--flagged::after { + content: 'โจ'; + font-size: 1.3rem; +} + +.cell--flagged { + background: linear-gradient(135deg, rgba(255, 194, 232, 0.95), rgba(209, 224, 255, 0.95)); + box-shadow: 0 8px 18px rgba(255, 125, 206, 0.4); +} + +.cell--fairy { + background: radial-gradient(circle, #fff6ff, #ffe0ff); + box-shadow: 0 0 18px rgba(255, 126, 203, 0.7); + animation: fairy-pop 0.6s ease; +} + +@keyframes fairy-pop { + 0% { transform: scale(0.5); opacity: 0; } + 100% { transform: scale(1); opacity: 1; } +} + +.cell--num-1 { color: #7ab6ff; } +.cell--num-2 { color: #64c5b8; } +.cell--num-3 { color: #ff8fb9; } +.cell--num-4 { color: #b892ff; } +.cell--num-5 { color: #ffa56b; } +.cell--num-6 { color: #94d0ff; } +.cell--num-7 { color: #c4a0ff; } +.cell--num-8 { color: #ff6b9c; } + +.instructions { + background: rgba(255, 255, 255, 0.88); + border-radius: 1.5rem; + padding: 1.2rem 1.5rem; + box-shadow: 0 18px 40px rgba(138, 166, 255, 0.3); +} + +.instructions p { + margin: 0.2rem 0 0; +} + +.overlay { + position: fixed; + inset: 0; + background: rgba(216, 197, 255, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 20; + transition: opacity 0.3s ease; +} + +.overlay.hidden { + opacity: 0; + pointer-events: none; +} + +.overlay-card { + background: white; + padding: 2.2rem 3rem; + border-radius: 2rem; + text-align: center; + box-shadow: 0 30px 60px rgba(162, 147, 255, 0.4); +} + +@media (max-width: 900px) { + .main-area { + grid-template-columns: 1fr; + } + + .board { + justify-self: center; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..f575097 --- /dev/null +++ b/index.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Viviana's Sparkle Arcade</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600&family=Nunito:wght@400;600&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="styles.css" /> +</head> +<body> + <div class="emojis" aria-hidden="true">๐ธ ๐ ๐ง ๐ ๐ โจ</div> + <main class="frame"> + <header class="hero"> + <p class="subhead">tailrecursion.com/arcade</p> + <h1>Viviana's Sparkle Arcade</h1> + <p class="intro">Two cozy browser games crafted for Vivianaโs magical playtime. Click a card to jump straight inโno downloads, no ads, just pure pastel fun.</p> + </header> + + <section class="game-grid"> + <article class="game-card" role="article"> + <div class="badge">Puzzle ๐ท</div> + <h2>Blossom Blocks</h2> + <p>Arrange glowing flower tiles on an 8ร8 garden. Clear rows and columns to earn sparkles and beat your best bouquet score.</p> + <ul> + <li>Flower emojis for every tile</li> + <li>Drag & drop placement</li> + <li>Local best-score tracker</li> + </ul> + <a class="btn" href="blossom-blocks/" aria-label="Play Blossom Blocks">Play Blossom Blocks</a> + </article> + + <article class="game-card" role="article"> + <div class="badge">Puzzle ๐ง</div> + <h2>Fairy Finder</h2> + <p>A gentle Minesweeper-style adventure. Reveal safe meadow squares, flag fairy hiding spots, and clear the whole glen.</p> + <ul> + <li>Easy / Medium / Hard gardens</li> + <li>Right-click or long-press flags</li> + <li>Timer plus fairy counter</li> + </ul> + <a class="btn" href="fairy-finder/" aria-label="Play Fairy Finder">Play Fairy Finder</a> + </article> + </section> + + <section class="instructions"> + <h3>How to share</h3> + <p>Send friends to <strong>https://tailrecursion.com/arcade/</strong>. Each mini-game runs instantly in the browser on laptops or tablets.</p> + </section> + </main> +</body> +</html> diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..9840547 --- /dev/null +++ b/styles.css @@ -0,0 +1,132 @@ +:root { + --bg-top: #f9e7ff; + --bg-bottom: #dff6ff; + --card-bg: rgba(255, 255, 255, 0.95); + --accent: #ff80c8; + --accent-2: #a8c4ff; + --text: #3e2d4f; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: 'Nunito', system-ui, sans-serif; + background: radial-gradient(circle at top, var(--bg-top), var(--bg-bottom)); + color: var(--text); +} + +.emojis { + position: fixed; + inset: 0; + font-size: max(5rem, 7vw); + opacity: 0.05; + pointer-events: none; + text-align: center; + padding-top: 2rem; + letter-spacing: 2rem; +} + +.frame { + max-width: 1100px; + margin: 0 auto; + padding: 3rem clamp(1rem, 4vw, 4rem) 5rem; + position: relative; + z-index: 1; +} + +.hero { + text-align: center; + margin-bottom: 2.5rem; +} + +h1, h2, h3 { + font-family: 'Baloo 2', 'Nunito', sans-serif; + margin: 0 0 0.5rem; + color: var(--accent); +} + +.subhead { + text-transform: uppercase; + letter-spacing: 0.2em; + font-size: 0.85rem; + color: #7c6797; +} + +.intro { + max-width: 720px; + margin: 0.5rem auto 0; + color: #5f4a74; +} + +.game-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.game-card { + background: var(--card-bg); + border-radius: 1.8rem; + padding: 1.8rem; + box-shadow: 0 16px 40px rgba(178, 145, 255, 0.35); + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.badge { + align-self: flex-start; + background: linear-gradient(120deg, var(--accent), var(--accent-2)); + color: white; + padding: 0.3rem 0.9rem; + border-radius: 999px; + font-weight: 600; + font-size: 0.9rem; +} + +.game-card ul { + margin: 0; + padding-left: 1.2rem; + color: #6d5a87; +} + +.btn { + margin-top: auto; + align-self: flex-start; + padding: 0.65rem 1.6rem; + border-radius: 999px; + background: linear-gradient(120deg, var(--accent), var(--accent-2)); + color: white; + text-decoration: none; + font-weight: 700; + box-shadow: 0 16px 30px rgba(255, 128, 200, 0.35); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 20px 36px rgba(168, 196, 255, 0.4); +} + +.instructions { + margin-top: 3rem; + padding: 1.5rem 2rem; + background: rgba(255, 255, 255, 0.85); + border-radius: 1.5rem; + box-shadow: 0 14px 35px rgba(137, 190, 255, 0.3); + text-align: center; +} + +@media (max-width: 600px) { + .frame { + padding-bottom: 3rem; + } + + .game-card { + padding: 1.4rem; + } +}