{"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-28T01:15:41","modified_gmt":"2026-06-28T01:15:41","slug":"munchie-words","status":"publish","type":"page","link":"https:\/\/seanholden.xyz\/?page_id=438","title":{"rendered":"Munchie Words"},"content":{"rendered":"\n<div id=\"munchieWordsApp\" class=\"munchie-words-app\">\n  <style>\n    #munchieWordsApp {\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      --shadow: 0 18px 45px rgba(34, 48, 68, 0.18);\n      width: min(980px, 94vw);\n      margin: 0 auto;\n      padding: 24px 0 36px;\n      font-family: \"Comic Sans MS\", \"Trebuchet MS\", Arial, sans-serif;\n      color: var(--ink);\n    }\n\n    #munchieWordsApp * {\n      box-sizing: border-box;\n    }\n\n    #munchieWordsApp .mw-bg {\n      background: linear-gradient(135deg, var(--bg1), var(--bg2), var(--bg3));\n      background-size: 300% 300%;\n      animation: mwFloatBackground 14s ease infinite;\n      border-radius: 36px;\n      padding: 22px;\n    }\n\n    @keyframes mwFloatBackground {\n      0% { background-position: 0% 50%; }\n      50% { background-position: 100% 50%; }\n      100% { background-position: 0% 50%; }\n    }\n\n    #munchieWordsApp 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    #munchieWordsApp 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    #munchieWordsApp .top-buttons {\n      display: flex;\n      gap: 10px;\n      flex-wrap: wrap;\n      align-items: center;\n    }\n\n    #munchieWordsApp .pill,\n    #munchieWordsApp #mwVoiceSelect {\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    #munchieWordsApp #mwVoiceSelect {\n      max-width: 260px;\n      font-size: 15px;\n      padding: 12px 14px;\n    }\n\n    #munchieWordsApp .score-row {\n      display: grid;\n      grid-template-columns: repeat(4, 1fr);\n      gap: 12px;\n      margin-bottom: 18px;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .score-box .label {\n      font-size: 14px;\n      opacity: 0.75;\n      font-weight: 800;\n    }\n\n    #munchieWordsApp .score-box .value {\n      font-size: 26px;\n      font-weight: 900;\n      margin-top: 4px;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .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    #munchieWordsApp .instruction {\n      font-size: clamp(18px, 3.4vw, 28px);\n      font-weight: 900;\n      margin: 6px 0 16px;\n    }\n\n    #munchieWordsApp .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      user-select: none;\n    }\n\n    #munchieWordsApp .word.pop {\n      animation: mwPop 0.38s ease;\n    }\n\n    @keyframes mwPop {\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    #munchieWordsApp .button-row {\n      display: flex;\n      justify-content: center;\n      gap: 16px;\n      flex-wrap: wrap;\n      margin-top: 16px;\n    }\n\n    #munchieWordsApp .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: 230px;\n      transition: transform 0.08s ease, box-shadow 0.08s ease;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .known { background: var(--green); }\n    #munchieWordsApp .learning { background: var(--yellow); }\n\n    #munchieWordsApp .message {\n      min-height: 42px;\n      margin-top: 18px;\n      font-size: clamp(18px, 3.2vw, 26px);\n      font-weight: 900;\n    }\n\n    #munchieWordsApp .progress-wrap {\n      margin: 22px auto 0;\n      max-width: 620px;\n      text-align: left;\n    }\n\n    #munchieWordsApp .progress-label {\n      display: flex;\n      justify-content: space-between;\n      font-weight: 900;\n      margin-bottom: 6px;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .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    #munchieWordsApp .badges {\n      display: flex;\n      flex-wrap: wrap;\n      justify-content: center;\n      gap: 10px;\n      margin-top: 18px;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .badge.unlocked {\n      opacity: 1;\n      filter: none;\n      border: 3px solid #ffe66d;\n    }\n\n    #munchieWordsApp .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    #munchieWordsApp .panel.open { display: block; }\n    #munchieWordsApp .panel h2 { margin-top: 0; }\n\n    #munchieWordsApp 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    #munchieWordsApp th,\n    #munchieWordsApp td {\n      padding: 10px;\n      border-bottom: 1px solid #eee;\n      text-align: left;\n      font-size: 14px;\n    }\n\n    #munchieWordsApp th { background: #f3f6ff; }\n\n    #munchieWordsApp 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    #munchieWordsApp .small-note {\n      font-family: Arial, sans-serif;\n      opacity: 0.78;\n      font-size: 14px;\n      line-height: 1.45;\n    }\n\n    #munchieWordsApp .confetti {\n      position: fixed;\n      top: -20px;\n      width: 12px;\n      height: 18px;\n      opacity: 0.9;\n      pointer-events: none;\n      animation: mwFall 1.6s linear forwards;\n      z-index: 1000;\n    }\n\n    @keyframes mwFall {\n      to {\n        transform: translateY(110vh) rotate(720deg);\n        opacity: 0.4;\n      }\n    }\n\n    @media (max-width: 700px) {\n      #munchieWordsApp .score-row { grid-template-columns: repeat(2, 1fr); }\n      #munchieWordsApp .big-button { width: 100%; }\n      #munchieWordsApp .word { min-width: 100%; }\n      #munchieWordsApp #mwVoiceSelect { width: 100%; max-width: none; }\n    }\n  <\/style>\n\n  <div class=\"mw-bg\">\n    <header>\n      <h1>\ud83c\udf6a Munchie Words<\/h1>\n      <div class=\"top-buttons\">\n        <button class=\"pill\" type=\"button\" id=\"mwParentBtn\">\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67 Parent<\/button>\n        <button class=\"pill\" type=\"button\" id=\"mwWordsBtn\">\u2795 Words<\/button>\n        <button class=\"pill\" type=\"button\" id=\"mwResetBtn\">\ud83d\udd04 Reset<\/button>\n        <label class=\"pill\" for=\"mwVoiceSelect\">\ud83d\udde3\ufe0f Voice<\/label>\n        <select id=\"mwVoiceSelect\"><\/select>\n      <\/div>\n    <\/header>\n\n    <section class=\"score-row\">\n      <div class=\"score-box\"><div class=\"label\">Points<\/div><div class=\"value\" id=\"mwScore\">0<\/div><\/div>\n      <div class=\"score-box\"><div class=\"label\">Level<\/div><div class=\"value\" id=\"mwLevel\">1<\/div><\/div>\n      <div class=\"score-box\"><div class=\"label\">Streak<\/div><div class=\"value\" id=\"mwStreak\">0<\/div><\/div>\n      <div class=\"score-box\"><div class=\"label\">Words Tried<\/div><div class=\"value\" id=\"mwTried\">0<\/div><\/div>\n    <\/section>\n\n    <main class=\"card\">\n      <div class=\"mascot\" id=\"mwMascot\">\ud83c\udf6a<\/div>\n      <div class=\"instruction\">Try reading the word. It only reads aloud after &#8220;Still learning&#8221;.<\/div>\n      <div class=\"word\" id=\"mwWord\">cat<\/div>\n\n      <div class=\"button-row\">\n        <button class=\"big-button known\" type=\"button\" id=\"mwKnownBtn\">\u2705 I know it<\/button>\n        <button class=\"big-button learning\" type=\"button\" id=\"mwLearningBtn\">\ud83e\udd14 Still learning<\/button>\n      <\/div>\n\n      <div class=\"message\" id=\"mwMessage\"><\/div>\n\n      <div class=\"progress-wrap\">\n        <div class=\"progress-label\">\n          <span id=\"mwLevelName\">Level 1 Reader<\/span>\n          <span id=\"mwNextLevel\">0 \/ 50<\/span>\n        <\/div>\n        <div class=\"progress\"><div class=\"progress-fill\" id=\"mwProgressFill\"><\/div><\/div>\n      <\/div>\n\n      <div class=\"badges\" id=\"mwBadges\"><\/div>\n    <\/main>\n\n    <section class=\"panel\" id=\"mwParentPanel\">\n      <h2>Parent Stats<\/h2>\n      <p class=\"small-note\">\n        There are 84 default words, including 60 new words. Unknown words are shown more often.\n        Known words still return occasionally. Audio only plays after a word is marked &#8220;Still learning&#8221;.\n      <\/p>\n      <div id=\"mwStatsTable\"><\/div>\n    <\/section>\n\n    <section class=\"panel\" id=\"mwWordPanel\">\n      <h2>Add Your Own Words<\/h2>\n      <p class=\"small-note\">Add one word per line. New words are automatically given a rough difficulty based on length.<\/p>\n      <textarea id=\"mwNewWords\" placeholder=\"apple&#10;happy&#10;because\"><\/textarea>\n      <div class=\"button-row\">\n        <button class=\"big-button learning\" type=\"button\" id=\"mwAddWordsBtn\">\u2795 Add Words<\/button>\n      <\/div>\n    <\/section>\n  <\/div>\n\n  <script>\n    (function() {\n      const storagePrefix = \"munchieWordsV2\";\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      { text: \"and\", difficulty: 1 },\n      { text: \"the\", difficulty: 1 },\n      { text: \"is\", difficulty: 1 },\n      { text: \"it\", difficulty: 1 },\n      { text: \"in\", difficulty: 1 },\n      { text: \"on\", difficulty: 1 },\n      { text: \"up\", difficulty: 1 },\n      { text: \"at\", difficulty: 1 },\n      { text: \"me\", difficulty: 1 },\n      { text: \"we\", difficulty: 1 },\n      { text: \"go\", difficulty: 1 },\n      { text: \"no\", difficulty: 1 },\n      { text: \"yes\", difficulty: 1 },\n      { text: \"see\", difficulty: 1 },\n      { text: \"can\", difficulty: 1 },\n      { text: \"hat\", difficulty: 1 },\n      { text: \"bat\", difficulty: 1 },\n      { text: \"mat\", difficulty: 1 },\n      { text: \"sit\", difficulty: 1 },\n      { text: \"pin\", difficulty: 1 },\n      { text: \"cup\", difficulty: 2 },\n      { text: \"bus\", difficulty: 2 },\n      { text: \"bed\", difficulty: 2 },\n      { text: \"box\", difficulty: 2 },\n      { text: \"fox\", difficulty: 2 },\n      { text: \"hen\", difficulty: 2 },\n      { text: \"pig\", difficulty: 2 },\n      { text: \"duck\", difficulty: 2 },\n      { text: \"frog\", difficulty: 2 },\n      { text: \"milk\", difficulty: 2 },\n      { text: \"tree\", difficulty: 2 },\n      { text: \"ball\", difficulty: 2 },\n      { text: \"cake\", difficulty: 2 },\n      { text: \"bike\", difficulty: 2 },\n      { text: \"home\", difficulty: 2 },\n      { text: \"rain\", difficulty: 2 },\n      { text: \"moon\", difficulty: 2 },\n      { text: \"star\", difficulty: 2 },\n      { text: \"hand\", difficulty: 2 },\n      { text: \"fast\", difficulty: 2 },\n      { text: \"smile\", difficulty: 3 },\n      { text: \"water\", difficulty: 3 },\n      { text: \"house\", difficulty: 3 },\n      { text: \"chair\", difficulty: 3 },\n      { text: \"apple\", difficulty: 3 },\n      { text: \"table\", difficulty: 3 },\n      { text: \"garden\", difficulty: 3 },\n      { text: \"rocket\", difficulty: 3 },\n      { text: \"animal\", difficulty: 3 },\n      { text: \"purple\", difficulty: 3 },\n      { text: \"orange\", difficulty: 3 },\n      { text: \"window\", difficulty: 3 },\n      { text: \"rabbit\", difficulty: 3 },\n      { text: \"flower\", difficulty: 3 },\n      { text: \"morning\", difficulty: 4 },\n      { text: \"picture\", difficulty: 4 },\n      { text: \"outside\", difficulty: 4 },\n      { text: \"teacher\", difficulty: 4 },\n      { text: \"chicken\", difficulty: 4 },\n      { text: \"monster\", difficulty: 4 }\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 Word Muncher\", 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: \"munchie\", label: \"\ud83c\udf6a Word Muncher\", points: 250 }\n      ];\n\n      const mascots = [\"\ud83c\udf6a\", \"\ud83e\udd89\", \"\ud83d\udc31\", \"\ud83e\udd84\", \"\ud83d\udc38\", \"\ud83d\udc36\", \"\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(storagePrefix + \"Score\")) || 0;\n      let streak = Number(localStorage.getItem(storagePrefix + \"Streak\")) || 0;\n      let currentWord = null;\n\n      const el = (id) => document.getElementById(id);\n\n      function loadWords() {\n        const saved = localStorage.getItem(storagePrefix + \"Words\");\n        const defaultWords = DEFAULT_WORDS.map(word => ({\n          ...word,\n          shown: 0,\n          known: 0,\n          missed: 0,\n          lastSeen: null\n        }));\n\n        if (!saved) return defaultWords;\n\n        const savedWords = JSON.parse(saved);\n        const savedMap = new Map(savedWords.map(word => [word.text.toLowerCase(), word]));\n\n        defaultWords.forEach(word => {\n          if (!savedMap.has(word.text.toLowerCase())) savedWords.push(word);\n        });\n\n        return savedWords;\n      }\n\n      function saveAll() {\n        localStorage.setItem(storagePrefix + \"Words\", JSON.stringify(words));\n        localStorage.setItem(storagePrefix + \"Score\", String(score));\n        localStorage.setItem(storagePrefix + \"Streak\", 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++) weightedWords.push(word);\n        });\n\n        currentWord = weightedWords[Math.floor(Math.random() * weightedWords.length)];\n        currentWord.lastSeen = Date.now();\n\n        const wordElement = el(\"mwWord\");\n        wordElement.textContent = currentWord.text;\n        wordElement.classList.remove(\"pop\");\n        void wordElement.offsetWidth;\n        wordElement.classList.add(\"pop\");\n\n        el(\"mwMascot\").textContent = mascots[Math.floor(Math.random() * mascots.length)];\n        updateDisplay();\n        saveAll();\n      }\n\n      function populateVoiceList() {\n        const voiceSelect = el(\"mwVoiceSelect\");\n        if (!voiceSelect) return;\n\n        const voices = window.speechSynthesis.getVoices();\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(storagePrefix + \"VoiceName\");\n        const englishVoices = voices.filter(voice => voice.lang && voice.lang.toLowerCase().startsWith(\"en\"));\n        const voicesToShow = englishVoices.length ? englishVoices : voices;\n\n        voicesToShow.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          voicesToShow.find(v => v.name === savedVoiceName) ||\n          voicesToShow.find(v => v.name.includes(\"Natural\")) ||\n          voicesToShow.find(v => v.name.includes(\"Sonia\")) ||\n          voicesToShow.find(v => v.name.includes(\"Jenny\")) ||\n          voicesToShow.find(v => v.lang && v.lang.toLowerCase().startsWith(\"en-au\")) ||\n          voicesToShow.find(v => v.lang && v.lang.toLowerCase().startsWith(\"en\")) ||\n          voicesToShow[0];\n\n        if (preferredVoice) voiceSelect.value = preferredVoice.name;\n      }\n\n      function getSelectedVoice() {\n        const voices = window.speechSynthesis.getVoices();\n        const voiceSelect = el(\"mwVoiceSelect\");\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 && voice.lang.toLowerCase().startsWith(\"en-au\")) ||\n          voices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith(\"en\")) ||\n          voices[0] ||\n          null\n        );\n      }\n\n      function speakText(text) {\n        window.speechSynthesis.cancel();\n\n        const speech = new SpeechSynthesisUtterance(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          el(\"mwMessage\").textContent = niceMessages[Math.floor(Math.random() * niceMessages.length)];\n        } else {\n          currentWord.missed++;\n          streak = 0;\n          score += 1;\n          el(\"mwMessage\").textContent = `Let's hear it: ${currentWord.text}`;\n          speakText(currentWord.text);\n        }\n\n        const oldBadgeCount = getUnlockedBadges().length;\n\n        saveAll();\n        updateDisplay();\n        updateStats();\n\n        const newBadgeCount = getUnlockedBadges().length;\n\n        if (understood) burstConfetti(20);\n\n        if (newBadgeCount > oldBadgeCount) {\n          el(\"mwMessage\").textContent = \"New badge unlocked!\";\n          burstConfetti(50);\n        }\n\n        const delay = understood ? 750 : Math.max(1500, currentWord.text.length * 190);\n        setTimeout(chooseWord, delay);\n      }\n\n      function getCurrentLevelIndex() {\n        let index = 0;\n        LEVELS.forEach((level, i) => {\n          if (score >= level.min) index = i;\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        el(\"mwScore\").textContent = score;\n        el(\"mwLevel\").textContent = levelIndex + 1;\n        el(\"mwStreak\").textContent = streak;\n        el(\"mwTried\").textContent = tried;\n        el(\"mwLevelName\").textContent = level.name;\n        el(\"mwNextLevel\").textContent = `${score} \/ ${levelEnd}`;\n        el(\"mwProgressFill\").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        el(\"mwBadges\").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        el(\"mwStatsTable\").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        el(id).classList.toggle(\"open\");\n        updateStats();\n      }\n\n      function addWords() {\n        const textarea = el(\"mwNewWords\");\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            existing.add(text);\n          }\n        });\n\n        textarea.value = \"\";\n        saveAll();\n        updateStats();\n        el(\"mwMessage\").textContent = \"New words added!\";\n        chooseWord();\n      }\n\n      function resetProgress() {\n        const sure = confirm(\"Reset all points and word progress?\");\n        if (!sure) return;\n\n        localStorage.removeItem(storagePrefix + \"Words\");\n        localStorage.removeItem(storagePrefix + \"Score\");\n        localStorage.removeItem(storagePrefix + \"Streak\");\n\n        words = loadWords();\n        score = 0;\n        streak = 0;\n\n        el(\"mwMessage\").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          setTimeout(() => piece.remove(), 2000);\n        }\n      }\n\n      el(\"mwParentBtn\").addEventListener(\"click\", () => togglePanel(\"mwParentPanel\"));\n      el(\"mwWordsBtn\").addEventListener(\"click\", () => togglePanel(\"mwWordPanel\"));\n      el(\"mwResetBtn\").addEventListener(\"click\", resetProgress);\n      el(\"mwKnownBtn\").addEventListener(\"click\", () => markWord(true));\n      el(\"mwLearningBtn\").addEventListener(\"click\", () => markWord(false));\n      el(\"mwAddWordsBtn\").addEventListener(\"click\", addWords);\n\n      el(\"mwVoiceSelect\").addEventListener(\"change\", () => {\n        localStorage.setItem(storagePrefix + \"VoiceName\", el(\"mwVoiceSelect\").value);\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    })();\n  <\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>\ud83c\udf6a Munchie Words \ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67 Parent \u2795 Words \ud83d\udd04 Reset \ud83d\udde3\ufe0f Voice Points 0 Level 1 Streak 0 Words Tried 0 \ud83c\udf6a Try reading the word. It only reads aloud after &#8220;Still learning&#8221;. cat \u2705 I know it \ud83e\udd14 Still learning Level 1 Reader 0 \/ 50 Parent Stats There are 84 default words, including 60 &#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":7,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages\/438\/revisions"}],"predecessor-version":[{"id":481,"href":"https:\/\/seanholden.xyz\/index.php?rest_route=\/wp\/v2\/pages\/438\/revisions\/481"}],"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}]}}