git » sparkle-arcade » commit 2d7f287

init

author Alan Dipert
2025-11-16 17:06:20 UTC
committer Alan Dipert
2025-11-16 17:06:20 UTC

init

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 &amp; 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;
+  }
+}