{"id":438,"date":"2026-06-04T08:36:46","date_gmt":"2026-06-04T08:36:46","guid":{"rendered":"https:\/\/seanholden.xyz\/?page_id=438"},"modified":"2026-06-04T09:48:09","modified_gmt":"2026-06-04T09:48:09","slug":"munchie-words","status":"publish","type":"page","link":"https:\/\/seanholden.xyz\/?page_id=438","title":{"rendered":"Munchie Words"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>Word Wizard Reading Game<\/title>\n  <style>\n    :root {\n      --bg1: #ffe0f0;\n      --bg2: #d7f7ff;\n      --bg3: #fff3b0;\n      --ink: #243044;\n      --card: rgba(255, 255, 255, 0.92);\n      --green: #7bed9f;\n      --yellow: #ffd166;\n      --blue: #74b9ff;\n      --pink: #ff8fab;\n      --purple: #b197fc;\n      --shadow: 0 18px 45px rgba(34, 48, 68, 0.18);\n    }\n\n    * {\n      box-sizing: border-box;\n    }\n\n    body {\n      margin: 0;\n      min-height: 80vh;\n      font-family: \"Comic Sans MS\", \"Trebuchet MS\", Arial, sans-serif;\n      color: var(--ink);\n      background: linear-gradient(135deg, var(--bg1), var(--bg2), var(--bg3));\n      background-size: 300% 300%;\n      animation: floatBackground 14s ease infinite;\n      overflow-x: hidden;\n    }\n\n    @keyframes floatBackground {\n      0% { background-position: 0% 50%; }\n      50% { background-position: 100% 50%; }\n      100% { background-position: 0% 50%; }\n    }\n\n    .app {\n      width: min(980px, 94vw);\n      margin: 0 auto;\n      padding: 24px 0 36px;\n    }\n\n    header {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      gap: 12px;\n      margin-bottom: 18px;\n      flex-wrap: wrap;\n    }\n\n    h1 {\n      font-size: clamp(32px, 6vw, 56px);\n      margin: 0;\n      letter-spacing: -1px;\n      text-shadow: 2px 2px 0 rgba(255,255,255,0.8);\n    }\n\n    .top-buttons {\n      display: flex;\n      gap: 10px;\n      flex-wrap: wrap;\n    }\n\n    .pill {\n      border: none;\n      border-radius: 999px;\n      padding: 12px 16px;\n      font-size: 16px;\n      font-weight: 800;\n      cursor: pointer;\n      background: white;\n      box-shadow: 0 8px 20px rgba(0,0,0,0.12);\n      color: var(--ink);\n    }\n\n    .pill:hover {\n      transform: translateY(-1px);\n    }\n\n    #voiceSelect {\n      border: none;\n      border-radius: 999px;\n      padding: 12px 14px;\n      font-size: 15px;\n      font-weight: 700;\n      background: white;\n      box-shadow: 0 8px 20px rgba(0,0,0,0.12);\n      color: var(--ink);\n      max-width: 260px;\n    }\n\n    .score-row {\n      display: grid;\n      grid-template-columns: repeat(4, 1fr);\n      gap: 12px;\n      margin-bottom: 18px;\n    }\n\n    .score-box {\n      background: rgba(255,255,255,0.82);\n      border: 3px solid rgba(255,255,255,0.9);\n      border-radius: 22px;\n      padding: 14px;\n      text-align: center;\n      box-shadow: 0 8px 18px rgba(0,0,0,0.09);\n    }\n\n    .score-box .label {\n      font-size: 14px;\n      opacity: 0.75;\n      font-weight: 800;\n    }\n\n    .score-box .value {\n      font-size: 26px;\n      font-weight: 900;\n      margin-top: 4px;\n    }\n\n    .card {\n      background: var(--card);\n      border: 4px solid rgba(255,255,255,0.9);\n      border-radius: 36px;\n      box-shadow: var(--shadow);\n      padding: clamp(22px, 5vw, 44px);\n      text-align: center;\n      position: relative;\n      overflow: hidden;\n    }\n\n    .mascot {\n      font-size: clamp(56px, 12vw, 110px);\n      line-height: 1;\n      margin-bottom: 8px;\n      filter: drop-shadow(0 8px 8px rgba(0,0,0,0.15));\n      user-select: none;\n    }\n\n    .instruction {\n      font-size: clamp(18px, 3.4vw, 28px);\n      font-weight: 900;\n      margin: 6px 0 16px;\n    }\n\n    .word {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      min-height: 140px;\n      min-width: min(720px, 88vw);\n      padding: 26px 36px;\n      margin: 12px auto 20px;\n      border-radius: 36px;\n      background: white;\n      border: 6px solid #fff0a8;\n      font-size: clamp(60px, 14vw, 132px);\n      font-weight: 900;\n      letter-spacing: 2px;\n      box-shadow: inset 0 -8px 0 rgba(0,0,0,0.05), 0 12px 22px rgba(0,0,0,0.12);\n      cursor: pointer;\n      user-select: none;\n      transition: transform 0.15s ease;\n    }\n\n    .word:hover {\n      transform: scale(1.02);\n    }\n\n    .word.pop {\n      animation: pop 0.38s ease;\n    }\n\n    @keyframes pop {\n      0% { transform: scale(0.8) rotate(-1deg); opacity: 0; }\n      70% { transform: scale(1.05) rotate(1deg); opacity: 1; }\n      100% { transform: scale(1) rotate(0deg); }\n    }\n\n    .button-row {\n      display: flex;\n      justify-content: center;\n      gap: 16px;\n      flex-wrap: wrap;\n      margin-top: 16px;\n    }\n\n    .big-button {\n      border: none;\n      border-radius: 28px;\n      padding: 22px 28px;\n      font-size: clamp(20px, 4vw, 30px);\n      font-weight: 900;\n      cursor: pointer;\n      color: var(--ink);\n      box-shadow: 0 12px 0 rgba(0,0,0,0.12), 0 18px 24px rgba(0,0,0,0.12);\n      min-width: 210px;\n      transition: transform 0.08s ease, box-shadow 0.08s ease;\n    }\n\n    .big-button:active {\n      transform: translateY(8px);\n      box-shadow: 0 4px 0 rgba(0,0,0,0.12), 0 8px 12px rgba(0,0,0,0.1);\n    }\n\n    .known {\n      background: var(--green);\n    }\n\n    .learning {\n      background: var(--yellow);\n    }\n\n    .listen {\n      background: var(--blue);\n    }\n\n    .message {\n      min-height: 42px;\n      margin-top: 18px;\n      font-size: clamp(18px, 3.2vw, 26px);\n      font-weight: 900;\n    }\n\n    .progress-wrap {\n      margin: 22px auto 0;\n      max-width: 620px;\n      text-align: left;\n    }\n\n    .progress-label {\n      display: flex;\n      justify-content: space-between;\n      font-weight: 900;\n      margin-bottom: 6px;\n    }\n\n    .progress {\n      height: 24px;\n      background: rgba(255,255,255,0.8);\n      border-radius: 999px;\n      overflow: hidden;\n      border: 3px solid white;\n    }\n\n    .progress-fill {\n      height: 100%;\n      width: 0%;\n      background: linear-gradient(90deg, #7bed9f, #74b9ff, #b197fc);\n      border-radius: 999px;\n      transition: width 0.4s ease;\n    }\n\n    .badges {\n      display: flex;\n      flex-wrap: wrap;\n      justify-content: center;\n      gap: 10px;\n      margin-top: 18px;\n    }\n\n    .badge {\n      background: white;\n      border-radius: 999px;\n      padding: 10px 14px;\n      font-size: 16px;\n      font-weight: 900;\n      box-shadow: 0 8px 16px rgba(0,0,0,0.08);\n      opacity: 0.38;\n      filter: grayscale(1);\n    }\n\n    .badge.unlocked {\n      opacity: 1;\n      filter: none;\n      border: 3px solid #ffe66d;\n    }\n\n    .panel {\n      display: none;\n      margin-top: 18px;\n      background: rgba(255,255,255,0.9);\n      border-radius: 26px;\n      padding: 20px;\n      box-shadow: var(--shadow);\n    }\n\n    .panel.open {\n      display: block;\n    }\n\n    .panel h2 {\n      margin-top: 0;\n    }\n\n    table {\n      width: 100%;\n      border-collapse: collapse;\n      background: white;\n      border-radius: 18px;\n      overflow: hidden;\n      font-family: Arial, sans-serif;\n    }\n\n    th, td {\n      padding: 10px;\n      border-bottom: 1px solid #eee;\n      text-align: left;\n      font-size: 14px;\n    }\n\n    th {\n      background: #f3f6ff;\n    }\n\n    textarea {\n      width: 100%;\n      min-height: 120px;\n      border-radius: 18px;\n      border: 2px solid #d7dff5;\n      padding: 14px;\n      font-size: 16px;\n      font-family: Arial, sans-serif;\n    }\n\n    .small-note {\n      font-family: Arial, sans-serif;\n      opacity: 0.78;\n      font-size: 14px;\n      line-height: 1.45;\n    }\n\n    .confetti {\n      position: fixed;\n      top: -20px;\n      width: 12px;\n      height: 18px;\n      opacity: 0.9;\n      pointer-events: none;\n      animation: fall 1.6s linear forwards;\n      z-index: 1000;\n    }\n\n    @keyframes fall {\n      to {\n        transform: translateY(110vh) rotate(720deg);\n        opacity: 0.4;\n      }\n    }\n\n    @media (max-width: 700px) {\n      .score-row {\n        grid-template-columns: repeat(2, 1fr);\n      }\n\n      .big-button {\n        width: 100%;\n      }\n\n      .word {\n        min-width: 100%;\n      }\n    }\n  <\/style>\n<\/head>\n<body>\n  <div class=\"app\">\n    <header>\n      <h1>\ud83c\udf08 Word Wizard<\/h1>\n      <div class=\"top-buttons\">\n        <button class=\"pill\" onclick=\"togglePanel('parentPanel')\">\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67 Parent<\/button>\n        <button class=\"pill\" onclick=\"togglePanel('wordPanel')\">\u2795 Words<\/button>\n        <button class=\"pill\" onclick=\"resetProgress()\">\ud83d\udd04 Reset<\/button>\n\ud83d\udde3\ufe0fvoice <select id=\"voiceSelect\"><\/select>      \n<\/div>\n    <\/header>\n\n    <section class=\"score-row\">\n      <div class=\"score-box\">\n        <div class=\"label\">Points<\/div>\n        <div class=\"value\" id=\"score\">0<\/div>\n      <\/div>\n      <div class=\"score-box\">\n        <div class=\"label\">Level<\/div>\n        <div class=\"value\" id=\"level\">1<\/div>\n      <\/div>\n      <div class=\"score-box\">\n        <div class=\"label\">Streak<\/div>\n        <div class=\"value\" id=\"streak\">0<\/div>\n      <\/div>\n      <div class=\"score-box\">\n        <div class=\"label\">Words Tried<\/div>\n        <div class=\"value\" id=\"tried\">0<\/div>\n      <\/div>\n    <\/section>\n\n    <main class=\"card\">\n      <div class=\"mascot\" id=\"mascot\">\ud83e\udd89<\/div>\n      <div class=\"instruction\" id=\"instruction\">Can you read this word?<\/div>\n      <div class=\"word\" id=\"word\" onclick=\"speakCurrentWord()\">cat<\/div>\n\n      <div class=\"button-row\">\n        <button class=\"big-button listen\" onclick=\"speakCurrentWord()\">\ud83d\udd0a Read it<\/button>\n        <button class=\"big-button known\" onclick=\"markWord(true)\">\u2705 I know it<\/button>\n        <button class=\"big-button learning\" onclick=\"markWord(false)\">\ud83e\udd14 Still learning<\/button>\n      <\/div>\n\n      <div class=\"message\" id=\"message\"><\/div>\n\n      <div class=\"progress-wrap\">\n        <div class=\"progress-label\">\n          <span id=\"levelName\">Level 1 Reader<\/span>\n          <span id=\"nextLevel\">0 \/ 50<\/span>\n        <\/div>\n        <div class=\"progress\">\n          <div class=\"progress-fill\" id=\"progressFill\"><\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"badges\" id=\"badges\"><\/div>\n    <\/main>\n\n    <section class=\"panel\" id=\"parentPanel\">\n      <h2>Parent Stats<\/h2>\n      <p class=\"small-note\">\n        Unknown words are shown more often. Known words still come back, but less often.\n        Progress is saved only in this browser using local storage.\n      <\/p>\n      <div id=\"statsTable\"><\/div>\n    <\/section>\n\n    <section class=\"panel\" id=\"wordPanel\">\n      <h2>Add Your Own Words<\/h2>\n      <p class=\"small-note\">\n        Add one word per line. You can use simple words, school spelling words, sight words, or tricky words.\n      <\/p>\n      <textarea id=\"newWords\" placeholder=\"apple&#10;happy&#10;because\"><\/textarea>\n      <div class=\"button-row\">\n        <button class=\"big-button listen\" onclick=\"addWords()\">\u2795 Add Words<\/button>\n      <\/div>\n    <\/section>\n  <\/div>\n\n  <script>\n    const DEFAULT_WORDS = [\n      { text: \"cat\", difficulty: 1 },\n      { text: \"dog\", difficulty: 1 },\n      { text: \"sun\", difficulty: 1 },\n      { text: \"mum\", difficulty: 1 },\n      { text: \"dad\", difficulty: 1 },\n      { text: \"red\", difficulty: 1 },\n      { text: \"big\", difficulty: 1 },\n      { text: \"run\", difficulty: 1 },\n      { text: \"jump\", difficulty: 2 },\n      { text: \"play\", difficulty: 2 },\n      { text: \"look\", difficulty: 2 },\n      { text: \"book\", difficulty: 2 },\n      { text: \"fish\", difficulty: 2 },\n      { text: \"shop\", difficulty: 2 },\n      { text: \"green\", difficulty: 3 },\n      { text: \"little\", difficulty: 3 },\n      { text: \"yellow\", difficulty: 3 },\n      { text: \"happy\", difficulty: 3 },\n      { text: \"friend\", difficulty: 4 },\n      { text: \"school\", difficulty: 4 },\n      { text: \"people\", difficulty: 4 },\n      { text: \"because\", difficulty: 5 },\n      { text: \"beautiful\", difficulty: 5 },\n      { text: \"different\", difficulty: 5 }\n    ];\n\n    const LEVELS = [\n      { name: \"Level 1 Reader\", min: 0, next: 50 },\n      { name: \"Level 2 Word Explorer\", min: 50, next: 120 },\n      { name: \"Level 3 Sentence Starter\", min: 120, next: 220 },\n      { name: \"Level 4 Book Adventurer\", min: 220, next: 360 },\n      { name: \"Level 5 Reading Wizard\", min: 360, next: 520 },\n      { name: \"Level 6 Story Champion\", min: 520, next: 750 }\n    ];\n\n    const BADGES = [\n      { id: \"first\", label: \"\ud83c\udf1f First Try\", points: 1 },\n      { id: \"ten\", label: \"\ud83d\udc23 10 Points\", points: 10 },\n      { id: \"fifty\", label: \"\ud83d\ude80 50 Points\", points: 50 },\n      { id: \"hundred\", label: \"\ud83c\udfc6 100 Points\", points: 100 },\n      { id: \"streak5\", label: \"\ud83d\udd25 5 Streak\", streak: 5 },\n      { id: \"wizard\", label: \"\ud83e\uddd9 Word Wizard\", points: 250 }\n    ];\n\n    const mascots = [\"\ud83e\udd89\", \"\ud83d\udc31\", \"\ud83e\udd84\", \"\ud83d\udc38\", \"\ud83d\udc36\", \"\ud83d\udc35\", \"\ud83d\udc3c\", \"\ud83e\udd8a\"];\n    const niceMessages = [\n      \"Great reading!\",\n      \"You are getting stronger!\",\n      \"Brilliant try!\",\n      \"Fantastic effort!\",\n      \"Keep going, superstar!\",\n      \"That brain is working hard!\"\n    ];\n\n    let words = loadWords();\n    let score = Number(localStorage.getItem(\"wordWizardScore\")) || 0;\n    let streak = Number(localStorage.getItem(\"wordWizardStreak\")) || 0;\n    let currentWord = null;\n\n    function loadWords() {\n      const saved = localStorage.getItem(\"wordWizardWords\");\n\n      if (saved) {\n        return JSON.parse(saved);\n      }\n\n      return DEFAULT_WORDS.map(word => ({\n        ...word,\n        shown: 0,\n        known: 0,\n        missed: 0,\n        lastSeen: null\n      }));\n    }\n\n    function saveAll() {\n      localStorage.setItem(\"wordWizardWords\", JSON.stringify(words));\n      localStorage.setItem(\"wordWizardScore\", String(score));\n      localStorage.setItem(\"wordWizardStreak\", String(streak));\n    }\n\n    function getWordWeight(word) {\n      const notSeenBonus = word.shown === 0 ? 3 : 0;\n      const missBonus = word.missed * 3;\n      const knownReduction = word.known * 1.4;\n      const difficultyBonus = word.difficulty * 1.2;\n      const recentPenalty = word.lastSeen && Date.now() - word.lastSeen < 15000 ? 3 : 0;\n\n      return Math.max(0.5, difficultyBonus + missBonus + notSeenBonus - knownReduction - recentPenalty);\n    }\n\n    function chooseWord() {\n      const weightedWords = [];\n\n      words.forEach(word => {\n        const weight = Math.round(getWordWeight(word) * 10);\n        for (let i = 0; i < weight; i++) {\n          weightedWords.push(word);\n        }\n      });\n\n      currentWord = weightedWords[Math.floor(Math.random() * weightedWords.length)];\n      currentWord.lastSeen = Date.now();\n\n      const wordElement = document.getElementById(\"word\");\n      wordElement.textContent = currentWord.text;\n      wordElement.classList.remove(\"pop\");\n      void wordElement.offsetWidth;\n      wordElement.classList.add(\"pop\");\n\n      document.getElementById(\"mascot\").textContent = mascots[Math.floor(Math.random() * mascots.length)];\n      updateDisplay();\n\n    }\n\n    function populateVoiceList() {\n      const voiceSelect = document.getElementById(\"voiceSelect\");\n      if (!voiceSelect) return;\n\n      const voices = window.speechSynthesis.getVoices();\n\n      voiceSelect.innerHTML = \"\";\n\n      if (!voices.length) {\n        const option = document.createElement(\"option\");\n        option.textContent = \"Loading voices...\";\n        option.value = \"\";\n        voiceSelect.appendChild(option);\n        return;\n      }\n\n      const savedVoiceName = localStorage.getItem(\"wordWizardVoiceName\");\n\n      voices.forEach(voice => {\n        const option = document.createElement(\"option\");\n        option.value = voice.name;\n        option.textContent = `${voice.name} (${voice.lang})`;\n        voiceSelect.appendChild(option);\n      });\n\n      const preferredVoice =\n        voices.find(v => v.name === savedVoiceName) ||\n        voices.find(v => v.name.includes(\"Natural\")) ||\n        voices.find(v => v.name.includes(\"Sonia\")) ||\n        voices.find(v => v.name.includes(\"Jenny\")) ||\n        voices.find(v => v.lang.toLowerCase().startsWith(\"en-au\")) ||\n        voices.find(v => v.lang.toLowerCase().startsWith(\"en\")) ||\n        voices[0];\n\n      if (preferredVoice) {\n        voiceSelect.value = preferredVoice.name;\n      }\n    }\n\n    function getSelectedVoice() {\n      const voices = window.speechSynthesis.getVoices();\n      const voiceSelect = document.getElementById(\"voiceSelect\");\n      const selectedName = voiceSelect ? voiceSelect.value : \"\";\n\n      return (\n        voices.find(voice => voice.name === selectedName) ||\n        voices.find(voice => voice.name.includes(\"Natural\")) ||\n        voices.find(voice => voice.lang.toLowerCase().startsWith(\"en-au\")) ||\n        voices.find(voice => voice.lang.toLowerCase().startsWith(\"en\")) ||\n        voices[0]\n      );\n    }\n\n    function speakCurrentWord() {\n      if (!currentWord) return;\n\n      window.speechSynthesis.cancel();\n\n      const speech = new SpeechSynthesisUtterance(currentWord.text);\n      speech.voice = getSelectedVoice();\n      speech.rate = 0.7;\n      speech.pitch = 1.05;\n      speech.volume = 1;\n\n      window.speechSynthesis.speak(speech);\n    }\n\n    function markWord(understood) {\n      if (!currentWord) return;\n\n      currentWord.shown++;\n\n      if (understood) {\n        currentWord.known++;\n        streak++;\n        score += currentWord.difficulty * 2;\n        document.getElementById(\"message\").textContent = niceMessages[Math.floor(Math.random() * niceMessages.length)];\n      } else {\n        currentWord.missed++;\n        streak = 0;\n        score += 1;\n        document.getElementById(\"message\").textContent = \"Good try. We will practise that one again!\";\n      }\n\n      const oldBadgeCount = getUnlockedBadges().length;\n\n      saveAll();\n      updateDisplay();\n      updateStats();\n\n      const newBadgeCount = getUnlockedBadges().length;\n\n      if (understood) {\n        burstConfetti(understood ? 20 : 8);\n      }\n\n      if (newBadgeCount > oldBadgeCount) {\n        document.getElementById(\"message\").textContent = \"New badge unlocked!\";\n        burstConfetti(50);\n      }\n\n      setTimeout(chooseWord, 750);\n    }\n\n    function getCurrentLevelIndex() {\n      let index = 0;\n\n      LEVELS.forEach((level, i) => {\n        if (score >= level.min) index = i;\n      });\n\n      return index;\n    }\n\n    function updateDisplay() {\n      const tried = words.reduce((total, word) => total + word.shown, 0);\n      const levelIndex = getCurrentLevelIndex();\n      const level = LEVELS[levelIndex];\n      const nextLevel = LEVELS[levelIndex + 1] || level;\n      const levelStart = level.min;\n      const levelEnd = nextLevel.min || level.next;\n      const progress = Math.min(100, ((score - levelStart) \/ (levelEnd - levelStart)) * 100);\n\n      document.getElementById(\"score\").textContent = score;\n      document.getElementById(\"level\").textContent = levelIndex + 1;\n      document.getElementById(\"streak\").textContent = streak;\n      document.getElementById(\"tried\").textContent = tried;\n      document.getElementById(\"levelName\").textContent = level.name;\n      document.getElementById(\"nextLevel\").textContent = `${score} \/ ${levelEnd}`;\n      document.getElementById(\"progressFill\").style.width = `${progress}%`;\n\n      renderBadges();\n    }\n\n    function getUnlockedBadges() {\n      return BADGES.filter(badge => {\n        if (badge.points !== undefined && score >= badge.points) return true;\n        if (badge.streak !== undefined && streak >= badge.streak) return true;\n        return false;\n      });\n    }\n\n    function renderBadges() {\n      const unlockedIds = getUnlockedBadges().map(badge => badge.id);\n      const container = document.getElementById(\"badges\");\n\n      container.innerHTML = BADGES.map(badge => {\n        const unlocked = unlockedIds.includes(badge.id) ? \"unlocked\" : \"\";\n        return `<span class=\"badge ${unlocked}\">${badge.label}<\/span>`;\n      }).join(\"\");\n    }\n\n    function updateStats() {\n      const sorted = [...words].sort((a, b) => {\n        const aAccuracy = a.shown ? a.known \/ a.shown : 1;\n        const bAccuracy = b.shown ? b.known \/ b.shown : 1;\n        return aAccuracy - bAccuracy || b.missed - a.missed;\n      });\n\n      const rows = sorted.map(word => {\n        const accuracy = word.shown ? Math.round((word.known \/ word.shown) * 100) : 0;\n        return `\n          <tr>\n            <td><strong>${word.text}<\/strong><\/td>\n            <td>${word.difficulty}<\/td>\n            <td>${word.shown}<\/td>\n            <td>${word.known}<\/td>\n            <td>${word.missed}<\/td>\n            <td>${accuracy}%<\/td>\n          <\/tr>\n        `;\n      }).join(\"\");\n\n      document.getElementById(\"statsTable\").innerHTML = `\n        <table>\n          <thead>\n            <tr>\n              <th>Word<\/th>\n              <th>Difficulty<\/th>\n              <th>Shown<\/th>\n              <th>Known<\/th>\n              <th>Learning<\/th>\n              <th>Known Rate<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody>${rows}<\/tbody>\n        <\/table>\n      `;\n    }\n\n    function togglePanel(id) {\n      document.getElementById(id).classList.toggle(\"open\");\n      updateStats();\n    }\n\n    function addWords() {\n      const textarea = document.getElementById(\"newWords\");\n      const newWords = textarea.value\n        .split(\"\\n\")\n        .map(word => word.trim().toLowerCase())\n        .filter(Boolean);\n\n      const existing = new Set(words.map(word => word.text.toLowerCase()));\n\n      newWords.forEach(text => {\n        if (!existing.has(text)) {\n          const difficulty = Math.min(5, Math.max(1, Math.ceil(text.length \/ 3)));\n          words.push({\n            text,\n            difficulty,\n            shown: 0,\n            known: 0,\n            missed: 0,\n            lastSeen: null\n          });\n        }\n      });\n\n      textarea.value = \"\";\n      saveAll();\n      updateStats();\n      document.getElementById(\"message\").textContent = \"New words added!\";\n      chooseWord();\n    }\n\n    function resetProgress() {\n      const sure = confirm(\"Reset all points and word progress?\");\n\n      if (!sure) return;\n\n      localStorage.removeItem(\"wordWizardWords\");\n      localStorage.removeItem(\"wordWizardScore\");\n      localStorage.removeItem(\"wordWizardStreak\");\n\n      words = loadWords();\n      score = 0;\n      streak = 0;\n\n      document.getElementById(\"message\").textContent = \"Progress reset.\";\n      chooseWord();\n    }\n\n    function burstConfetti(amount) {\n      const colours = [\"#ff8fab\", \"#ffd166\", \"#7bed9f\", \"#74b9ff\", \"#b197fc\", \"#ffffff\"];\n\n      for (let i = 0; i < amount; i++) {\n        const piece = document.createElement(\"div\");\n        piece.className = \"confetti\";\n        piece.style.left = `${Math.random() * 100}vw`;\n        piece.style.background = colours[Math.floor(Math.random() * colours.length)];\n        piece.style.animationDelay = `${Math.random() * 0.35}s`;\n        piece.style.transform = `rotate(${Math.random() * 360}deg)`;\n        document.body.appendChild(piece);\n\n        setTimeout(() => piece.remove(), 2000);\n      }\n    }\n\n    const voiceSelect = document.getElementById(\"voiceSelect\");\n\n    if (voiceSelect) {\n      voiceSelect.addEventListener(\"change\", () => {\n        localStorage.setItem(\"wordWizardVoiceName\", voiceSelect.value);\n      });\n    }\n\n    populateVoiceList();\n\n    if (\"onvoiceschanged\" in window.speechSynthesis) {\n      window.speechSynthesis.onvoiceschanged = populateVoiceList;\n    }\n\n    setTimeout(populateVoiceList, 300);\n    setTimeout(populateVoiceList, 1000);\n\n    updateDisplay();\n    updateStats();\n    chooseWord();\n  <\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Word Wizard Reading Game \ud83c\udf08 Word Wizard \ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67 Parent \u2795 Words \ud83d\udd04 Reset \ud83d\udde3\ufe0fvoice Points 0 Level 1 Streak 0 Words Tried 0 \ud83e\udd89 Can you read this word? cat \ud83d\udd0a Read it \u2705 I know it \ud83e\udd14 Still learning Level 1 Reader 0 \/ 50 Parent Stats Unknown words are shown more often. Known &#8230; <a title=\"Munchie Words\" class=\"read-more\" href=\"https:\/\/seanholden.xyz\/?page_id=438\" aria-label=\"More on Munchie Words\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-438","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages\/438","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=438"}],"version-history":[{"count":5,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages\/438\/revisions"}],"predecessor-version":[{"id":448,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages\/438\/revisions\/448"}],"wp:attachment":[{"href":"https:\/\/seanholden.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=438"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}