diff --git a/multiplayer_crosswords/crossword.py b/multiplayer_crosswords/crossword.py index 4d6fb70..90aea01 100644 --- a/multiplayer_crosswords/crossword.py +++ b/multiplayer_crosswords/crossword.py @@ -49,6 +49,10 @@ class Crossword: @property def current_grid(self) -> List[List[Optional[str]]]: return self._current_grid + + @property + def solution_word_positions(self) -> Optional[List[Tuple[int, int]]]: + return self._solution_word_positions def get_words_by_y_x_position(self, x, y) -> List[CrosswordWord]: @@ -123,13 +127,62 @@ class Crossword: logger.info("Crossword generated successfully for grid size %dx%d with block ratio %.2f", grid_width, grid_height, grid_block_ratio) + # Build letter position index for efficient lookup + letter_to_positions: Dict[str, List[Tuple[int, int]]] = {} + for r in range(len(final_step.grid)): + for c in range(len(final_step.grid[0])): + cell = final_step.grid[r][c] + if cell and cell != '#': + if cell not in letter_to_positions: + letter_to_positions[cell] = [] + letter_to_positions[cell].append((r, c)) + + # Now find a solution word: generate a random word from the dictionary (with 10-20 letters) + # and try to find the necessary letter positions in the grid. if we fail we repeat with another word (max 10 tries) + solution_word_positions: Optional[List[Tuple[int, int]]] = None + max_solution_word_attempts = 10 + for _ in range(max_solution_word_attempts): + random_length = random.randint(10, 20) + possible_words = dictionary.find_by_pattern('*' * random_length) + if not possible_words: + continue + + chosen_word = random.choice(possible_words) + letter_positions = [] + used_positions = set() + + for letter in chosen_word.word: + if letter not in letter_to_positions: + letter_positions = [] + break + + # Pick random position for this letter that's not already used + available = [p for p in letter_to_positions[letter] if p not in used_positions] + if not available: + letter_positions = [] + break + + chosen_position = random.choice(available) + letter_positions.append(chosen_position) + used_positions.add(chosen_position) + + if len(letter_positions) == random_length: + solution_word_positions = letter_positions + break + + if solution_word_positions is None: + logger.warning("Failed to find a solution word for the generated crossword after %d attempts", max_solution_word_attempts) + return None + cw = Crossword( dictionary=dictionary, grid=final_step.grid, + solution_word_positions=solution_word_positions ) logger.debug("Generated Crossword: \n\n%s", cw) + return cw @@ -139,7 +192,19 @@ class Crossword: grid: List[List[Optional[str]]], current_grid: Optional[List[List[Optional[str]]]] = None, words: Optional[List[CrosswordWord]] = None, + solution_word_positions: Optional[List[Tuple[int, int]]] = None, ): + """ + Initialize a Crossword object. + + Args: + dictionary (Dictionary): The dictionary containing words and hints. + grid (List[List[Optional[str]]]): The solved crossword grid. + current_grid (Optional[List[List[Optional[str]]]]): The current state of the crossword grid. + words (Optional[List[CrosswordWord]]): Pre-extracted list of CrosswordWord objects. + solution_word_positions (Optional[List[Tuple[int, int]]]): Positions of letters building the solution word. + """ + self._dictionary = dictionary self._solved_grid = grid self._words: List[CrosswordWord] = [] @@ -147,6 +212,8 @@ class Crossword: self._horizontal_words_by_y_x_position = {} self._vertical_words_by_y_x_position = {} + self._solution_word_positions = solution_word_positions + if current_grid is not None: self._current_grid = current_grid diff --git a/multiplayer_crosswords/server/server_messages.py b/multiplayer_crosswords/server/server_messages.py index 0ef4f6e..085df75 100644 --- a/multiplayer_crosswords/server/server_messages.py +++ b/multiplayer_crosswords/server/server_messages.py @@ -30,9 +30,10 @@ class SendFullSessionStateServerMessage(ServerMessageBase): grid: list[list[str]] # 2D array representing the current grid state clues_across: dict[str, str] # mapping from clue number to clue text for across clues clues_down: dict[str, str] # mapping from clue number to clue text for down clues - clue_positions_across: dict[str, tuple[int, int]] # mapping from clue number to its (col, row) position - clue_positions_down: dict[str, tuple[int, int]] # mapping from clue number to its (col, row) position - solved_positions: list[tuple[int, int]] # list of (col, row) positions that are solved + clue_positions_across: dict[str, tuple[int, int]] # mapping from clue number to its (row, col) position + clue_positions_down: dict[str, tuple[int, int]] # mapping from clue number to its (row, col) position + solved_positions: list[tuple[int, int]] # list of (row, col) positions that are solved + solution_word_positions: list[tuple[int, int]] # list of (row, col) positions that are part of solution word class LetterUpdateBroadcastServerMessage(ServerMessageBase): type: str = "letter_update" diff --git a/multiplayer_crosswords/server/websocket_crossword_server.py b/multiplayer_crosswords/server/websocket_crossword_server.py index 103da6d..804e299 100644 --- a/multiplayer_crosswords/server/websocket_crossword_server.py +++ b/multiplayer_crosswords/server/websocket_crossword_server.py @@ -224,6 +224,11 @@ class WebsocketCrosswordServer(object): solved_positions = list(solved_positions) + solution_word_positions = [] + positions = session.crossword.solution_word_positions + for pos in positions: + solution_word_positions.append((pos[1], pos[0])) # convert (row, col) to (col, row) + response = server_messages.SendFullSessionStateServerMessage( session_id=session.session_id, grid=grid_state, @@ -232,6 +237,7 @@ class WebsocketCrosswordServer(object): clue_positions_across=clue_positions_across, clue_positions_down=clue_positions_down, solved_positions=solved_positions, + solution_word_positions=solution_word_positions, ) # register the client to the session session.add_client(handler) diff --git a/multiplayer_crosswords/webui/clue_area.js b/multiplayer_crosswords/webui/clue_area.js index d61e6c7..203e9af 100644 --- a/multiplayer_crosswords/webui/clue_area.js +++ b/multiplayer_crosswords/webui/clue_area.js @@ -15,8 +15,11 @@ export class ClueArea extends LitElement { selectedCol: { type: Number }, selectedMode: { type: String }, // 'horizontal' or 'vertical' grid: { type: Array }, // 2D grid from server (needed to find walls) + gridData: { type: Object }, // { rows, cols, walls, solvedCells } _showAllCluesAcross: { state: true }, - _showAllCluesDown: { state: true } + _showAllCluesDown: { state: true }, + _solvedCluesAcross: { state: true }, // Set of solved clue numbers + _solvedCluesDown: { state: true } // Set of solved clue numbers }; } @@ -30,8 +33,11 @@ export class ClueArea extends LitElement { this.selectedCol = 0; this.selectedMode = 'horizontal'; this.grid = []; + this.gridData = { rows: 0, cols: 0, walls: new Set(), solvedCells: new Set() }; this._showAllCluesAcross = false; this._showAllCluesDown = false; + this._solvedCluesAcross = new Set(); + this._solvedCluesDown = new Set(); } /** @@ -178,7 +184,7 @@ export class ClueArea extends LitElement { } } - _toggleShowAllClues() { + _toggleShowAllCluesAcross() { this._showAllCluesAcross = !this._showAllCluesAcross; } @@ -186,6 +192,143 @@ export class ClueArea extends LitElement { this._showAllCluesDown = !this._showAllCluesDown; } + /** + * Find the starting row,col of an across clue by clue number + */ + _getAcrossClueStart(clueNum) { + const position = this.cluePositionsAcross[clueNum]; + if (!position) return null; + // Server sends (x, y) = (col, row) + const col = position[0]; + const row = position[1]; + return { row, col }; + } + + /** + * Find the starting row,col of a down clue by clue number + */ + _getDownClueStart(clueNum) { + const position = this.cluePositionsDown[clueNum]; + if (!position) return null; + // Server sends (x, y) = (col, row) + const col = position[0]; + const row = position[1]; + return { row, col }; + } + + /** + * Get all cells that belong to an across clue + */ + _getAcrossCluesCells(clueNum) { + const startPos = this._getAcrossClueStart(clueNum); + if (!startPos) return []; + + const { row, col } = startPos; + const cells = []; + + // Expand right until we hit a wall + for (let c = col; c < this.gridData.cols; c++) { + if (this.gridData.walls.has(`${row},${c}`)) { + break; + } + cells.push({ row, col: c }); + } + + return cells; + } + + /** + * Get all cells that belong to a down clue + */ + _getDownCluesCells(clueNum) { + const startPos = this._getDownClueStart(clueNum); + if (!startPos) return []; + + const { row, col } = startPos; + const cells = []; + + // Expand down until we hit a wall + for (let r = row; r < this.gridData.rows; r++) { + if (this.gridData.walls.has(`${r},${col}`)) { + break; + } + cells.push({ row: r, col }); + } + + return cells; + } + + /** + * Check if a clue is fully solved + */ + _isCluesSolved(clueNum, direction) { + const cells = direction === 'across' + ? this._getAcrossCluesCells(clueNum) + : this._getDownCluesCells(clueNum); + + return cells.length > 0 && cells.every(cell => + this.gridData.solvedCells.has(`${cell.row},${cell.col}`) + ); + } + + /** + * Update which clues are solved + */ + _updateSolvedClues() { + this._solvedCluesAcross = new Set(); + this._solvedCluesDown = new Set(); + + for (const clueNum of Object.keys(this.cluesAcross)) { + if (this._isCluesSolved(clueNum, 'across')) { + this._solvedCluesAcross.add(clueNum); + } + } + + for (const clueNum of Object.keys(this.cluesDown)) { + if (this._isCluesSolved(clueNum, 'down')) { + this._solvedCluesDown.add(clueNum); + } + } + } + + /** + * Handle clue item click - focus the cell and set orientation + */ + _onClueItemClick(clueNum, direction) { + let startPos; + let mode; + + if (direction === 'across') { + startPos = this._getAcrossClueStart(clueNum); + mode = 'horizontal'; + } else { + startPos = this._getDownClueStart(clueNum); + mode = 'vertical'; + } + + if (!startPos) return; + + // Update selected cell and mode in parent grid + this.selectedRow = startPos.row; + this.selectedCol = startPos.col; + this.selectedMode = mode; + + // Dispatch event to notify grid component + this.dispatchEvent(new CustomEvent('clue-selected', { + detail: { + row: startPos.row, + col: startPos.col, + mode: mode + }, + bubbles: true, + composed: true + })); + + // Close the all-clues view and return to default view + this._showAllCluesAcross = false; + this._showAllCluesDown = false; + } + render() { const currentClue = this._getCurrentClue(); @@ -195,15 +338,15 @@ export class ClueArea extends LitElement {

Across Clues

-
${Object.entries(this.cluesAcross).map(([num, text]) => html` -
+
${num}. ${text}
@@ -221,14 +364,14 @@ export class ClueArea extends LitElement {

Down Clues

${Object.entries(this.cluesDown).map(([num, text]) => html` -
+
${num}. ${text}
@@ -253,11 +396,13 @@ export class ClueArea extends LitElement { `}
-
diff --git a/multiplayer_crosswords/webui/grid.js b/multiplayer_crosswords/webui/grid.js index 0f70a46..9ef6649 100644 --- a/multiplayer_crosswords/webui/grid.js +++ b/multiplayer_crosswords/webui/grid.js @@ -18,6 +18,10 @@ export class CrosswordGrid extends LitElement { _inputMode: { state: true }, // 'horizontal' or 'vertical' _solvedCells: { state: true }, // tracks which cells are solved _clueNumbers: { state: true }, // map of "row,col" -> { across: number, down: number } + _solutionIndices: { state: true }, // map of "row,col" -> solution index + _solutionWordPositions: { state: true }, // list of [col, row] positions for solution word + _solutionWordValues: { state: true }, // map of index -> letter for solution word + _solutionWordSolved: { state: true }, // set of solution word indices that are solved }; // styles moved to webui/styles.css; render into light DOM so external CSS applies @@ -31,6 +35,10 @@ export class CrosswordGrid extends LitElement { this._inputMode = 'horizontal'; // default input mode this._solvedCells = new Set(); // set of "r,c" strings for solved cells this._clueNumbers = new Map(); // map of "row,col" -> { across: number, down: number } + this._solutionIndices = new Map(); // map of "row,col" -> solution index (1-indexed) + this._solutionWordPositions = []; // list of [col, row] positions + this._solutionWordValues = new Map(); // map of index -> letter + this._solutionWordSolved = new Set(); // set of solution word indices that are solved this.sessionId = null; // Session ID for sending updates to server } @@ -73,8 +81,31 @@ export class CrosswordGrid extends LitElement { this._ensureGrid(); // set CSS variables for cell-size and column count; layout done in external stylesheet return html` -
- ${this._grid.map((row, r) => row.map((cell, c) => this._renderCell(r, c, cell))).flat()} +
+
+ ${this._grid.map((row, r) => row.map((cell, c) => this._renderCell(r, c, cell))).flat()} +
+
+ ${this._solutionWordPositions.length > 0 ? html` +

Solution Word

+
+ ${this._solutionWordPositions.map((pos, i) => this._renderSolutionCell(i, pos))} +
+ ` : ''} + `; + } + + _renderSolutionCell(index, position) { + const letter = this._solutionWordValues.get(index) || ''; + const isSolved = this._solutionWordSolved.has(index); + const classes = ['cell']; + if (isSolved) classes.push('solved'); + + return html` +
this._onSolutionCellClick(index, position)}> +
+ ${index + 1} + ${letter}
`; } @@ -137,11 +168,18 @@ export class CrosswordGrid extends LitElement { } } + // Get solution index for this cell + const solutionIndex = this._solutionIndices.get(cellKey); + const cellContent = clueNumberDisplay ? html`${clueNumberDisplay}${value}` : html`${value}`; - return html`
this._onCellClick(r, c)} data-r="${r}" data-c="${c}">${cellContent}
`; + const cellHTML = solutionIndex !== undefined + ? html`${cellContent}
${solutionIndex}` + : cellContent; + + return html`
this._onCellClick(r, c)} data-r="${r}" data-c="${c}">${cellHTML}
`; } /** @@ -204,6 +242,14 @@ export class CrosswordGrid extends LitElement { return end - start + 1; } + /** + * Check if the entire solution word is solved + */ + _isSolutionWordComplete() { + if (this._solutionWordPositions.length === 0) return false; + return this._solutionWordPositions.every((_, i) => this._solutionWordSolved.has(i)); + } + /** * Check if cell (r, c) is part of the vertical line from the selected cell * (i.e., same column and not blocked by walls above/below this cell) @@ -228,25 +274,35 @@ export class CrosswordGrid extends LitElement { return r >= start && r <= end; } - _onCellClick(r, c) { + _onCellClick(r, c, preferredMode = null) { // if same cell is clicked again, toggle the input mode if (this._selected.r === r && this._selected.c === c) { - this._inputMode = this._inputMode === 'horizontal' ? 'vertical' : 'horizontal'; + // If a preferred mode is provided, use it (don't toggle) + if (preferredMode) { + this._inputMode = preferredMode; + } else { + this._inputMode = this._inputMode === 'horizontal' ? 'vertical' : 'horizontal'; + } } else { // select a new cell this._selected = { r, c }; - // auto-select mode based on line lengths - const horizontalLength = this._getHorizontalLineLength(r, c); - const verticalLength = this._getVerticalLineLength(r, c); - - // if one mode only has 1 cell but the other has multiple, use the one with multiple - if (horizontalLength === 1 && verticalLength > 1) { - this._inputMode = 'vertical'; - } else if (verticalLength === 1 && horizontalLength > 1) { - this._inputMode = 'horizontal'; + // Use preferred mode if provided, otherwise auto-select based on line lengths + if (preferredMode) { + this._inputMode = preferredMode; + } else { + // auto-select mode based on line lengths + const horizontalLength = this._getHorizontalLineLength(r, c); + const verticalLength = this._getVerticalLineLength(r, c); + + // if one mode only has 1 cell but the other has multiple, use the one with multiple + if (horizontalLength === 1 && verticalLength > 1) { + this._inputMode = 'vertical'; + } else if (verticalLength === 1 && horizontalLength > 1) { + this._inputMode = 'horizontal'; + } + // otherwise keep current mode (both >1 or both =1) } - // otherwise keep current mode (both >1 or both =1) } this.requestUpdate(); this.dispatchEvent(new CustomEvent('cell-selected', { detail: { row: r, col: c, mode: this._inputMode }, bubbles: true, composed: true })); @@ -254,6 +310,12 @@ export class CrosswordGrid extends LitElement { this.focus(); } + _onSolutionCellClick(index, position) { + // When clicking a solution word cell, select the corresponding grid cell + const [col, row] = position; + this._onCellClick(row, col); + } + _onKeydown(e) { // Only handle keys when the grid has focus // Map letters, arrows and backspace to our handlers @@ -483,7 +545,44 @@ export class CrosswordGrid extends LitElement { this._solvedCells.delete(cellKey); } + // Update solution word if this position is part of it + for (let i = 0; i < this._solutionWordPositions.length; i++) { + const [col_sw, row_sw] = this._solutionWordPositions[i]; + if (row === row_sw && col === col_sw) { + this._solutionWordValues.set(i, letter); + // Mark solution word cell as solved + if (is_solved) { + this._solutionWordSolved.add(i); + } else { + this._solutionWordSolved.delete(i); + } + break; + } + } + this.requestUpdate(); + + // Trigger animation if solution word just completed + if (this._isSolutionWordComplete()) { + this.updateComplete.then(() => { + const gridContainer = this.querySelector('.solution-word-grid'); + if (gridContainer) { + // Force reflow to trigger animation + gridContainer.offsetHeight; + gridContainer.classList.remove('complete'); + gridContainer.offsetHeight; + gridContainer.classList.add('complete'); + } + }); + } + + // Emit a letter-changed event so solution word can update + this.dispatchEvent(new CustomEvent('letter-changed', { + detail: { row, col, letter, is_solved }, + bubbles: true, + composed: true + })); + console.log(`Letter update from server: [${row}, ${col}] = "${letter}" (solved: ${is_solved})`); } } @@ -522,6 +621,49 @@ export class CrosswordGrid extends LitElement { this.requestUpdate(); } + + /** + * Populate solution word indices from server data + * @param {Array} solutionPositions - list of [col, row] positions in order + */ + populateSolutionIndices(solutionPositions = []) { + this._solutionIndices.clear(); + this._solutionWordPositions = solutionPositions; + this._solutionWordValues.clear(); + this._solutionWordSolved.clear(); + + for (let i = 0; i < solutionPositions.length; i++) { + const [col, row] = solutionPositions[i]; + const cellKey = `${row},${col}`; + this._solutionIndices.set(cellKey, i + 1); // 1-indexed + + // Initialize solution word value with current grid letter + const letter = this._grid[row][col] || ''; + this._solutionWordValues.set(i, letter); + + // Check if this position is already solved + if (this._solvedCells.has(cellKey)) { + this._solutionWordSolved.add(i); + } + } + + console.log('Solution word initialized. Solved:', this._solutionWordSolved.size, 'Total:', this._solutionWordPositions.length); + this.requestUpdate(); + + // Trigger animation on init if already complete + if (this._isSolutionWordComplete()) { + this.updateComplete.then(() => { + const gridContainer = this.querySelector('.solution-word-grid'); + if (gridContainer) { + // Force reflow to trigger animation + gridContainer.offsetHeight; + gridContainer.classList.remove('complete'); + gridContainer.offsetHeight; + gridContainer.classList.add('complete'); + } + }); + } + } } customElements.define('crossword-grid', CrosswordGrid); \ No newline at end of file diff --git a/multiplayer_crosswords/webui/index.html b/multiplayer_crosswords/webui/index.html index 30d4252..df004c2 100644 --- a/multiplayer_crosswords/webui/index.html +++ b/multiplayer_crosswords/webui/index.html @@ -125,7 +125,14 @@ gridContainer.innerHTML = `

Crossword

- +
+ + +
@@ -163,6 +170,7 @@ for (let r = 0; r < gridRows; r++) { for (let c = 0; c < gridCols; c++) { const cell = message.grid[r][c]; + // Skip walls and empty cells if (cell !== '#' && cell !== '') { gridElement._grid[r][c] = cell; @@ -181,6 +189,11 @@ // Populate clue numbers for display gridElement.populateClueNumbers(message.clue_positions_across, message.clue_positions_down); + // Populate solution word + if (message.solution_word_positions) { + gridElement.populateSolutionIndices(message.solution_word_positions); + } + gridElement.requestUpdate(); console.log(`Grid created: ${gridRows}x${gridCols} with ${wallPositions.length} walls`); @@ -188,6 +201,9 @@ if (message.solved_positions) { console.log(`Solved positions: ${message.solved_positions.length}`); } + if (message.solution_word_positions) { + console.log(`Solution word positions: ${message.solution_word_positions.length}`); + } }, 0); // Create and add clue area @@ -197,9 +213,36 @@ clueArea.cluePositionsAcross = message.clue_positions_across; clueArea.cluePositionsDown = message.clue_positions_down; clueArea.grid = message.grid; // Pass grid for dimension calculation + + // Setup gridData for solved clue tracking + const walls = new Set(); + for (let r = 0; r < gridRows; r++) { + for (let c = 0; c < gridCols; c++) { + if (message.grid[r][c] === '#') { + walls.add(`${r},${c}`); + } + } + } + const solvedCells = new Set(); + if (message.solved_positions) { + for (const [col, row] of message.solved_positions) { + solvedCells.add(`${row},${col}`); + } + } + clueArea.gridData = { + rows: gridRows, + cols: gridCols, + walls: walls, + solvedCells: solvedCells + }; + clueArea.selectedRow = 0; clueArea.selectedCol = 0; clueArea.selectedMode = 'horizontal'; + + // Update solved clues initially + clueArea._updateSolvedClues(); + document.body.insertBefore(clueArea, document.body.firstChild); // Listen for cell selection changes @@ -210,12 +253,157 @@ clueArea.requestUpdate(); }); + // Listen for clue clicks to navigate grid + clueArea.addEventListener('clue-selected', (e) => { + const { row, col, mode } = e.detail; + // Call _onCellClick with preferred mode from the clue + gridElement._onCellClick(row, col, mode); + gridElement.focus(); + }); + + // Listen for letter updates to update solved clues + gridElement.addEventListener('letter-changed', (e) => { + const { row, col, is_solved } = e.detail; + const cellKey = `${row},${col}`; + if (is_solved) { + clueArea.gridData.solvedCells.add(cellKey); + } else { + clueArea.gridData.solvedCells.delete(cellKey); + } + clueArea._updateSolvedClues(); + clueArea.requestUpdate(); + }); + // Close button handler closeBtn.addEventListener('click', closeGame); + const shareBtn = gridContainer.querySelector('.share-game-btn'); + shareBtn.addEventListener('click', shareGame); + notificationManager.success('Game loaded successfully'); }); + // Function to share game + function shareGame() { + console.log('Sharing game with session ID:', currentSessionId); + + // Build URL with session ID + const url = `${window.location.origin}${window.location.pathname}?session_id=${currentSessionId}`; + + // Try native share API first (mobile) + if (navigator.share) { + navigator.share({ + title: 'Join my Crossword!', + text: 'Play crossword with me!', + url: url + }).then(() => { + console.log('Share successful'); + }).catch(err => { + if (err.name !== 'AbortError') { + console.error('Error sharing:', err); + showShareDialog(url); + } + }); + } else { + // Fallback: show dialog with link + showShareDialog(url); + } + } + + // Function to show share dialog with copy option + function showShareDialog(url) { + console.log('Showing share dialog with URL:', url); + + // Create modal dialog + const dialog = document.createElement('div'); + dialog.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: #2a2520; + padding: 1.5rem; + border-radius: 0.5rem; + max-width: 90%; + width: 100%; + max-width: 400px; + box-shadow: 0 10px 40px rgba(0,0,0,0.5); + `; + + content.innerHTML = ` +

Share Game Link

+

Copy this link and send it to friends:

+ +
+ + +
+ `; + + dialog.appendChild(content); + document.body.appendChild(dialog); + + // Copy button + document.getElementById('copy-btn').addEventListener('click', () => { + const input = document.getElementById('share-url-input'); + input.select(); + document.execCommand('copy'); + notificationManager.success('Link copied!'); + dialog.remove(); + }); + + // Close button + document.getElementById('close-share-btn').addEventListener('click', () => { + dialog.remove(); + }); + + // Close on background click + dialog.addEventListener('click', (e) => { + if (e.target === dialog) { + dialog.remove(); + } + }); + } + // Function to close game and return to menu function closeGame() { console.log('Closing game'); diff --git a/multiplayer_crosswords/webui/keyboard.js b/multiplayer_crosswords/webui/keyboard.js index a8d69d0..81ae929 100644 --- a/multiplayer_crosswords/webui/keyboard.js +++ b/multiplayer_crosswords/webui/keyboard.js @@ -25,72 +25,97 @@ export class MobileKeyboard extends LitElement { createRenderRoot() { return this; } - render() { - // simple QWERTY-like rows - const rows = [ - 'qwertyuiop'.split(''), - 'asdfghjkl'.split(''), - 'zxcvbnm'.split(''), - ]; + render() { + // simple QWERTY-like rows + const rows = [ + 'qwertyuiop'.split(''), + 'asdfghjkl'.split(''), + 'zxcvbnm'.split(''), + ]; - // compute the maximum number of columns across rows (account for backspace in first row) - const counts = rows.map((r, idx) => r.length + (idx === 0 ? 1 : 0)); - const arrowCols = 3; // reserve 3 columns on the right for [left][down][right] - const baseMax = Math.max(...counts, 10); - const maxCols = baseMax; + // compute the maximum number of columns across rows (account for backspace in second row now) + const counts = rows.map((r, idx) => r.length + (idx === 1 ? 1 : 0)); + const arrowCols = 3; // reserve 3 columns on the right for [left][down][right] + const baseMax = Math.max(...counts, 10); + const maxCols = baseMax; - return html` -
- ${html`
${this.collapsed ? '▲' : '▼'}
`} -
-
- ${rows.map((r, idx) => { - // center the letter keys leaving the rightmost `arrowCols` for the arrow block + return html` +
+ ${html`
${this.collapsed ? '▲' : '▼'}
`} +
+
+ ${rows.map((r, idx) => { + // center the letter keys leaving the rightmost `arrowCols` for the arrow block - let rowClasses = 'row'; - if (idx === 1) rowClasses += ' stagger'; // A row - if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent - return html`
-
- ${r.map(l => html``) } - ${idx === 0 ? html`` : ''} -
-
- ${Array.from({ length: arrowCols }).map((_, i) => { - if (idx === 2 && i === 1) return html``; - return html`
`; - })} -
-
`; - })} + let rowClasses = 'row'; + if (idx === 1) rowClasses += ' stagger'; // A row + if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent + return html`
+
+ ${r.map(l => html``) } + ${idx === 1 ? html`` : ''} +
+
+ ${Array.from({ length: arrowCols }).map((_, i) => { + if (idx === 2 && i === 1) return html``; + return html`
`; + })} +
+
`; + })} - -
- - - - - - -
+ +
+ + + + + +
- `; - } - - _emitLetter(l) { +
+ `; + } _emitLetter(l) { + this._vibrate(); this._emit({ type: 'letter', value: l }); } _emitNavigate(dir) { + this._vibrate(); this._emit({ type: 'navigate', value: dir }); } + _emitBackspace() { + this._vibrate(); + this._emit({ type: 'backspace' }); + } + + _emitSpace() { + this._vibrate(); + this._emit({ type: 'letter', value: '' }); + } + _emit(detail) { window.dispatchEvent(new CustomEvent('key-press', { detail })); } + _vibrate() { + // Use Vibration API for haptic feedback + try { + console.log('Attempting vibration... navigator.vibrate:', typeof navigator.vibrate); + if (navigator.vibrate) { + navigator.vibrate(10); // 10ms short buzz + console.log('Vibration sent!'); + } else { + console.log('Vibration API not available on this device'); + } + } catch (e) { + console.warn('Vibration API error:', e); + } + } + connectedCallback() { super.connectedCallback(); window.addEventListener('resize', this._onResize); diff --git a/multiplayer_crosswords/webui/styles.css b/multiplayer_crosswords/webui/styles.css index 104dd7c..fff46e4 100644 --- a/multiplayer_crosswords/webui/styles.css +++ b/multiplayer_crosswords/webui/styles.css @@ -20,11 +20,17 @@ html, body { -webkit-text-size-adjust: 100%; } body { font-family: 'Segoe UI', 'Helvetica Neue', system-ui, Roboto, Arial; margin: 0; - background: #0a0805; - background-image: - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), + radial-gradient(ellipse 900px 700px at 25% 35%, rgba(0,0,0,.3) 0%, transparent 45%), + radial-gradient(ellipse 700px 600px at 75% 65%, rgba(255,255,255,.02) 0%, transparent 50%), + linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); color: var(--ink-dark); -webkit-font-smoothing: antialiased; font-size: 100%; @@ -61,22 +67,36 @@ main { } crossword-grid { display: block; margin: 0 auto; } mobile-keyboard { display: block; } -h2, h3 { margin: 0.5rem 0; color: #f5f1ed; text-align: center; text-shadow: 0 2px 4px rgba(0,0,0,0.7); font-weight: 600; } +h2, h3 { + margin: 0.5rem 0; + color: #f5f1ed; + text-align: center; + font-weight: 600; + letter-spacing: 0.01em; +} crossword-grid { display: block; margin: 0 auto; } -.grid { - display: grid; - gap: 0; - background: var(--ink-dark); - background-image: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.025) 1px, rgba(0,0,0,.025) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.025) 1px, rgba(0,0,0,.025) 2px); - padding: 0.8rem; - grid-template-columns: repeat(var(--cols), var(--cell-size)); - border: 2px solid var(--ink-dark); +.grid-container { + display: inline-block; + gap: 0; + overflow: visible; + padding: 20px; +} +.grid { + display: grid; + gap: 0; + background: transparent; + padding: 0; + grid-template-columns: repeat(var(--cols), var(--cell-size)); + border: none; +} + +.grid-container.complete .grid { + animation: grid-glow 2s ease-in-out infinite; + will-change: box-shadow; } .cell { @@ -86,14 +106,19 @@ crossword-grid { display: block; margin: 0 auto; } align-items: center; justify-content: center; background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), - radial-gradient(ellipse 600px 500px at 70% 60%, rgba(0,0,0,.02) 0%, transparent 50%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.03) 3px, rgba(0,0,0,.03) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.025) 4px, rgba(0,0,0,.025) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.05) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(0,0,0,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px), + radial-gradient(ellipse 900px 700px at 25% 35%, rgba(255,255,255,.35) 0%, transparent 45%), + radial-gradient(ellipse 700px 600px at 75% 65%, rgba(0,0,0,.03) 0%, transparent 50%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.025) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.025) 0%, transparent 70%), linear-gradient(135deg, #fefdfb 0%, #f9f7f3 100%); color: var(--ink-dark); font-weight: 700; @@ -119,11 +144,18 @@ crossword-grid { display: block; margin: 0 auto; } .cell.wall { background: - repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), - repeating-linear-gradient(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.05) 2px, rgba(0,0,0,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.05) 2px, rgba(0,0,0,.05) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.025) 3px, rgba(0,0,0,.025) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.02) 4px, rgba(0,0,0,.02) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.035) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(255,255,255,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(0,0,0,.025) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(0,0,0,.15) 0%, transparent 50%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.03) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.03) 0%, transparent 70%), linear-gradient(135deg, #3a3530 0%, #2a2520 100%); color: transparent; border-color: var(--wall-dark); @@ -134,19 +166,34 @@ crossword-grid { display: block; margin: 0 auto; } inset 1px 1px 2px rgba(255,255,255,0.05); } +.cell.wall .cell-letter { + font-size: 0; + text-shadow: none; +} + .cell.wall.selected { border-color: var(--accent-wall); box-shadow: inset 0 1px 2px rgba(255,255,255,0.1), inset 0 0 0 1px var(--accent-wall), 0 0 6px rgba(212,105,107,0.3); background: - repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), - repeating-linear-gradient(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), linear-gradient(135deg, #3a3530 0%, #2a2520 100%); } .cell.wall.mode-highlighted { background: - repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), - repeating-linear-gradient(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), linear-gradient(135deg, #3a3530 0%, #2a2520 100%); } @@ -168,17 +215,58 @@ crossword-grid { display: block; margin: 0 auto; } justify-content: center; width: 100%; height: 100%; + filter: contrast(1.05); + letter-spacing: 0.02em; } -.cell.mode-highlighted { +.solution-index { + position: absolute; + bottom: 1px; + right: 2px; + font-size: 0.5rem; + font-weight: 700; + line-height: 1; + color: #000000 !important; + pointer-events: none; +} + +.solution-circle { + position: absolute; + width: calc(var(--cell-size) * 0.75); + height: calc(var(--cell-size) * 0.75); + top: 50%; + left: 50%; + transform: translate(calc(-50% + var(--cell-size) * 0.0625), calc(-50% + var(--cell-size) * 0.0625)); + border: 1.5px solid rgba(26, 24, 21, 0.7); + border-radius: 50%; + background: transparent; + pointer-events: none; + /* Hide bottom-right quarter with clip-path for 270 degree arc */ + clip-path: polygon(0 0, 100% 0, 100% 50%, 50% 50%, 50% 100%, 0 100%); +} + +.solution-word-grid .solution-circle { + display: block; +} + +.grid .cell .solution-circle { + display: block; +} + +.cell.mode-highlighted { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(100,180,200,.2) 1px, rgba(100,180,200,.2) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(100,180,200,.2) 1px, rgba(100,180,200,.2) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(100,180,200,.1) 1px, rgba(100,180,200,.1) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(100,180,200,.1) 1px, rgba(100,180,200,.1) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(100,180,200,.05) 2px, rgba(100,180,200,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(100,180,200,.05) 2px, rgba(100,180,200,.05) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,180,200,.15) 3px, rgba(100,180,200,.15) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,180,200,.1) 4px, rgba(100,180,200,.1) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,180,200,.08) 3px, rgba(100,180,200,.08) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,180,200,.05) 4px, rgba(100,180,200,.05) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(100,180,200,.07) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,180,200,.09) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(100,180,200,.04) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,180,200,.02) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.04) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.3) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(100,180,200,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(100,180,200,.02) 0%, transparent 70%), linear-gradient(135deg, #f0fafb 0%, #e8f4f8 100%); box-shadow: inset 0 1px 2px rgba(255,255,255,0.9), @@ -190,15 +278,44 @@ crossword-grid { display: block; margin: 0 auto; } } /* Solved cells - green background and not editable */ +@keyframes cell-bounce { + 0%, 100% { + transform: scale(1); + } + 25% { + transform: scale(1.15); + } + 50% { + transform: scale(1.25); + } + 75% { + transform: scale(1.15); + } +} + +@keyframes grid-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(100, 200, 100, 0.3), inset 0 0 20px rgba(100, 200, 100, 0.1); + } + 50% { + box-shadow: 0 0 40px rgba(100, 200, 100, 0.6), inset 0 0 40px rgba(100, 200, 100, 0.2); + } +} + .cell.solved { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(100,200,100,.15) 1px, rgba(100,200,100,.15) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(100,200,100,.15) 1px, rgba(100,200,100,.15) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(100,200,100,.1) 1px, rgba(100,200,100,.1) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(100,200,100,.1) 1px, rgba(100,200,100,.1) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(100,200,100,.05) 2px, rgba(100,200,100,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(100,200,100,.05) 2px, rgba(100,200,100,.05) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,200,100,.1) 3px, rgba(100,200,100,.1) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,200,100,.08) 4px, rgba(100,200,100,.08) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,200,100,.06) 3px, rgba(100,200,100,.06) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,200,100,.04) 4px, rgba(100,200,100,.04) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(100,200,100,.06) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,200,100,.07) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(100,200,100,.03) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,200,100,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.2) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(100,200,100,.015) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(100,200,100,.015) 0%, transparent 70%), linear-gradient(135deg, #d4f4d4 0%, #c8ead4 100%); box-shadow: inset 0 1px 2px rgba(255,255,255,0.8), @@ -206,21 +323,58 @@ crossword-grid { display: block; margin: 0 auto; } 0 0.5px 1px rgba(0,0,0,0.05), inset -1px -1px 2px rgba(100,200,100,0.08), inset 1px 1px 2px rgba(255,255,255,0.3); - border-color: #90c890; - cursor: not-allowed; - opacity: 0.9; } +@keyframes cell-bounce { + 0%, 100% { + transform: scale(1); + } + 25% { + transform: scale(1.15); + } + 50% { + transform: scale(1.25); + } + 75% { + transform: scale(1.15); + } +} + +.solution-word-grid.complete .cell { + animation: cell-bounce 0.8s ease-in-out; +} + +.solution-word-grid.complete .cell:nth-child(1) { animation-delay: 0s; } +.solution-word-grid.complete .cell:nth-child(2) { animation-delay: 0.1s; } +.solution-word-grid.complete .cell:nth-child(3) { animation-delay: 0.2s; } +.solution-word-grid.complete .cell:nth-child(4) { animation-delay: 0.3s; } +.solution-word-grid.complete .cell:nth-child(5) { animation-delay: 0.4s; } +.solution-word-grid.complete .cell:nth-child(6) { animation-delay: 0.5s; } +.solution-word-grid.complete .cell:nth-child(7) { animation-delay: 0.6s; } +.solution-word-grid.complete .cell:nth-child(8) { animation-delay: 0.7s; } +.solution-word-grid.complete .cell:nth-child(9) { animation-delay: 0.8s; } +.solution-word-grid.complete .cell:nth-child(10) { animation-delay: 0.9s; } +.solution-word-grid.complete .cell:nth-child(11) { animation-delay: 1.0s; } +.solution-word-grid.complete .cell:nth-child(12) { animation-delay: 1.1s; } +.solution-word-grid.complete .cell:nth-child(13) { animation-delay: 1.2s; } +.solution-word-grid.complete .cell:nth-child(14) { animation-delay: 1.3s; } +.solution-word-grid.complete .cell:nth-child(15) { animation-delay: 1.4s; } + .cell.selected { outline: none; background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,200,0,.15) 3px, rgba(255,200,0,.15) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,200,0,.1) 4px, rgba(255,200,0,.1) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,200,0,.08) 3px, rgba(255,200,0,.08) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,200,0,.05) 4px, rgba(255,200,0,.05) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,200,0,.07) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,200,0,.09) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,200,0,.04) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(255,200,0,.02) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.04) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(255,200,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(255,200,0,.02) 0%, transparent 70%), linear-gradient(135deg, #fffef9 0%, #fff8e6 100%); border-color: var(--ink-dark); box-shadow: @@ -233,13 +387,18 @@ crossword-grid { display: block; margin: 0 auto; } .cell.selected.mode-highlighted { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(200,150,0,.2) 1px, rgba(200,150,0,.2) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(200,150,0,.2) 1px, rgba(200,150,0,.2) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(200,150,0,.1) 1px, rgba(200,150,0,.1) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(200,150,0,.1) 1px, rgba(200,150,0,.1) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(200,150,0,.05) 2px, rgba(200,150,0,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(200,150,0,.05) 2px, rgba(200,150,0,.05) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(200,150,0,.15) 3px, rgba(200,150,0,.15) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(200,150,0,.1) 4px, rgba(200,150,0,.1) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(200,150,0,.08) 3px, rgba(200,150,0,.08) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(200,150,0,.05) 4px, rgba(200,150,0,.05) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(200,150,0,.07) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(200,150,0,.09) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(200,150,0,.04) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(200,150,0,.02) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.04) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(200,150,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(200,150,0,.02) 0%, transparent 70%), linear-gradient(135deg, #fffaf0 0%, #fff5d6 100%); box-shadow: inset 0 1px 2px rgba(255,255,255,0.9), @@ -263,7 +422,7 @@ mobile-keyboard { z-index: 2000; font-size: clamp(0.95rem, 2.4vw, 1.15rem); --key-width: clamp(1.7rem, 5.5vw, 2.1rem); - --key-height: calc(var(--key-width) * 1.2); + --key-height: calc(var(--key-width) * 1.4); --stagger-factor: 0.4; --stagger-factor-deep: 0.8; --up-arrow-offset: calc(0 - var(--key-width) * var(--stagger-factor-deep)); @@ -291,11 +450,17 @@ mobile-keyboard .keyboard { flex-direction: column; gap: 0.35rem; padding: 0.75em; - background: #0a0805; - background-image: - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), + radial-gradient(ellipse 900px 700px at 25% 35%, rgba(0,0,0,.3) 0%, transparent 45%), + radial-gradient(ellipse 700px 600px at 75% 65%, rgba(255,255,255,.02) 0%, transparent 50%), + linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); border-top: 2px solid #3a3530; box-shadow: 0 -0.6rem 1.4rem rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.05); margin: 0 auto; @@ -340,13 +505,18 @@ mobile-keyboard button { border-radius: 0.5rem; border: none; background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.015) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(0,0,0,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px), + radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.35) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%), linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%); font-family: inherit; font-weight: 600; @@ -371,42 +541,57 @@ mobile-keyboard button:active { mobile-keyboard button:hover { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.025) 3px, rgba(0,0,0,.025) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.018) 4px, rgba(0,0,0,.018) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.05) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(0,0,0,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.04) 0.8px, transparent 0.8px), radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.45) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%), linear-gradient(180deg, #fffbf5 0%, #f8f4ef 100%); transform: scale(1.05); } mobile-keyboard button.backspace { - width: calc(var(--key-width) * 0.8); + width: calc(var(--key-width) * 1.3); } mobile-keyboard button[aria-pressed="true"] { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.03) 3px, rgba(0,0,0,.03) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.02) 4px, rgba(0,0,0,.02) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,200,0,.12) 3px, rgba(255,200,0,.12) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,200,0,.08) 4px, rgba(255,200,0,.08) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,200,0,.08) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(255,200,0,.05) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,200,0,.06) 0.8px, transparent 0.8px), + radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.3) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%), linear-gradient(180deg, #fffef5 0%, #fffbf0 100%); box-shadow: 0 3px 8px rgba(0,0,0,0.3), inset 0 0 0 2px #ffc107, inset 0 1px 0 rgba(255,255,255,0.8); } mobile-keyboard .nav { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.08) 3px, rgba(0,0,0,.08) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.05) 4px, rgba(0,0,0,.05) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.04) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.06) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.03) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(255,255,255,.02) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(0,0,0,.04) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.03) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.03) 0%, transparent 70%), linear-gradient(180deg, #5a8ca4 0%, #4a7c94 100%); color: #f0f8ff; width: var(--key-width); @@ -418,12 +603,17 @@ mobile-keyboard .nav { mobile-keyboard .nav:hover { background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.1) 3px, rgba(0,0,0,.1) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.06) 4px, rgba(0,0,0,.06) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.05) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.08) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.04) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(255,255,255,.025) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(0,0,0,.05) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.04) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.04) 0%, transparent 70%), linear-gradient(180deg, #6a9cb4 0%, #5a8ca4 100%); } @@ -435,13 +625,18 @@ mobile-keyboard .space { border-radius: 0.5rem; border: none; background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.015) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(0,0,0,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px), + radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.35) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%), linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%); font-family: inherit; font-weight: 600; @@ -466,11 +661,14 @@ mobile-keyboard .handle { left: 50%; transform: translateX(-50%); background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.15) 1px, rgba(0,0,0,.15) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.15) 1px, rgba(0,0,0,.15) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.08) 1px, rgba(0,0,0,.08) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.08) 1px, rgba(0,0,0,.08) 2px), - #0a0805; + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), + linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); color: #a89f99; padding: 0.55rem 0.9rem; border-radius: 0.25rem 0.25rem 0 0; @@ -501,32 +699,39 @@ crossword-menu { } .menu-container { - background: #0a0805; - background-image: - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), - repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), + radial-gradient(ellipse 900px 700px at 25% 35%, rgba(0,0,0,.3) 0%, transparent 45%), + radial-gradient(ellipse 700px 600px at 75% 65%, rgba(255,255,255,.02) 0%, transparent 50%), + linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); min-height: 100vh; display: flex; - align-items: center; + align-items: flex-start; justify-content: center; - padding: 2rem; + padding: 1rem; box-sizing: border-box; } .menu { background: #f9f7f3; background-image: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.015) 1.5px, transparent 1.5px), + radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.35) 0%, transparent 40%), radial-gradient(ellipse 600px 500px at 70% 60%, rgba(0,0,0,.02) 0%, transparent 50%), linear-gradient(135deg, #fefdfb 0%, #f9f7f3 100%); - padding: 3rem; + padding: 2rem; border-radius: 0.5rem; box-shadow: 0 10px 40px rgba(0,0,0,0.3), @@ -545,21 +750,21 @@ crossword-menu { } .form-group { - margin-bottom: 1.5rem; + margin-bottom: 1rem; } .form-group label { display: block; color: #1a1815; font-weight: 600; - margin-bottom: 0.5rem; + margin-bottom: 0.3rem; font-size: 0.95rem; } .form-group select, .form-group input[type="text"] { width: 100%; - padding: 0.75rem; + padding: 0.6rem; border: 1px solid #c0bbb5; border-radius: 0.25rem; font-size: 1rem; @@ -579,15 +784,21 @@ crossword-menu { .menu button { width: 100%; - padding: 0.75rem; + padding: 0.6rem; + margin-bottom: 0.7rem; background: - repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), - repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), - repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), - radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.015) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(0,0,0,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(255,255,255,.03) 0.8px, transparent 0.8px), + radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.35) 0%, transparent 40%), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%), linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%); color: #1a1815; border: 1px solid #c0bbb5; @@ -655,12 +866,20 @@ crossword-menu { border-radius: 0.375rem; font-size: 0.95rem; font-weight: 500; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); animation: slideIn 0.3s ease-out; pointer-events: auto; gap: 0.75rem; max-width: 350px; - backdrop-filter: blur(4px); + background: + repeating-linear-gradient(87deg, transparent, transparent 2px, rgba(0,0,0,.015) 2px, rgba(0,0,0,.015) 3px), + repeating-linear-gradient(22deg, transparent, transparent 3px, rgba(0,0,0,.01) 3px, rgba(0,0,0,.01) 4px), + repeating-linear-gradient(59deg, transparent, transparent 2px, rgba(255,255,255,.03) 2px, rgba(255,255,255,.03) 3px), + repeating-linear-gradient(-11deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 4px), + repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.02) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.02) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.02) 0%, transparent 70%); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); } @keyframes slideIn { @@ -785,9 +1004,71 @@ crossword-menu { flex-grow: 1; } +.header-buttons { + display: flex; + gap: 0.5rem; +} + +.share-game-btn { + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,180,255,.04) 3px, rgba(100,180,255,.04) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,180,255,.025) 4px, rgba(100,180,255,.025) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,180,255,.03) 3px, rgba(100,180,255,.03) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,180,255,.02) 4px, rgba(100,180,255,.02) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(100,180,255,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,180,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(100,180,255,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,180,255,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(100,180,255,.015) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.03) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.03) 0%, transparent 70%), + linear-gradient(180deg, #4a5a6a 0%, #3a4a5a 100%); + border: 1px solid rgba(100, 180, 255, 0.3); + color: #f5f1ed; + cursor: pointer; + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + transition: all 0.2s ease; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.08); +} + +.share-game-btn:hover { + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,180,255,.05) 3px, rgba(100,180,255,.05) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,180,255,.03) 4px, rgba(100,180,255,.03) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,180,255,.04) 3px, rgba(100,180,255,.04) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,180,255,.025) 4px, rgba(100,180,255,.025) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(100,180,255,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,180,255,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(100,180,255,.025) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,180,255,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(100,180,255,.02) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(100,180,255,.05) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(100,180,255,.05) 0%, transparent 70%), + linear-gradient(180deg, #5a6a7a 0%, #4a5a6a 100%); + border-color: rgba(100, 180, 255, 0.5); +} + .close-game-btn { - background: none; - border: 1px solid rgba(245, 241, 237, 0.3); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.04) 3px, rgba(255,255,255,.04) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.025) 4px, rgba(255,255,255,.025) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(180,80,80,.03) 3px, rgba(180,80,80,.03) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(180,80,80,.02) 4px, rgba(180,80,80,.02) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(180,80,80,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(180,80,80,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(180,80,80,.015) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.03) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.03) 0%, transparent 70%), + linear-gradient(180deg, #5a4a4a 0%, #4a3a3a 100%); + border: 1px solid rgba(160, 90, 90, 0.3); color: #f5f1ed; font-size: 1.5rem; cursor: pointer; @@ -801,11 +1082,25 @@ crossword-menu { flex-shrink: 0; font-weight: 300; line-height: 1; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.08); } .close-game-btn:hover { - background: rgba(245, 241, 237, 0.1); - border-color: rgba(245, 241, 237, 0.6); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.045) 3px, rgba(255,255,255,.045) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(180,80,80,.04) 3px, rgba(180,80,80,.04) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(180,80,80,.025) 4px, rgba(180,80,80,.025) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(180,80,80,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.025) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(180,80,80,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(180,80,80,.02) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.035) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.035) 0%, transparent 70%), + linear-gradient(180deg, #6a5a5a 0%, #5a4a4a 100%); + border-color: rgba(160, 90, 90, 0.4); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255,255,255,0.1); } .close-game-btn:active { @@ -827,7 +1122,6 @@ crossword-grid { display: inline-block; min-width: fit-content; /* Ensure grid takes its full needed width */ border-radius: 0px; - background: var(--ink-dark); position: relative; } @@ -837,7 +1131,17 @@ crossword-grid { top: 0; left: 0; right: 0; - background: rgba(26, 24, 21, 0.95); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), + radial-gradient(ellipse 900px 700px at 25% 35%, rgba(0,0,0,.3) 0%, transparent 45%), + radial-gradient(ellipse 700px 600px at 75% 65%, rgba(255,255,255,.02) 0%, transparent 50%), + linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); backdrop-filter: blur(8px); border-bottom: 2px solid rgba(245, 241, 237, 0.2); z-index: 1000; @@ -879,8 +1183,20 @@ crossword-grid { } .clue-toggle { - background: none; - border: 1px solid rgba(245, 241, 237, 0.3); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.04) 3px, rgba(255,255,255,.04) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.025) 4px, rgba(255,255,255,.025) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,130,180,.03) 3px, rgba(100,130,180,.03) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,130,180,.02) 4px, rgba(100,130,180,.02) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.025) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,130,180,.02) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.02) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,130,180,.01) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(100,130,180,.015) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.03) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.03) 0%, transparent 70%), + linear-gradient(180deg, #3a4a5a 0%, #2a3a4a 100%); + border: 1px solid rgba(100, 130, 180, 0.3); color: #f5f1ed; font-size: 1rem; cursor: pointer; @@ -893,11 +1209,25 @@ crossword-grid { transition: all 0.2s ease; flex-shrink: 0; padding: 0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.08); } .clue-toggle:hover { - background: rgba(245, 241, 237, 0.1); - border-color: rgba(245, 241, 237, 0.6); + background: + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.045) 3px, rgba(255,255,255,.045) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,130,180,.04) 3px, rgba(100,130,180,.04) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,130,180,.025) 4px, rgba(100,130,180,.025) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(100,130,180,.03) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.025) 1.5px, transparent 1.5px), + repeating-radial-gradient(circle at 23% 67%, rgba(100,130,180,.015) 0.8px, transparent 0.8px), + repeating-radial-gradient(circle at 78% 22%, rgba(100,130,180,.02) 0.8px, transparent 0.8px), + radial-gradient(circle at 0% 0%, rgba(0,0,0,.035) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0,0,0,.035) 0%, transparent 70%), + linear-gradient(180deg, #4a5a6a 0%, #3a4a5a 100%); + border-color: rgba(100, 130, 180, 0.4); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255,255,255,0.1); } .clue-toggle:active { @@ -920,7 +1250,7 @@ crossword-grid { .clue-list-container { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr; gap: 1.5rem; margin-top: 1rem; } @@ -960,6 +1290,11 @@ crossword-grid { background: rgba(255, 255, 255, 0.05); } +.clue-item.solved .clue-text { + text-decoration: line-through; + opacity: 0.6; +} + .clue-item .clue-number { flex-shrink: 0; min-width: 2rem;