Forum Discussion
Crossword in Rise
danielbenton Thank you for the idea and original code. I've been meaning to play around with coding with AI and this gave me the nudge to do it. The attached PDF shows all of the features and functionality in this new version. Just change the file extension from .txt to .html
- CDawes7 days agoCommunity Member
I'm not sure why the txt file won't stay attached.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crossword</title>
<style>
:root {
--gap: 2px;
--ok: #c8f7c5;
--bad: #f7c5c5;
--blank: #eee;
--brand: #0079bf;
--cell-size: 40px;
--highlight: #fff9b3;
--focus-ring: #0079bf;
}
body { font-family: Segoe UI, Arial, sans-serif; margin: 0; padding: 20px; }
.cw-wrap { display:flex; align-items:flex-start; gap:20px; }
.controls { display:flex; gap:10px; margin-bottom:15px; flex-wrap:wrap; }
.controls button { padding:10px 16px; background:var(--brand); color:#fff; border:none; border-radius:6px; cursor:pointer; font-size:14px; font-weight:500; min-height:44px; transition:background .2s; }
.controls button.secondary { background:#666; }
.controls button:hover { background:#006aa8; }
.controls button.secondary:hover { background:#555; }
.controls button:focus { outline:3px solid var(--focus-ring); outline-offset:2px; }
.grid { display:grid; gap:var(--gap); justify-content:center; grid-template-columns: repeat(var(--cols), var(--cell-size)); }
.cell { width:var(--cell-size); height:var(--cell-size); background:#fff; border:1px solid #c9c9c9; display:flex; align-items:center; justify-content:center; position:relative; }
.blank { background:var(--blank); border-color:#d9d9d9; }
.cell-number { position:absolute; top:2px; left:3px; font-size:10px; font-weight:bold; color:#333; pointer-events:none; }
.cell input { font-size:calc(var(--cell-size)*0.6); text-transform:uppercase; text-align:center; width:100%; height:100%; border:none; outline:none; background:transparent; -moz-appearance:textfield; }
.cell input:focus { outline:3px solid var(--focus-ring); outline-offset:-3px; z-index:10; }
.ok::after { content:"✔"; color:green; font-size:.8em; position:absolute; bottom:2px; right:2px; }
.bad::after { content:"✖"; color:red; font-size:.8em; position:absolute; bottom:2px; right:2px; }
.highlight { background-color:var(--highlight); }
.highlight-active { background-color:#ffd700; }
.clues { max-width:380px; }
.clue-section { margin-bottom:20px; }
.clue-heading { padding:8px; background:var(--brand); color:#fff; border-radius:6px; font-size:1.1rem; margin-bottom:8px; font-weight:bold; }
.clue-section p { padding:8px 10px; margin:6px 0; cursor:pointer; border-radius:4px; transition:background .2s; min-height:44px; display:flex; align-items:center; }
.clue-section p.active-clue { background:var(--highlight); border-left:4px solid var(--brand); }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }.modal { display:none; position:fixed; z-index:1000; inset:0; background:rgba(0,0,0,.5); }
.modal.show { display:flex; align-items:center; justify-content:center; }
.modal-content { background:#fff; padding:30px; border-radius:12px; max-width:420px; text-align:center; box-shadow:0 4px 20px rgba(0,0,0,.3); }MeDia (max-width:800px) {
.cw-wrap { flex-direction:column; align-items:center; }
.board, .clues { width:100%; max-width:95vw; }
.controls button { flex:1 1 calc(50% - 5px); min-width:120px; }
}body.high-contrast { --ok:#90ee90; --bad:#ffb3b3; --blank:#ddd; --highlight:#ffff00; --focus-ring:#000; }
body.high-contrast .cell { border:2px solid #000; }
body.high-contrast .cell input { color:#000; font-weight:bold; }
</style>
</head>
<body>
<div class="cw-wrap">
<div class="board">
<div class="controls">
<button id="checkBtn" aria-label="Check all answers">Check Answers</button>
<button id="clearWordBtn" class="secondary" aria-label="Clear current word">Clear Word</button>
<button id="clearAllBtn" class="secondary" aria-label="Clear all answers">Clear All</button>
<button id="revealLetterBtn" aria-label="Reveal current letter">Reveal Letter</button>
<button id="revealWordBtn" aria-label="Reveal current word">Reveal Word</button>
<button id="contrastBtn" class="secondary" aria-label="Toggle high contrast mode">High Contrast</button>
</div>
<div id="grid" class="grid" aria-label="Crossword grid" role="grid"></div>
</div><div class="clues">
<div class="clue-section">
<div class="clue-heading">Across</div>
<div id="acrossClues"></div>
</div>
<div class="clue-section">
<div class="clue-heading">Down</div>
<div id="downClues"></div>
</div>
</div>
</div><div id="completionModal" class="modal" role="dialog" aria-labelledby="modalTitle" aria-modal="true">
<div class="modal-content">
<h2 id="modalTitle">🎉 Congratulations!</h2>
<p>You've completed the crossword correctly!</p>
<button onclick="closeModal()">Close</button>
</div>
</div><div id="announcements" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<script>
/* ===== Editable Word List ===== */
const wordList = [
{word:"APPLE", clue:"A type of fruit that is usually red or green"},
{word:"TRIGGER", clue:"What action occurs? When does it happen?"},
{word:"MATTER", clue:"Subject ________ Expert"},
{word:"LEARNERS", clue:"Use materials"},
{word:"SCHEDULE", clue:"What we need to follow"},
{word:"NOVEMBER", clue:"Election month"},
{word:"TUESDAY", clue:"Election day"},
{word:"RECHARGE", clue:"Weekends"},
{word:"STORYLINE", clue:"Can be extended with Javascript"},
{word:"ONLINE", clue:"__________ Learning"}
];/* ===== Generator ===== */
function generateCrossword(list){
const words = list.map(w=>({...w, word:w.word.toUpperCase()})).sort((a,b)=>b.word.length-a.word.length);
const placed = [];
const grid = {};
const first = words[0]; const sr=10, sc=10;
placed.push({word:first.word, clue:first.clue, r:sr, c:sc, dir:"across"});
for(let i=0;i<first.word.length;i++){ grid[`${sr},${sc+i}`] = {letter:first.word[i], words:[0]}; }for(let w=1; w<words.length; w++){
const word = words[w].word, clue = words[w].clue;
let best=null, maxI=0;
for(let p=0;p<placed.length;p++){
const pw=placed[p];
for(const dir of ["across","down"]){
if(dir===pw.dir) continue;
for(let i=0;i<word.length;i++){
for(let j=0;j<pw.word.length;j++){
if(word[i]!==pw.word[j]) continue;
let r,c;
if(dir==="across"){ r = pw.dir==="across"? pw.r : pw.r + j; c = pw.dir==="across"? pw.c + j - i : pw.c - i; }
else { r = pw.dir==="across"? pw.r - i : pw.r + j - i; c = pw.dir==="across"? pw.c + j : pw.c; }
if(isValid(word,r,c,dir,grid)){
const inter = countInter(word,r,c,dir,grid);
if(inter>maxI){ maxI=inter; best={r,c,dir}; }
}
}
}
}
}
if(best){
placed.push({word, clue, r:best.r, c:best.c, dir:best.dir});
for(let i=0;i<word.length;i++){
const rr = best.dir==="across" ? best.r : best.r + i;
const cc = best.dir==="across" ? best.c + i : best.c;
const key = `${rr},${cc}`;
if(!grid[key]) grid[key] = {letter:word[i], words:[]};
grid[key].words.push(placed.length-1);
}
}
}// Normalize to start at (1,1)
let minR=Infinity, minC=Infinity;
for(const key in grid){ const [r,c]=key.split(',').map(Number); minR=Math.min(minR,r); minC=Math.min(minC,c); }
placed.forEach(w=>{ w.r = w.r - minR + 1; w.c = w.c - minC + 1; });// Assign clue numbers by start cell order
const starts = {};
placed.forEach((w,idx)=>{ const k=`${w.r},${w.c}`; if(!starts[k]) starts[k]=[]; starts[k].push({idx,dir:w.dir}); });
const startKeys = Object.keys(starts).sort((a,b)=>{
const [r1,c1]=a.split(',').map(Number); const [r2,c2]=b.split(',').map(Number);
return r1===r2 ? c1-c2 : r1-r2;
});
let num=1;
startKeys.forEach(k=>{ starts[k].forEach(({idx})=> placed[idx].num=num); num++; });return placed;
}
function isValid(word,r,c,dir,g){
for(let i=0;i<word.length;i++){
const rr = dir==="across"? r : r + i;
const cc = dir==="across"? c + i : c;
const key = `${rr},${cc}`;
if(g[key] && g[key].letter !== word[i]) return false;
if(!g[key]){
const checks = dir==="across" ? [[rr-1,cc],[rr+1,cc]] : [[rr,cc-1],[rr,cc+1]];
for(const [cr,cc2] of checks){ if(g[`${cr},${cc2}`]) return false; }
}
}
const before = dir==="across"? `${r},${c-1}` : `${r-1},${c}`;
const after = dir==="across"? `${r},${c+word.length}` : `${r+word.length},${c}`;
if(g[before] || g[after]) return false;
return true;
}
function countInter(word,r,c,dir,g){
let n=0;
for(let i=0;i<word.length;i++){
const rr = dir==="across"? r : r + i;
const cc = dir==="across"? c + i : c;
const key = `${rr},${cc}`;
if(g[key] && g[key].letter===word[i]) n++;
}
return n;
}/* ===== Render & Layout ===== */
const words = generateCrossword(wordList).sort((a,b)=>a.num-b.num);
let rows=0, cols=0;
words.forEach(({r,c,dir,word})=>{
const endR = dir==="across" ? r : r + word.length - 1;
const endC = dir==="across" ? c + word.length - 1 : c;
rows = Math.max(rows, endR);
cols = Math.max(cols, endC);
});
const BLANK = "#";
const SOL = Array.from({length: rows*cols}, ()=>BLANK);
const idx = (r,c)=>(r-1)*cols+(c-1);
function place(r,c,dir,word){
word = word.toUpperCase();
for(let i=0;i<word.length;i++){
const rr = dir==="across" ? r : r + i;
const cc = dir==="across" ? c + i : c;
SOL[idx(rr,cc)] = word[i];
}
}
words.forEach(w=>place(w.r,w.c,w.dir,w.word));// Auto-bumped storage version based on layout + list
function computeVersion(){
try{
const data = JSON.stringify({rows,cols,sol:SOL.join(''), list:wordList.map(w=>w.word).join('|')});
let h=0; for(let i=0;i<data.length;i++){ h=((h<<5)-h)+data.charCodeAt(i); h|=0; }
return 'v'+Math.abs(h);
}catch(e){ return 'v0'; }
}
const APP_VERSION = computeVersion();
const STORAGE_KEYS = { progress:`crossword-progress-${APP_VERSION}`, contrast:'highContrast' };const gridEl = document.getElementById('grid');
document.documentElement.style.setProperty('--cols', cols);
if(gridEl) gridEl.style.setProperty('--cols', cols);let currentDirection = "across";
let currentWord = null;
let autoCheckEnabled = false;
/* ----- Responsive sizing (mobile-friendly) ----- */
function updateCellSize() {
// Leave some room for controls/clues
const verticalPadding = 260; // header/controls/modal margin
const horizontalPadding = 60; // padding/margins
const maxCell = 56; // cap desktop
const minCell = 24; // cap mobile
const availH = Math.max(200, (window.innerHeight || document.documentElement.clientHeight) - verticalPadding);
const availW = Math.max(200, (window.innerWidth || document.documentElement.clientWidth) - horizontalPadding);
const hSize = Math.floor(availH / rows);
const wSize = Math.floor(availW / cols);
const size = Math.max(minCell, Math.min(maxCell, Math.min(hSize, wSize)));
document.documentElement.style.setProperty('--cell-size', size + 'px');
}
updateCellSize();
window.addEventListener('resize', updateCellSize, {passive:true});
window.addEventListener('orientationchange', updateCellSize);/* ===== Clues ===== */
function getCellNumbers(){
const nums={};
words.forEach(w=>{ const k=`${w.r},${w.c}`; if(!nums[k] || nums[k] > w.num) nums[k]=w.num; });
return nums;
}
function renderClues(){
const acrossEl = document.getElementById('acrossClues');
const downEl = document.getElementById('downClues');
acrossEl.innerHTML = ''; downEl.innerHTML = '';
const across = words.filter(w=>w.dir==='across').sort((a,b)=>a.num-b.num);
const down = words.filter(w=>w.dir==='down').sort((a,b)=>a.num-b.num);
for(const w of across){
const p=document.createElement('p'); p.id=`clue-${w.dir}-${w.num}`;
p.innerHTML = `<b>${w.num}</b> – ${w.clue}`; p.tabIndex=0;
p.addEventListener('click',()=>focusWord(w));
p.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); focusWord(w); } });
acrossEl.appendChild(p);
}
for(const w of down){
const p=document.createElement('p'); p.id=`clue-${w.dir}-${w.num}`;
p.innerHTML = `<b>${w.num}</b> – ${w.clue}`; p.tabIndex=0;
p.addEventListener('click',()=>focusWord(w));
p.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); focusWord(w); } });
downEl.appendChild(p);
}
}/* ===== Grid Render ===== */
function render(){
gridEl.innerHTML='';
const cellNumbers = getCellNumbers();
for(let r=1;r<=rows;r++){
for(let c=1;c<=cols;c++){
const val = SOL[idx(r,c)];
const cell = document.createElement('div');
cell.className='cell'; cell.dataset.row=r; cell.dataset.col=c;
if(val===BLANK){
cell.classList.add('blank');
}else{
const key=`${r},${c}`;
if(cellNumbers[key]){
const span=document.createElement('span'); span.className='cell-number'; span.textContent=cellNumbers[key];
cell.appendChild(span);
}
const input=document.createElement('input'); input.type='text'; input.maxLength=1;
input.setAttribute('aria-label',`Row ${r}, Column ${c}`);
input.addEventListener('input', e=>handleInput(e,cell,r,c));
input.addEventListener('keydown', e=>handleKeyDown(e,cell,r,c));
input.addEventListener('focus', ()=>highlightWord(input));
cell.dataset.answer = val;
cell.appendChild(input);
}
gridEl.appendChild(cell);
}
}
loadProgress();
}/* ===== Helpers ===== */
function getWordCells(w){
const arr=[];
for(let i=0;i<w.word.length;i++){
const r = w.dir==="across" ? w.r : w.r + i;
const c = w.dir==="across" ? w.c + i : w.c;
arr.push({row:r,col:c});
}
return arr;
}
function getFirstEmptyInputInWord(w){
for(const pos of getWordCells(w)){
const el = gridEl.children[idx(pos.row,pos.col)];
const i = el && el.querySelector('input');
if(i && !i.value) return i;
}
return null;
}
function getStrictOrder(){ // Across then Down by number
const across = words.filter(w=>w.dir==='across').sort((a,b)=>a.num-b.num);
const down = words.filter(w=>w.dir==='down').sort((a,b)=>a.num-b.num);
return [...across, ...down];
}/* ===== Highlight & Focus ===== */
function highlightWord(input){
document.querySelectorAll('.cell').forEach(c=>c.classList.remove('highlight','highlight-active'));
document.querySelectorAll('.clue-section p').forEach(p=>p.classList.remove('active-clue'));
if(!input) return;
const cell = input.closest('.cell'); if(!cell) return;
const row = +cell.dataset.row, col = +cell.dataset.col;
const w = words.find(w=> (w.dir===currentDirection &&
((w.dir==='across' && w.r===row && col>=w.c && col<w.c+w.word.length) ||
(w.dir==='down' && w.c===col && row>=w.r && row<w.r+w.word.length)))) ||
words.find(w=> ((w.dir==='across' && w.r===row && col>=w.c && col<w.c+w.word.length) ||
(w.dir==='down' && w.c===col && row>=w.r && row<w.r+w.word.length)));
if(w){
currentWord=w; currentDirection=w.dir;
for(const pos of getWordCells(w)){
const el = gridEl.children[idx(pos.row,pos.col)];
el.classList.add('highlight');
if(pos.row===row && pos.col===col) el.classList.add('highlight-active');
}
const clueEl = document.getElementById(`clue-${w.dir}-${w.num}`);
if(clueEl) clueEl.classList.add('active-clue');
announce(`${w.dir==='across'?'Across':'Down'} ${w.num}: ${w.clue}`);
}
}
function focusWord(w){
currentWord=w; currentDirection=w.dir;
const first = gridEl.children[idx(w.r,w.c)]; const input = first && first.querySelector('input');
if(input){ input.focus(); highlightWord(input); }
}/* ===== Input & Navigation (2ms autoskip, intelligent next-word, stop at end) ===== */
let saveTimer=null;
function throttledSaveProgress(){ clearTimeout(saveTimer); saveTimer=setTimeout(()=>{ saveProgress(); saveTimer=null; },500); }function handleInput(e,cell,r,c){
const t=e.target.value.toUpperCase(); e.target.value=t;
if(autoCheckEnabled) paint(cell,t);
if(t){
const next = findNextEmptyCellInCurrentWord(r,c);
if(next){ setTimeout(()=>next.focus(),2); }
else{
// Move to first empty cell of the next word in strict Across→Down order; stop at end
if(currentWord){
const order = getStrictOrder();
const curIdx = order.indexOf(currentWord);
for(let k=curIdx+1; k<order.length; k++){
const i = getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}
}
}
throttledSaveProgress();
}
function findNextEmptyCellInCurrentWord(r,c){
if(!currentWord) return null;
const cells = getWordCells(currentWord);
const i = cells.findIndex(p=>p.row===r && p.col===c);
if(i>=0){
for(let k=i+1;k<cells.length;k++){
const el = gridEl.children[idx(cells[k].row,cells[k].col)];
const inp = el && el.querySelector('input');
if(inp && !inp.value) return inp;
}
}
return null;
}function handleKeyDown(e,cell,r,c){
const inputs = Array.from(document.querySelectorAll('.cell input'));
const currentIndex = inputs.indexOf(e.target);
let nextIndex=null;
switch(e.key){
case "ArrowRight": nextIndex=currentIndex+1; currentDirection="across"; break;
case "ArrowLeft": nextIndex=currentIndex-1; currentDirection="across"; break;
case "ArrowDown": nextIndex=currentIndex+cols; currentDirection="down"; break;
case "ArrowUp": nextIndex=currentIndex-cols; currentDirection="down"; break;
case "Tab": return;
case " ":
e.preventDefault(); toggleDirection(); highlightWord(e.target); return;
case "Backspace":
if(!e.target.value && currentWord){
e.preventDefault();
const cells=getWordCells(currentWord);
const i=cells.findIndex(p=>p.row===r && p.col===c);
if(i>0){
const prev = gridEl.children[idx(cells[i-1].row,cells[i-1].col)].querySelector('input');
if(prev){ prev.value=''; prev.focus(); if(autoCheckEnabled) paint(prev.closest('.cell'),''); throttledSaveProgress(); }
}
}else{ setTimeout(()=>throttledSaveProgress(),0); }
return;
case "Home":
e.preventDefault(); if(currentWord){ const first=gridEl.children[idx(currentWord.r,currentWord.c)].querySelector('input'); if(first) first.focus(); } return;
case "End":
e.preventDefault(); if(currentWord){ const lr=currentWord.dir==="across"? currentWord.r : currentWord.r+currentWord.word.length-1; const lc=currentWord.dir==="across"? currentWord.c+currentWord.word.length-1 : currentWord.c; const last=gridEl.children[idx(lr,lc)].querySelector('input'); if(last) last.focus(); } return;
}
if(nextIndex!==null && inputs[nextIndex]){ e.preventDefault(); inputs[nextIndex].focus(); }
}
function toggleDirection(){ currentDirection = currentDirection==="across" ? "down" : "across"; announce(`Direction: ${currentDirection}`); }/* ===== Checking & Modal ===== */
function paint(cell,val){
cell.classList.remove('ok','bad');
if(!val) return;
if(val===cell.dataset.answer){ cell.classList.add('ok'); announce('Correct'); }
else{ cell.classList.add('bad'); announce('Incorrect'); }
}
function checkAllAnswers(){
autoCheckEnabled = true;
const cells = gridEl.querySelectorAll('.cell:not(.blank)');
let allCorrect = true;
cells.forEach(cell=>{
const ans = cell.dataset.answer;
const input = cell.querySelector('input');
const val = (input && input.value ? input.value.toUpperCase() : "");
cell.classList.remove('ok','bad');
if(val === ans){
cell.classList.add('ok');
} else {
cell.classList.add('bad');
allCorrect = false;
}
});
if(allCorrect) showCompletionModal(); else announce("Some answers are incorrect. Incorrect cells are marked.");
}
function showCompletionModal(){ document.getElementById('completionModal').classList.add('show'); }
function closeModal(){ document.getElementById('completionModal').classList.remove('show'); }/* ===== Clear / Reveal ===== */
function clearCurrentWord(){
if(!currentWord){ announce("No word selected"); return; }
for(const pos of getWordCells(currentWord)){
const cell = gridEl.children[idx(pos.row,pos.col)];
const i = cell.querySelector('input');
if(i){ i.value=''; cell.classList.remove('ok','bad'); }
}
announce(`Cleared ${currentWord.dir} ${currentWord.num}`);
throttledSaveProgress();
}
function clearAll(){
// Clear everything without a confirm to avoid mobile popup issues
const inputs = gridEl.querySelectorAll('.cell input');
inputs.forEach(i=>{ i.value=''; i.closest('.cell').classList.remove('ok','bad'); });
announce("All answers cleared");
try{ localStorage.removeItem(STORAGE_KEYS.progress); }catch(e){}
}
function revealLetter(){
let target=document.activeElement;
if(!target || target.tagName!=='INPUT'){
const active=document.querySelector('.cell.highlight-active input');
if(active) target=active;
}
if(target){
const cell=target.closest('.cell');
if(cell && cell.dataset.answer){
target.value=cell.dataset.answer;
cell.classList.remove('bad'); cell.classList.add('ok');
announce(`Revealed letter: ${cell.dataset.answer}`);
throttledSaveProgress();
const r=+cell.dataset.row, c=+cell.dataset.col;
const next=findNextEmptyCellInCurrentWord(r,c);
if(next){ setTimeout(()=>next.focus(),2); }
else if(currentWord){
const order=getStrictOrder(); const curIdx=order.indexOf(currentWord);
for(let k=curIdx+1;k<order.length;k++){
const i=getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}
}
} else { announce("Focus on a cell first"); }
}
function revealWord(){
if(!currentWord){ announce("No word selected"); return; }
for(const pos of getWordCells(currentWord)){
const cell = gridEl.children[idx(pos.row,pos.col)];
const i = cell.querySelector('input');
if(i){ i.value=cell.dataset.answer; cell.classList.remove('bad'); cell.classList.add('ok'); }
}
announce(`Revealed ${currentWord.dir} ${currentWord.num}: ${currentWord.word}`);
throttledSaveProgress();
// Move to first empty of next word (strict order); stop at end
const order=getStrictOrder(); const curIdx=order.indexOf(currentWord);
for(let k=curIdx+1;k<order.length;k++){
const i=getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}/* ===== Persistence ===== */
function saveProgress(){
try{
const progress={};
const inputs=gridEl.querySelectorAll('.cell input');
inputs.forEach((i,k)=>{ if(i.value) progress[k]=i.value; });
localStorage.setItem(STORAGE_KEYS.progress, JSON.stringify(progress));
}catch(e){ console.log('Could not save progress', e); }
}
function loadProgress(){
try{
const data = localStorage.getItem(STORAGE_KEYS.progress);
if(data){
const progress=JSON.parse(data);
const inputs=gridEl.querySelectorAll('.cell input');
inputs.forEach((i,k)=>{ if(progress && progress[k]) i.value=progress[k]; });
}
if(localStorage.getItem('highContrast')==='true') document.body.classList.add('high-contrast');
}catch(e){ console.log('Could not load progress', e); }
}/* ===== High Contrast ===== */
function toggleHighContrast(){
document.body.classList.toggle('high-contrast');
try{ localStorage.setItem('highContrast', document.body.classList.contains('high-contrast') ? 'true' : 'false'); }catch(e){}
}/* ===== Announce ===== */
function announce(m){ document.getElementById('announcements').textContent = m; }/* ===== Wire up ===== */
document.getElementById('checkBtn').onclick = checkAllAnswers;
document.getElementById('clearWordBtn').onclick = clearCurrentWord;
document.getElementById('clearAllBtn').onclick = clearAll;
document.getElementById('revealLetterBtn').onclick = revealLetter;
document.getElementById('revealWordBtn').onclick = revealWord;
document.getElementById('contrastBtn').onclick = toggleHighContrast;
window.onclick = e => { const m=document.getElementById('completionModal'); if(e.target===m) closeModal(); };/* ===== Init ===== */
renderClues();
render();
announce('Crossword ready — '+APP_VERSION);
</script>
</body>
</html>
Related Content
- 4 months ago
- 2 months ago
- 10 months ago
- 4 months ago