Compare commits

..

2 Commits

Author SHA1 Message Date
372e246124 more frontend fixes 2025-11-14 15:17:31 +01:00
9f02bc1888 more frontend fixes 2025-11-13 18:41:34 +01:00
9 changed files with 1254 additions and 241 deletions

View File

@ -49,6 +49,10 @@ class Crossword:
@property @property
def current_grid(self) -> List[List[Optional[str]]]: def current_grid(self) -> List[List[Optional[str]]]:
return self._current_grid 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]: 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) 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( cw = Crossword(
dictionary=dictionary, dictionary=dictionary,
grid=final_step.grid, grid=final_step.grid,
solution_word_positions=solution_word_positions
) )
logger.debug("Generated Crossword: \n\n%s", cw) logger.debug("Generated Crossword: \n\n%s", cw)
return cw return cw
@ -139,7 +192,19 @@ class Crossword:
grid: List[List[Optional[str]]], grid: List[List[Optional[str]]],
current_grid: Optional[List[List[Optional[str]]]] = None, current_grid: Optional[List[List[Optional[str]]]] = None,
words: Optional[List[CrosswordWord]] = 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._dictionary = dictionary
self._solved_grid = grid self._solved_grid = grid
self._words: List[CrosswordWord] = [] self._words: List[CrosswordWord] = []
@ -147,6 +212,8 @@ class Crossword:
self._horizontal_words_by_y_x_position = {} self._horizontal_words_by_y_x_position = {}
self._vertical_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: if current_grid is not None:
self._current_grid = current_grid self._current_grid = current_grid

View File

@ -6,7 +6,7 @@ DEFAULT_WEBSOCKET_PORT = 8765
DEFAULT_MIN_GRID_SIZE = 10 DEFAULT_MIN_GRID_SIZE = 10
DEFAULT_MAX_GRID_SIZE = 30 DEFAULT_MAX_GRID_SIZE = 30
DEFAULT_GRID_BLOCK_RATIO = 0.38 DEFAULT_GRID_BLOCK_RATIO = 0.39
DEFAULT_MAX_SESSION_IDLE_TIME_SECONDS = 3600 * 48 # 2 days DEFAULT_MAX_SESSION_IDLE_TIME_SECONDS = 3600 * 48 # 2 days

View File

@ -30,9 +30,10 @@ class SendFullSessionStateServerMessage(ServerMessageBase):
grid: list[list[str]] # 2D array representing the current grid state 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_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 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_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 (col, row) 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 (col, row) positions that are solved 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): class LetterUpdateBroadcastServerMessage(ServerMessageBase):
type: str = "letter_update" type: str = "letter_update"

View File

@ -224,6 +224,11 @@ class WebsocketCrosswordServer(object):
solved_positions = list(solved_positions) 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( response = server_messages.SendFullSessionStateServerMessage(
session_id=session.session_id, session_id=session.session_id,
grid=grid_state, grid=grid_state,
@ -232,6 +237,7 @@ class WebsocketCrosswordServer(object):
clue_positions_across=clue_positions_across, clue_positions_across=clue_positions_across,
clue_positions_down=clue_positions_down, clue_positions_down=clue_positions_down,
solved_positions=solved_positions, solved_positions=solved_positions,
solution_word_positions=solution_word_positions,
) )
# register the client to the session # register the client to the session
session.add_client(handler) session.add_client(handler)

View File

@ -15,8 +15,11 @@ export class ClueArea extends LitElement {
selectedCol: { type: Number }, selectedCol: { type: Number },
selectedMode: { type: String }, // 'horizontal' or 'vertical' selectedMode: { type: String }, // 'horizontal' or 'vertical'
grid: { type: Array }, // 2D grid from server (needed to find walls) grid: { type: Array }, // 2D grid from server (needed to find walls)
gridData: { type: Object }, // { rows, cols, walls, solvedCells }
_showAllCluesAcross: { state: true }, _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.selectedCol = 0;
this.selectedMode = 'horizontal'; this.selectedMode = 'horizontal';
this.grid = []; this.grid = [];
this.gridData = { rows: 0, cols: 0, walls: new Set(), solvedCells: new Set() };
this._showAllCluesAcross = false; this._showAllCluesAcross = false;
this._showAllCluesDown = 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; this._showAllCluesAcross = !this._showAllCluesAcross;
} }
@ -186,6 +192,143 @@ export class ClueArea extends LitElement {
this._showAllCluesDown = !this._showAllCluesDown; 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() { render() {
const currentClue = this._getCurrentClue(); const currentClue = this._getCurrentClue();
@ -195,15 +338,15 @@ export class ClueArea extends LitElement {
<div class="clue-area"> <div class="clue-area">
<div class="clue-header"> <div class="clue-header">
<h3>Across Clues</h3> <h3>Across Clues</h3>
<button class="clue-toggle" @click="${this._toggleShowAllClues}"> <button class="clue-toggle" @click="${this._toggleShowAllCluesAcross}">
<span class="chevron"></span> <span class="chevron"></span>
</button> </button>
</div> </div>
<div class="clue-list-container"> <div class="clue-list-container">
<div class="clue-list"> <div class="clue-list">
${Object.entries(this.cluesAcross).map(([num, text]) => html` ${Object.entries(this.cluesAcross).map(([num, text]) => html`
<div class="clue-item"> <div class="clue-item ${this._solvedCluesAcross.has(num) ? 'solved' : ''}" @click="${() => this._onClueItemClick(num, 'across')}" style="cursor: pointer;">
<span class="clue-number">${num}.</span> <span class="clue-number">${num}.</span>
<span class="clue-text">${text}</span> <span class="clue-text">${text}</span>
</div> </div>
@ -221,14 +364,14 @@ export class ClueArea extends LitElement {
<div class="clue-header"> <div class="clue-header">
<h3>Down Clues</h3> <h3>Down Clues</h3>
<button class="clue-toggle" @click="${this._toggleShowAllCluesDown}"> <button class="clue-toggle" @click="${this._toggleShowAllCluesDown}">
<span class="chevron"></span> <span class="chevron"></span>
</button> </button>
</div> </div>
<div class="clue-list-container"> <div class="clue-list-container">
<div class="clue-list"> <div class="clue-list">
${Object.entries(this.cluesDown).map(([num, text]) => html` ${Object.entries(this.cluesDown).map(([num, text]) => html`
<div class="clue-item"> <div class="clue-item ${this._solvedCluesDown.has(num) ? 'solved' : ''}" @click="${() => this._onClueItemClick(num, 'down')}" style="cursor: pointer;">
<span class="clue-number">${num}.</span> <span class="clue-number">${num}.</span>
<span class="clue-text">${text}</span> <span class="clue-text">${text}</span>
</div> </div>
@ -253,11 +396,13 @@ export class ClueArea extends LitElement {
`} `}
<div class="clue-toggle-group"> <div class="clue-toggle-group">
<button class="clue-toggle" @click="${this._toggleShowAllClues}" title="Show all across clues"> <div class="clue-text empty">Show all clues:</div>
<span class="chevron">▶ A</span>
<button class="clue-toggle" @click="${this._toggleShowAllCluesAcross}" title="Show all across clues">
<span class="chevron">▶</span>
</button> </button>
<button class="clue-toggle" @click="${this._toggleShowAllCluesDown}" title="Show all down clues"> <button class="clue-toggle" @click="${this._toggleShowAllCluesDown}" title="Show all down clues">
<span class="chevron">▼ D</span> <span class="chevron">▼</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -17,6 +17,11 @@ export class CrosswordGrid extends LitElement {
_selected: { state: true }, _selected: { state: true },
_inputMode: { state: true }, // 'horizontal' or 'vertical' _inputMode: { state: true }, // 'horizontal' or 'vertical'
_solvedCells: { state: true }, // tracks which cells are solved _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 // styles moved to webui/styles.css; render into light DOM so external CSS applies
@ -29,6 +34,11 @@ export class CrosswordGrid extends LitElement {
this._selected = { r: 0, c: 0 }; this._selected = { r: 0, c: 0 };
this._inputMode = 'horizontal'; // default input mode this._inputMode = 'horizontal'; // default input mode
this._solvedCells = new Set(); // set of "r,c" strings for solved cells 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 this.sessionId = null; // Session ID for sending updates to server
} }
@ -71,8 +81,31 @@ export class CrosswordGrid extends LitElement {
this._ensureGrid(); this._ensureGrid();
// set CSS variables for cell-size and column count; layout done in external stylesheet // set CSS variables for cell-size and column count; layout done in external stylesheet
return html` return html`
<div class="grid" style="--cell-size: ${this._cellSize}px; --cols: ${this.cols};"> <div class="grid-container ${this._isSolutionWordComplete() ? 'complete' : ''}">
${this._grid.map((row, r) => row.map((cell, c) => this._renderCell(r, c, cell))).flat()} <div class="grid" style="--cell-size: ${this._cellSize}px; --cols: ${this.cols};">
${this._grid.map((row, r) => row.map((cell, c) => this._renderCell(r, c, cell))).flat()}
</div>
</div>
${this._solutionWordPositions.length > 0 ? html`
<h3 style="margin-top: 2rem;">Solution Word</h3>
<div class="grid solution-word-grid" style="--cell-size: 40px; --cols: ${this._solutionWordPositions.length};">
${this._solutionWordPositions.map((pos, i) => this._renderSolutionCell(i, pos))}
</div>
` : ''}
`;
}
_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`
<div class="${classes.join(' ')}" data-solution-index="${index}" data-row="${position[1]}" data-col="${position[0]}" @click=${() => this._onSolutionCellClick(index, position)}>
<div class="solution-circle"></div>
<span class="solution-index">${index + 1}</span>
<span class="cell-letter">${letter}</span>
</div> </div>
`; `;
} }
@ -119,7 +152,34 @@ export class CrosswordGrid extends LitElement {
} }
} }
return html`<div class="${classes.join(' ')}" @click=${() => this._onCellClick(r, c)} data-r="${r}" data-c="${c}">${value}</div>`; // Get clue numbers for this cell
const clueInfo = this._clueNumbers.get(cellKey);
let clueNumberDisplay = '';
if (clueInfo) {
if (clueInfo.across !== null && clueInfo.down !== null) {
// Both across and down clues: show "across/down" format
clueNumberDisplay = `${clueInfo.across}/${clueInfo.down}`;
} else if (clueInfo.across !== null) {
// Only across clue
clueNumberDisplay = String(clueInfo.across);
} else if (clueInfo.down !== null) {
// Only down clue
clueNumberDisplay = String(clueInfo.down);
}
}
// Get solution index for this cell
const solutionIndex = this._solutionIndices.get(cellKey);
const cellContent = clueNumberDisplay
? html`<span class="clue-number">${clueNumberDisplay}</span><span class="cell-letter">${value}</span>`
: html`<span class="cell-letter">${value}</span>`;
const cellHTML = solutionIndex !== undefined
? html`${cellContent}<div class="solution-circle"></div><span class="solution-index">${solutionIndex}</span>`
: cellContent;
return html`<div class="${classes.join(' ')}" @click=${() => this._onCellClick(r, c)} data-r="${r}" data-c="${c}">${cellHTML}</div>`;
} }
/** /**
@ -182,6 +242,14 @@ export class CrosswordGrid extends LitElement {
return end - start + 1; 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 * 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) * (i.e., same column and not blocked by walls above/below this cell)
@ -206,25 +274,35 @@ export class CrosswordGrid extends LitElement {
return r >= start && r <= end; return r >= start && r <= end;
} }
_onCellClick(r, c) { _onCellClick(r, c, preferredMode = null) {
// if same cell is clicked again, toggle the input mode // if same cell is clicked again, toggle the input mode
if (this._selected.r === r && this._selected.c === c) { 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 { } else {
// select a new cell // select a new cell
this._selected = { r, c }; this._selected = { r, c };
// auto-select mode based on line lengths // Use preferred mode if provided, otherwise auto-select based on line lengths
const horizontalLength = this._getHorizontalLineLength(r, c); if (preferredMode) {
const verticalLength = this._getVerticalLineLength(r, c); this._inputMode = preferredMode;
} else {
// if one mode only has 1 cell but the other has multiple, use the one with multiple // auto-select mode based on line lengths
if (horizontalLength === 1 && verticalLength > 1) { const horizontalLength = this._getHorizontalLineLength(r, c);
this._inputMode = 'vertical'; const verticalLength = this._getVerticalLineLength(r, c);
} else if (verticalLength === 1 && horizontalLength > 1) {
this._inputMode = 'horizontal'; // 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.requestUpdate();
this.dispatchEvent(new CustomEvent('cell-selected', { detail: { row: r, col: c, mode: this._inputMode }, bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent('cell-selected', { detail: { row: r, col: c, mode: this._inputMode }, bubbles: true, composed: true }));
@ -232,6 +310,12 @@ export class CrosswordGrid extends LitElement {
this.focus(); 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) { _onKeydown(e) {
// Only handle keys when the grid has focus // Only handle keys when the grid has focus
// Map letters, arrows and backspace to our handlers // Map letters, arrows and backspace to our handlers
@ -461,10 +545,125 @@ export class CrosswordGrid extends LitElement {
this._solvedCells.delete(cellKey); 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(); 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})`); console.log(`Letter update from server: [${row}, ${col}] = "${letter}" (solved: ${is_solved})`);
} }
} }
/**
* Populate clue numbers from server data
* @param {Object} cluePositionsAcross - dict of clue_number -> [col, row]
* @param {Object} cluePositionsDown - dict of clue_number -> [col, row]
*/
populateClueNumbers(cluePositionsAcross = {}, cluePositionsDown = {}) {
this._clueNumbers.clear();
// Add across clues
for (const [clueNum, position] of Object.entries(cluePositionsAcross)) {
const [col, row] = position;
const cellKey = `${row},${col}`;
if (!this._clueNumbers.has(cellKey)) {
this._clueNumbers.set(cellKey, { across: null, down: null });
}
this._clueNumbers.get(cellKey).across = parseInt(clueNum);
}
// Add down clues
for (const [clueNum, position] of Object.entries(cluePositionsDown)) {
const [col, row] = position;
const cellKey = `${row},${col}`;
if (!this._clueNumbers.has(cellKey)) {
this._clueNumbers.set(cellKey, { across: null, down: null });
}
this._clueNumbers.get(cellKey).down = parseInt(clueNum);
}
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); customElements.define('crossword-grid', CrosswordGrid);

View File

@ -125,7 +125,14 @@
gridContainer.innerHTML = ` gridContainer.innerHTML = `
<div class="game-header"> <div class="game-header">
<h2>Crossword</h2> <h2>Crossword</h2>
<button class="close-game-btn" aria-label="Close game">✕</button> <div class="header-buttons">
<button class="share-game-btn" aria-label="Share game">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.15c.52.47 1.2.77 1.96.77 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.44 9.31 6.77 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.77 0 1.44-.3 1.96-.77l7.12 4.16c-.057.21-.087.43-.087.66 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/>
</svg>
</button>
<button class="close-game-btn" aria-label="Close game">✕</button>
</div>
</div> </div>
<div class="game-content"> <div class="game-content">
</div> </div>
@ -163,6 +170,7 @@
for (let r = 0; r < gridRows; r++) { for (let r = 0; r < gridRows; r++) {
for (let c = 0; c < gridCols; c++) { for (let c = 0; c < gridCols; c++) {
const cell = message.grid[r][c]; const cell = message.grid[r][c];
// Skip walls and empty cells // Skip walls and empty cells
if (cell !== '#' && cell !== '') { if (cell !== '#' && cell !== '') {
gridElement._grid[r][c] = cell; gridElement._grid[r][c] = cell;
@ -178,6 +186,14 @@
} }
} }
// 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(); gridElement.requestUpdate();
console.log(`Grid created: ${gridRows}x${gridCols} with ${wallPositions.length} walls`); console.log(`Grid created: ${gridRows}x${gridCols} with ${wallPositions.length} walls`);
@ -185,6 +201,9 @@
if (message.solved_positions) { if (message.solved_positions) {
console.log(`Solved positions: ${message.solved_positions.length}`); console.log(`Solved positions: ${message.solved_positions.length}`);
} }
if (message.solution_word_positions) {
console.log(`Solution word positions: ${message.solution_word_positions.length}`);
}
}, 0); }, 0);
// Create and add clue area // Create and add clue area
@ -194,9 +213,36 @@
clueArea.cluePositionsAcross = message.clue_positions_across; clueArea.cluePositionsAcross = message.clue_positions_across;
clueArea.cluePositionsDown = message.clue_positions_down; clueArea.cluePositionsDown = message.clue_positions_down;
clueArea.grid = message.grid; // Pass grid for dimension calculation 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.selectedRow = 0;
clueArea.selectedCol = 0; clueArea.selectedCol = 0;
clueArea.selectedMode = 'horizontal'; clueArea.selectedMode = 'horizontal';
// Update solved clues initially
clueArea._updateSolvedClues();
document.body.insertBefore(clueArea, document.body.firstChild); document.body.insertBefore(clueArea, document.body.firstChild);
// Listen for cell selection changes // Listen for cell selection changes
@ -207,12 +253,157 @@
clueArea.requestUpdate(); 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 // Close button handler
closeBtn.addEventListener('click', closeGame); closeBtn.addEventListener('click', closeGame);
const shareBtn = gridContainer.querySelector('.share-game-btn');
shareBtn.addEventListener('click', shareGame);
notificationManager.success('Game loaded successfully'); 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 = `
<h3 style="margin: 0 0 1rem 0; color: #f5f1ed; font-size: 1.2rem;">Share Game Link</h3>
<p style="margin: 0 0 1rem 0; color: #d4cdc5; font-size: 0.9rem;">Copy this link and send it to friends:</p>
<input type="text" value="${url}" readonly style="
width: 100%;
padding: 0.75rem;
background: #1a1511;
color: #f5f1ed;
border: 1px solid #5a4a4a;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.85rem;
box-sizing: border-box;
margin-bottom: 1rem;
" id="share-url-input" />
<div style="display: flex; gap: 0.5rem;">
<button id="copy-btn" style="
flex: 1;
padding: 0.75rem;
background: #4a7a9e;
color: #f5f1ed;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 600;
">Copy</button>
<button id="close-share-btn" style="
flex: 1;
padding: 0.75rem;
background: #5a4a4a;
color: #f5f1ed;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 600;
">Close</button>
</div>
`;
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 to close game and return to menu
function closeGame() { function closeGame() {
console.log('Closing game'); console.log('Closing game');

View File

@ -25,72 +25,97 @@ export class MobileKeyboard extends LitElement {
createRenderRoot() { return this; } createRenderRoot() { return this; }
render() { render() {
// simple QWERTY-like rows // simple QWERTY-like rows
const rows = [ const rows = [
'qwertyuiop'.split(''), 'qwertyuiop'.split(''),
'asdfghjkl'.split(''), 'asdfghjkl'.split(''),
'zxcvbnm'.split(''), 'zxcvbnm'.split(''),
]; ];
// compute the maximum number of columns across rows (account for backspace in first row) // compute the maximum number of columns across rows (account for backspace in second row now)
const counts = rows.map((r, idx) => r.length + (idx === 0 ? 1 : 0)); 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 arrowCols = 3; // reserve 3 columns on the right for [left][down][right]
const baseMax = Math.max(...counts, 10); const baseMax = Math.max(...counts, 10);
const maxCols = baseMax; const maxCols = baseMax;
return html` return html`
<div class="keyboard-container"> <div class="keyboard-container">
${html`<div class="handle" @click=${this._toggleCollapse}>${this.collapsed ? '▲' : '▼'}</div>`} ${html`<div class="handle" @click=${this._toggleCollapse}>${this.collapsed ? '▲' : '▼'}</div>`}
<div class="keyboard-wrapper"> <div class="keyboard-wrapper">
<div class="keyboard"> <div class="keyboard">
${rows.map((r, idx) => { ${rows.map((r, idx) => {
// center the letter keys leaving the rightmost `arrowCols` for the arrow block // center the letter keys leaving the rightmost `arrowCols` for the arrow block
let rowClasses = 'row'; let rowClasses = 'row';
if (idx === 1) rowClasses += ' stagger'; // A row if (idx === 1) rowClasses += ' stagger'; // A row
if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent
return html`<div class="${rowClasses}" style="--cols:${maxCols-idx}; --arrow-cols:${arrowCols};"> return html`<div class="${rowClasses}" style="--cols:${maxCols-idx}; --arrow-cols:${arrowCols};">
<div class="keys"> <div class="keys">
${r.map(l => html`<button @click=${() => this._emitLetter(l)}>${l}</button>`) } ${r.map(l => html`<button @click=${() => this._emitLetter(l)}>${l}</button>`) }
${idx === 0 ? html`<button class="backspace" @click=${() => this._emit({ type: 'backspace' })}>⌫</button>` : ''} ${idx === 1 ? html`<button class="backspace" @click=${() => this._emitBackspace()}>⌫</button>` : ''}
</div> </div>
<div class="arrows"> <div class="arrows">
${Array.from({ length: arrowCols }).map((_, i) => { ${Array.from({ length: arrowCols }).map((_, i) => {
if (idx === 2 && i === 1) return html`<button class="nav" @click=${() => this._emitNavigate('up')}>▲</button>`; if (idx === 2 && i === 1) return html`<button class="nav" @click=${() => this._emitNavigate('up')}>▲</button>`;
return html`<div class="key-spacer"></div>`; return html`<div class="key-spacer"></div>`;
})} })}
</div> </div>
</div>`; </div>`;
})} })}
<!-- spacebar row --> <!-- spacebar row -->
<div class="row" style="--cols:${maxCols};"> <div class="row" style="--cols:${maxCols};">
<!-- spacebar spans all but the right arrow columns --> <!-- spacebar spans all but the right arrow columns -->
<button class="space" @click=${() => this._emit({ type: 'letter', value: '' })}></button> <button class="space" @click=${() => this._emitSpace()}></button>
<!-- arrow columns: left, down, right (will occupy the last 3 columns) --> <!-- arrow columns: left, down, right (will occupy the last 3 columns) -->
<button class="nav" @click=${() => this._emitNavigate('left')}></button> <button class="nav" @click=${() => this._emitNavigate('left')}></button>
<button class="nav" @click=${() => this._emitNavigate('down')}></button> <button class="nav" @click=${() => this._emitNavigate('down')}></button>
<button class="nav" @click=${() => this._emitNavigate('right')}></button> <button class="nav" @click=${() => this._emitNavigate('right')}></button>
</div>
</div> </div>
</div> </div>
</div> </div>
`; </div>
} `;
} _emitLetter(l) {
_emitLetter(l) { this._vibrate();
this._emit({ type: 'letter', value: l }); this._emit({ type: 'letter', value: l });
} }
_emitNavigate(dir) { _emitNavigate(dir) {
this._vibrate();
this._emit({ type: 'navigate', value: dir }); this._emit({ type: 'navigate', value: dir });
} }
_emitBackspace() {
this._vibrate();
this._emit({ type: 'backspace' });
}
_emitSpace() {
this._vibrate();
this._emit({ type: 'letter', value: '' });
}
_emit(detail) { _emit(detail) {
window.dispatchEvent(new CustomEvent('key-press', { 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() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener('resize', this._onResize); window.addEventListener('resize', this._onResize);

View File

@ -20,11 +20,17 @@ html, body { -webkit-text-size-adjust: 100%; }
body { body {
font-family: 'Segoe UI', 'Helvetica Neue', system-ui, Roboto, Arial; font-family: 'Segoe UI', 'Helvetica Neue', system-ui, Roboto, Arial;
margin: 0; margin: 0;
background: #0a0805; background:
background-image: repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); 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); color: var(--ink-dark);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
font-size: 100%; font-size: 100%;
@ -36,36 +42,61 @@ main {
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
padding: var(--page-padding); padding: var(--page-padding);
padding-bottom: var(--keyboard-space); padding-bottom: 0; /* Remove keyboard space by default (desktop) */
min-height: calc(100vh - 0px); min-height: calc(100vh - 0px);
box-sizing: border-box; box-sizing: border-box;
} }
.container { width: 100%; max-width: var(--max-container-width); }
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; }
crossword-grid { /* Add keyboard space on mobile devices */
display: block; @media (max-width: 900px) {
max-width: 100%; main {
margin: 0 auto; padding-bottom: var(--keyboard-space);
}
border-radius: 0px; }
background: var(--ink-dark); .container {
position: relative; width: fit-content; /* Size exactly to content */
max-width: 100%; /* But don't exceed viewport */
} }
.grid { #grid-container {
display: grid; width: 100%;
gap: 0; overflow-x: auto;
background: var(--ink-dark); overflow-y: auto;
background-image: display: flex;
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.025) 1px, rgba(0,0,0,.025) 2px), justify-content: center; /* Center small grids */
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.025) 1px, rgba(0,0,0,.025) 2px); }
padding: 0.8rem; crossword-grid { display: block; margin: 0 auto; }
grid-template-columns: repeat(var(--cols), var(--cell-size)); mobile-keyboard { display: block; }
border: 2px solid var(--ink-dark); 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-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 { .cell {
@ -75,14 +106,19 @@ crossword-grid {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.03) 3px, rgba(0,0,0,.03) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.025) 4px, rgba(0,0,0,.025) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.03) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.05) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), repeating-radial-gradient(circle at 34% 51%, rgba(0,0,0,.02) 1.5px, transparent 1.5px),
radial-gradient(ellipse 600px 500px at 70% 60%, rgba(0,0,0,.02) 0%, transparent 50%), 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%); linear-gradient(135deg, #fefdfb 0%, #f9f7f3 100%);
color: var(--ink-dark); color: var(--ink-dark);
font-weight: 700; font-weight: 700;
@ -108,11 +144,18 @@ crossword-grid {
.cell.wall { .cell.wall {
background: background:
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(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.05) 2px, rgba(0,0,0,.05) 4px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.025) 3px, rgba(0,0,0,.025) 5px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.05) 2px, rgba(0,0,0,.05) 4px), 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(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%); linear-gradient(135deg, #3a3530 0%, #2a2520 100%);
color: transparent; color: transparent;
border-color: var(--wall-dark); border-color: var(--wall-dark);
@ -123,31 +166,107 @@ crossword-grid {
inset 1px 1px 2px rgba(255,255,255,0.05); inset 1px 1px 2px rgba(255,255,255,0.05);
} }
.cell.wall .cell-letter {
font-size: 0;
text-shadow: none;
}
.cell.wall.selected { .cell.wall.selected {
border-color: var(--accent-wall); 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); 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: background:
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(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), 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%); linear-gradient(135deg, #3a3530 0%, #2a2520 100%);
} }
.cell.wall.mode-highlighted { .cell.wall.mode-highlighted {
background: background:
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(-45deg, transparent, transparent 2px, rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px), 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%); linear-gradient(135deg, #3a3530 0%, #2a2520 100%);
} }
.cell.mode-highlighted { /* Clue number styling - only in grid cells */
.cell .clue-number {
position: absolute;
top: 2px;
left: 2px;
font-size: 0.55rem;
font-weight: 700;
line-height: 1;
color: #000000 !important;
pointer-events: none;
}
.cell-letter {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
filter: contrast(1.05);
letter-spacing: 0.02em;
}
.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: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(100,180,200,.2) 1px, rgba(100,180,200,.2) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,180,200,.15) 3px, rgba(100,180,200,.15) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(100,180,200,.2) 1px, rgba(100,180,200,.2) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,180,200,.1) 4px, rgba(100,180,200,.1) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(100,180,200,.1) 1px, rgba(100,180,200,.1) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,180,200,.08) 3px, rgba(100,180,200,.08) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(100,180,200,.1) 1px, rgba(100,180,200,.1) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,180,200,.05) 4px, rgba(100,180,200,.05) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(100,180,200,.05) 2px, rgba(100,180,200,.05) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(100,180,200,.07) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(100,180,200,.05) 2px, rgba(100,180,200,.05) 4px), 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(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%); linear-gradient(135deg, #f0fafb 0%, #e8f4f8 100%);
box-shadow: box-shadow:
inset 0 1px 2px rgba(255,255,255,0.9), inset 0 1px 2px rgba(255,255,255,0.9),
@ -159,15 +278,44 @@ crossword-grid {
} }
/* Solved cells - green background and not editable */ /* 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 { .cell.solved {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(100,200,100,.15) 1px, rgba(100,200,100,.15) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(100,200,100,.1) 3px, rgba(100,200,100,.1) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(100,200,100,.15) 1px, rgba(100,200,100,.15) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(100,200,100,.08) 4px, rgba(100,200,100,.08) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(100,200,100,.1) 1px, rgba(100,200,100,.1) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(100,200,100,.06) 3px, rgba(100,200,100,.06) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(100,200,100,.1) 1px, rgba(100,200,100,.1) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(100,200,100,.04) 4px, rgba(100,200,100,.04) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(100,200,100,.05) 2px, rgba(100,200,100,.05) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(100,200,100,.06) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(100,200,100,.05) 2px, rgba(100,200,100,.05) 4px), 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(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%); linear-gradient(135deg, #d4f4d4 0%, #c8ead4 100%);
box-shadow: box-shadow:
inset 0 1px 2px rgba(255,255,255,0.8), inset 0 1px 2px rgba(255,255,255,0.8),
@ -175,21 +323,58 @@ crossword-grid {
0 0.5px 1px rgba(0,0,0,0.05), 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(100,200,100,0.08),
inset 1px 1px 2px rgba(255,255,255,0.3); 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 { .cell.selected {
outline: none; outline: none;
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,200,0,.15) 3px, rgba(255,200,0,.15) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,200,0,.1) 4px, rgba(255,200,0,.1) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,200,0,.08) 3px, rgba(255,200,0,.08) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,200,0,.05) 4px, rgba(255,200,0,.05) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(255,200,0,.07) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), 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(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%); linear-gradient(135deg, #fffef9 0%, #fff8e6 100%);
border-color: var(--ink-dark); border-color: var(--ink-dark);
box-shadow: box-shadow:
@ -202,13 +387,18 @@ crossword-grid {
.cell.selected.mode-highlighted { .cell.selected.mode-highlighted {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(200,150,0,.2) 1px, rgba(200,150,0,.2) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(200,150,0,.15) 3px, rgba(200,150,0,.15) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(200,150,0,.2) 1px, rgba(200,150,0,.2) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(200,150,0,.1) 4px, rgba(200,150,0,.1) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(200,150,0,.1) 1px, rgba(200,150,0,.1) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(200,150,0,.08) 3px, rgba(200,150,0,.08) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(200,150,0,.1) 1px, rgba(200,150,0,.1) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(200,150,0,.05) 4px, rgba(200,150,0,.05) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(200,150,0,.05) 2px, rgba(200,150,0,.05) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(200,150,0,.07) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(200,150,0,.05) 2px, rgba(200,150,0,.05) 4px), 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(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%); linear-gradient(135deg, #fffaf0 0%, #fff5d6 100%);
box-shadow: box-shadow:
inset 0 1px 2px rgba(255,255,255,0.9), inset 0 1px 2px rgba(255,255,255,0.9),
@ -232,7 +422,7 @@ mobile-keyboard {
z-index: 2000; z-index: 2000;
font-size: clamp(0.95rem, 2.4vw, 1.15rem); font-size: clamp(0.95rem, 2.4vw, 1.15rem);
--key-width: clamp(1.7rem, 5.5vw, 2.1rem); --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: 0.4;
--stagger-factor-deep: 0.8; --stagger-factor-deep: 0.8;
--up-arrow-offset: calc(0 - var(--key-width) * var(--stagger-factor-deep)); --up-arrow-offset: calc(0 - var(--key-width) * var(--stagger-factor-deep));
@ -260,11 +450,17 @@ mobile-keyboard .keyboard {
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: 0.35rem;
padding: 0.75em; padding: 0.75em;
background: #0a0805; background:
background-image: repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); 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; 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); 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; margin: 0 auto;
@ -309,13 +505,18 @@ mobile-keyboard button {
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), 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%); linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%);
font-family: inherit; font-family: inherit;
font-weight: 600; font-weight: 600;
@ -340,42 +541,57 @@ mobile-keyboard button:active {
mobile-keyboard button:hover { mobile-keyboard button:hover {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.025) 3px, rgba(0,0,0,.025) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.018) 4px, rgba(0,0,0,.018) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.025) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), 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(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%); linear-gradient(180deg, #fffbf5 0%, #f8f4ef 100%);
transform: scale(1.05); transform: scale(1.05);
} }
mobile-keyboard button.backspace { mobile-keyboard button.backspace {
width: calc(var(--key-width) * 0.8); width: calc(var(--key-width) * 1.3);
} }
mobile-keyboard button[aria-pressed="true"] { mobile-keyboard button[aria-pressed="true"] {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.03) 3px, rgba(0,0,0,.03) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(255,200,0,.18) 1px, rgba(255,200,0,.18) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.02) 4px, rgba(0,0,0,.02) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,200,0,.12) 3px, rgba(255,200,0,.12) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(255,200,0,.09) 1px, rgba(255,200,0,.09) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,200,0,.08) 4px, rgba(255,200,0,.08) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.025) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,200,0,.05) 2px, rgba(255,200,0,.05) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,200,0,.08) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), 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%); 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); 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 { mobile-keyboard .nav {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.08) 3px, rgba(0,0,0,.08) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.05) 4px, rgba(0,0,0,.05) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.04) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), 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%); linear-gradient(180deg, #5a8ca4 0%, #4a7c94 100%);
color: #f0f8ff; color: #f0f8ff;
width: var(--key-width); width: var(--key-width);
@ -387,12 +603,17 @@ mobile-keyboard .nav {
mobile-keyboard .nav:hover { mobile-keyboard .nav:hover {
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.06) 3px, rgba(255,255,255,.06) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.04) 4px, rgba(255,255,255,.04) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.1) 3px, rgba(0,0,0,.1) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.06) 4px, rgba(0,0,0,.06) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.05) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), 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%); linear-gradient(180deg, #6a9cb4 0%, #5a8ca4 100%);
} }
@ -404,13 +625,18 @@ mobile-keyboard .space {
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), 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%); linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%);
font-family: inherit; font-family: inherit;
font-weight: 600; font-weight: 600;
@ -435,11 +661,14 @@ mobile-keyboard .handle {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.15) 1px, rgba(0,0,0,.15) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.15) 1px, rgba(0,0,0,.15) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.08) 1px, rgba(0,0,0,.08) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.08) 1px, rgba(0,0,0,.08) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
#0a0805; 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; color: #a89f99;
padding: 0.55rem 0.9rem; padding: 0.55rem 0.9rem;
border-radius: 0.25rem 0.25rem 0 0; border-radius: 0.25rem 0.25rem 0 0;
@ -470,32 +699,39 @@ crossword-menu {
} }
.menu-container { .menu-container {
background: #0a0805; background:
background-image: repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(255,255,255,.04) 2px, rgba(255,255,255,.04) 4px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 6px); 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; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: center; justify-content: center;
padding: 2rem; padding: 1rem;
box-sizing: border-box; box-sizing: border-box;
} }
.menu { .menu {
background: #f9f7f3; background: #f9f7f3;
background-image: background-image:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), 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%), radial-gradient(ellipse 600px 500px at 70% 60%, rgba(0,0,0,.02) 0%, transparent 50%),
linear-gradient(135deg, #fefdfb 0%, #f9f7f3 100%); linear-gradient(135deg, #fefdfb 0%, #f9f7f3 100%);
padding: 3rem; padding: 2rem;
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: box-shadow:
0 10px 40px rgba(0,0,0,0.3), 0 10px 40px rgba(0,0,0,0.3),
@ -514,21 +750,21 @@ crossword-menu {
} }
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1rem;
} }
.form-group label { .form-group label {
display: block; display: block;
color: #1a1815; color: #1a1815;
font-weight: 600; font-weight: 600;
margin-bottom: 0.5rem; margin-bottom: 0.3rem;
font-size: 0.95rem; font-size: 0.95rem;
} }
.form-group select, .form-group select,
.form-group input[type="text"] { .form-group input[type="text"] {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.6rem;
border: 1px solid #c0bbb5; border: 1px solid #c0bbb5;
border-radius: 0.25rem; border-radius: 0.25rem;
font-size: 1rem; font-size: 1rem;
@ -548,15 +784,21 @@ crossword-menu {
.menu button { .menu button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.6rem;
margin-bottom: 0.7rem;
background: background:
repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px),
repeating-linear-gradient(-45deg, transparent, transparent 1px, rgba(0,0,0,.06) 1px, rgba(0,0,0,.06) 2px), repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px),
repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 5px),
repeating-linear-gradient(90deg, transparent, transparent 1px, rgba(0,0,0,.035) 1px, rgba(0,0,0,.035) 2px), repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(255,255,255,.03) 4px, rgba(255,255,255,.03) 6px),
repeating-linear-gradient(67deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 12% 18%, rgba(0,0,0,.02) 1px, transparent 1px),
repeating-linear-gradient(23deg, transparent, transparent 2px, rgba(0,0,0,.02) 2px, rgba(0,0,0,.02) 4px), repeating-radial-gradient(circle at 67% 77%, rgba(255,255,255,.04) 1px, transparent 1px),
radial-gradient(ellipse 800px 600px at 30% 40%, rgba(255,255,255,.4) 0%, transparent 40%), 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%); linear-gradient(180deg, #fffef9 0%, #f5f1ed 100%);
color: #1a1815; color: #1a1815;
border: 1px solid #c0bbb5; border: 1px solid #c0bbb5;
@ -624,12 +866,20 @@ crossword-menu {
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 500; font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: slideIn 0.3s ease-out; animation: slideIn 0.3s ease-out;
pointer-events: auto; pointer-events: auto;
gap: 0.75rem; gap: 0.75rem;
max-width: 350px; 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 { @keyframes slideIn {
@ -754,9 +1004,71 @@ crossword-menu {
flex-grow: 1; 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 { .close-game-btn {
background: none; background:
border: 1px solid rgba(245, 241, 237, 0.3); 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; color: #f5f1ed;
font-size: 1.5rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
@ -770,11 +1082,25 @@ crossword-menu {
flex-shrink: 0; flex-shrink: 0;
font-weight: 300; font-weight: 300;
line-height: 1; 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 { .close-game-btn:hover {
background: rgba(245, 241, 237, 0.1); background:
border-color: rgba(245, 241, 237, 0.6); 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 { .close-game-btn:active {
@ -783,8 +1109,20 @@ crossword-menu {
} }
.game-content { .game-content {
display: flex; display: block;
justify-content: center; overflow-y: auto;
overflow-x: auto;
max-height: calc(100vh - 200px); /* Leave space for header, clue area, and keyboard */
width: 100%;
padding: 1rem 0; /* Add some padding for breathing room */
text-align: center; /* Center inline-block children */
}
crossword-grid {
display: inline-block;
min-width: fit-content; /* Ensure grid takes its full needed width */
border-radius: 0px;
position: relative;
} }
/* Clue Area */ /* Clue Area */
@ -793,7 +1131,17 @@ crossword-menu {
top: 0; top: 0;
left: 0; left: 0;
right: 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); backdrop-filter: blur(8px);
border-bottom: 2px solid rgba(245, 241, 237, 0.2); border-bottom: 2px solid rgba(245, 241, 237, 0.2);
z-index: 1000; z-index: 1000;
@ -835,8 +1183,20 @@ crossword-menu {
} }
.clue-toggle { .clue-toggle {
background: none; background:
border: 1px solid rgba(245, 241, 237, 0.3); 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; color: #f5f1ed;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
@ -849,11 +1209,25 @@ crossword-menu {
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
padding: 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 { .clue-toggle:hover {
background: rgba(245, 241, 237, 0.1); background:
border-color: rgba(245, 241, 237, 0.6); 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 { .clue-toggle:active {
@ -876,7 +1250,7 @@ crossword-menu {
.clue-list-container { .clue-list-container {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
margin-top: 1rem; margin-top: 1rem;
} }
@ -916,6 +1290,11 @@ crossword-menu {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
} }
.clue-item.solved .clue-text {
text-decoration: line-through;
opacity: 0.6;
}
.clue-item .clue-number { .clue-item .clue-number {
flex-shrink: 0; flex-shrink: 0;
min-width: 2rem; min-width: 2rem;