← Mike Higgins Home
Custom Code Experiment •

Satu Dua Tiga

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Satu Dua Tiga (1,2,3) — counting in Indonesian</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Anton&family=Archivo:ital,wght@1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
    <style>
        :root {
            --cream: #EFE9DF;
            --cream-deep: #E4DCCE;
            --pink: #EE6DB4;
            --pink-press: #D8509C;
            --cyan: #4FC3F2;
            --yellow: #F7D14B;
            --ink: #000;
            --r: 9px;
            --bd: 4px;
            --shadow: 5px 5px 0 var(--ink);
        }

        * {
            box-sizing: border-box;
        }

        html,
        body {
            margin: 0;
            padding: 0;
        }

        body {
            background: var(--cream);
            color: var(--ink);
            font-family: 'Archivo', 'Arial Narrow', Arial, sans-serif;
            font-style: italic;
            font-weight: 800;
            -webkit-font-smoothing: antialiased;
            min-height: 100vh;
            padding: 26px 16px 60px;
        }

        body::before {
            content: "";
            position: fixed;
            inset: 0;
            pointer-events: none;
            z-index: 0;
            opacity: .5;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='260' height='260' filter='url(%23n)' opacity='0.28'/%3E%3C/svg%3E");
        }

        .wrap {
            position: relative;
            z-index: 1;
            max-width: 660px;
            margin: 0 auto;
        }

        .eyebrow-row {
            display: flex;
            align-items: flex-end;
            justify-content: space-between;
            gap: 12px;
            margin-bottom: 2px;
        }

        .eyebrow {
            font-size: clamp(13px, 3.2vw, 17px);
            letter-spacing: .02em;
            text-transform: uppercase;
            font-weight: 900;
            margin: 0;
        }

        .streak {
            font-size: clamp(12px, 3vw, 15px);
            text-transform: uppercase;
            font-weight: 900;
            white-space: nowrap;
        }

        .streak b {
            background: var(--yellow);
            border: 2px solid var(--ink);
            padding: 1px 7px;
            border-radius: 5px;
            display: inline-block;
        }

        .btn-mute {
            font-size: 11px;
            padding: 3px 8px;
            border-width: 2px;
            border-radius: 5px;
            box-shadow: 2px 2px 0 var(--ink);
            margin-left: 7px;
            vertical-align: 2px;
        }

        .btn-mute[aria-pressed="true"] {
            background: var(--cream-deep);
            opacity: .65;
        }

        .btn-mute:active {
            transform: translate(2px, 2px);
        }

        .display {
            font-family: 'Anton', Impact, 'Arial Narrow Bold', sans-serif;
            font-style: normal;
            font-weight: 400;
            text-transform: uppercase;
            color: var(--pink);
            -webkit-text-stroke: 5px var(--ink);
            paint-order: stroke fill;
            text-shadow: 7px 8px 0 var(--ink);
            transform: skewX(-8deg);
            transform-origin: left bottom;
            line-height: .82;
            letter-spacing: -.01em;
            margin: 0;
            user-select: none;
        }

        .numeral {
            font-size: clamp(120px, 30vw, 215px);
            display: block;
            padding-left: 6px;
        }

        .word {
            font-size: clamp(34px, 10.5vw, 74px);
            display: block;
            padding-left: 6px;
            margin-top: 6px;
            min-height: 1.05em;
            -webkit-text-stroke: 4px var(--ink);
            text-shadow: 5px 6px 0 var(--ink);
            color: var(--cream);
        }

        .word.solved {
            color: var(--pink);
        }

        .word.blank {
            color: transparent;
            -webkit-text-stroke: 0;
            text-shadow: none;
        }

        .bar {
            display: flex;
            gap: 5px;
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            background: var(--cream-deep);
            padding: 6px;
            margin: 22px 0 14px;
            box-shadow: var(--shadow);
        }

        .sq {
            flex: 1;
            height: 26px;
            border: 3px solid var(--ink);
            border-radius: 4px;
            background: transparent;
        }

        .sq.on {
            background: var(--pink);
            box-shadow: inset -4px -4px 0 rgba(0, 0, 0, .22), inset 4px 4px 0 rgba(255, 255, 255, .35);
        }

        .panel {
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            background: var(--cream);
            box-shadow: var(--shadow);
            overflow: hidden;
            margin-bottom: 22px;
        }

        .panel-body {
            padding: 18px;
        }

        .label {
            display: block;
            text-transform: uppercase;
            font-size: 13px;
            font-weight: 900;
            margin: 0 0 8px;
        }

        input[type=text] {
            width: 100%;
            font-family: 'Archivo', Arial, sans-serif;
            font-style: italic;
            font-weight: 900;
            font-size: clamp(20px, 5.4vw, 28px);
            text-transform: lowercase;
            color: var(--ink);
            background: #fff;
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            padding: 12px 14px;
            outline: none;
        }

        input[type=text]::placeholder {
            color: #9a938a;
        }

        input[type=text]:focus {
            background: #FFF8E6;
            box-shadow: inset 0 0 0 3px var(--yellow);
        }

        input[type=text].wrong {
            animation: shake .32s;
        }

        input[type=text].right {
            background: var(--cyan);
        }

        @keyframes shake {
            0%,
            100% {
                transform: translateX(0);
            }
            20% {
                transform: translateX(-9px);
            }
            45% {
                transform: translateX(8px);
            }
            70% {
                transform: translateX(-5px);
            }
        }

        .feedback {
            text-transform: uppercase;
            font-size: clamp(13px, 3.4vw, 15px);
            font-weight: 900;
            margin: 10px 2px 0;
            min-height: 1.3em;
            letter-spacing: .01em;
        }

        .feedback.ok {
            color: #1B7FAE;
        }

        .feedback.no {
            color: var(--pink-press);
        }

        .row {
            display: flex;
            gap: 10px;
            margin-top: 14px;
            flex-wrap: wrap;
        }

        button {
            font-family: 'Archivo', Arial, sans-serif;
            font-style: italic;
            font-weight: 900;
            text-transform: uppercase;
            color: var(--ink);
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            background: var(--cream);
            cursor: pointer;
            padding: 11px 16px;
            font-size: 15px;
            box-shadow: 4px 4px 0 var(--ink);
            transition: transform .06s, box-shadow .06s;
        }

        button:hover {
            background: var(--cream-deep);
        }

        button:active {
            transform: translate(4px, 4px);
            box-shadow: 0 0 0 var(--ink);
        }

        button:focus-visible {
            outline: 4px solid var(--yellow);
            outline-offset: 3px;
        }

        .btn-audio {
            background: var(--cyan);
            flex: 1;
            min-width: 150px;
        }

        .btn-audio:hover {
            background: #37B4E8;
        }

        .btn-audio.speaking {
            background: var(--yellow);
        }

        .strip {
            background: var(--cyan);
            border-top: var(--bd) solid var(--ink);
            padding: 18px;
            display: flex;
            gap: 12px;
        }

        .btn-check {
            flex: 1;
            background: var(--pink);
            font-size: clamp(20px, 5vw, 27px);
            padding: 14px 10px;
            letter-spacing: .01em;
        }

        .btn-check:hover {
            background: var(--pink-press);
        }

        .btn-skip {
            background: var(--cream);
            flex: 0 0 auto;
        }

        .sets {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 12px;
        }

        .set {
            display: flex;
            align-items: center;
            gap: 11px;
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            padding: 12px 14px;
            cursor: pointer;
            background: var(--cream);
            text-transform: uppercase;
            font-size: 14px;
            font-weight: 900;
            line-height: 1.15;
            user-select: none;
        }

        .set:hover {
            background: var(--cream-deep);
        }

        .set input {
            position: absolute;
            opacity: 0;
            width: 0;
            height: 0;
        }

        .dot {
            flex: 0 0 auto;
            width: 19px;
            height: 19px;
            border-radius: 50%;
            border: 4px solid var(--ink);
            background: var(--cream);
        }

        .set input:checked~.dot {
            background: var(--pink);
            box-shadow: inset 0 0 0 2px var(--cream);
        }

        .set:has(input:checked) {
            box-shadow: var(--shadow);
        }

        .set:has(input:focus-visible) {
            outline: 4px solid var(--yellow);
            outline-offset: 3px;
        }

        .chart-toggle {
            display: block;
            margin: 0 auto;
            background: none;
            border: none;
            box-shadow: none;
            text-decoration: underline;
            text-underline-offset: 4px;
            text-decoration-thickness: 3px;
            font-size: clamp(14px, 3.6vw, 17px);
            padding: 6px;
        }

        .chart-toggle:hover {
            background: none;
            color: var(--pink-press);
        }

        .chart-toggle:active {
            transform: none;
        }

        .chart {
            margin-top: 16px;
            border: var(--bd) solid var(--ink);
            border-radius: var(--r);
            background: var(--cream);
            box-shadow: var(--shadow);
            padding: 14px;
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
            gap: 8px;
        }

        .chart[hidden] {
            display: none;
        }

        .chip {
            display: flex;
            align-items: baseline;
            gap: 8px;
            border: 3px solid var(--ink);
            border-radius: 6px;
            background: var(--cream);
            box-shadow: none;
            padding: 8px 10px;
            font-size: 14px;
            text-transform: lowercase;
            width: 100%;
        }

        .chip:active {
            transform: translate(2px, 2px);
        }

        .chip b {
            font-family: 'Anton', Impact, sans-serif;
            font-style: normal;
            font-size: 17px;
            min-width: 32px;
            text-align: right;
        }

        .note {
            text-transform: uppercase;
            font-size: 12px;
            font-weight: 900;
            text-align: center;
            margin: 16px auto 0;
            max-width: 520px;
            line-height: 1.45;
            opacity: .75;
        }

        @media (max-width:520px) {
            .sets {
                grid-template-columns: 1fr;
            }
            .strip {
                flex-direction: column;
            }
            .btn-skip {
                width: 100%;
            }
        }

        @media (prefers-reduced-motion:reduce) {
            * {
                animation: none !important;
                transition: none !important;
            }
        }
    </style>
</head>
<body>

    <div class="wrap">

        <div class="eyebrow-row">
            <p class="eyebrow">Counting in Indonesian</p>
            <p class="streak">Streak <b id="streak">0</b>
                <button class="btn-mute" id="mute" type="button" aria-pressed="false" title="Turn sound effects off">Sound on</button>
            </p>
        </div>

        <h1 class="display"><span class="numeral" id="numeral">1</span></h1>
        <p class="display word blank" id="word" aria-live="polite">satu</p>

        <div class="bar" id="bar" aria-hidden="true"></div>

        <div class="panel">
            <div class="panel-body">
                <label class="label" for="answer">Write the number in Indonesian</label>
                <input id="answer" type="text" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="type it here">
                <p class="feedback" id="feedback"></p>
                <div class="row">
                    <button class="btn-audio" id="hear" type="button">🔊 Hear it</button>
                    <button id="hint" type="button">Hint</button>
                </div>
            </div>
            <div class="strip">
                <button class="btn-check" id="check" type="button">Check</button>
                <button class="btn-skip" id="skip" type="button">Skip</button>
            </div>
        </div>

        <div class="panel">
            <div class="panel-body">
                <span class="label">Pick a set</span>
                <div class="sets" id="sets"></div>
            </div>
        </div>

        <button class="chart-toggle" id="chartToggle" type="button" aria-expanded="false">Number chart + pronunciation</button>
        <div class="chart" id="chart" hidden></div>

        <p class="note" id="note"></p>
    </div>

    <script>
        (function() {
            'use strict';

            var UNITS = ['', 'satu', 'dua', 'tiga', 'empat', 'lima', 'enam', 'tujuh', 'delapan', 'sembilan'];

            function toIndo(n) {
                if (n === 0) return 'nol';
                if (n < 10) return UNITS[n];
                if (n === 10) return 'sepuluh';
                if (n === 11) return 'sebelas';
                if (n < 20) return UNITS[n - 10] + ' belas';
                if (n < 100) {
                    var t = Math.floor(n / 10),
                        r = n % 10;
                    return UNITS[t] + ' puluh' + (r ? ' ' + UNITS[r] : '');
                }
                if (n === 100) return 'seratus';
                if (n < 200) return 'seratus' + (n % 100 ? ' ' + toIndo(n % 100) : '');
                if (n < 1000) {
                    var h = Math.floor(n / 100),
                        rr = n % 100;
                    return UNITS[h] + ' ratus' + (rr ? ' ' + toIndo(rr) : '');
                }
                if (n === 1000) return 'seribu';
                return String(n);
            }

            function range(a, b) {
                var out = [];
                for (var i = a; i <= b; i++) out.push(i);
                return out;
            }

            function randInt(a, b) {
                return a + Math.floor(Math.random() * (b - a + 1));
            }

            var SETS = [{
                id: '1-10',
                label: '1 – 10',
                build: function() {
                    return range(1, 10);
                }
            }, {
                id: '11-20',
                label: '11 – 20',
                build: function() {
                    return range(11, 20);
                }
            }, {
                id: 'tens',
                label: 'Tens to 100',
                build: function() {
                    return [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
                }
            }, {
                id: '21-99',
                label: '21 – 99',
                build: function() {
                    var o = [];
                    while (o.length < 10) {
                        var n = randInt(21, 99);
                        if (o.indexOf(n) < 0) o.push(n);
                    }
                    return o;
                }
            }, {
                id: '100s',
                label: '100 – 1000',
                build: function() {
                    var o = [100, 1000];
                    while (o.length < 10) {
                        var n = randInt(101, 999);
                        if (o.indexOf(n) < 0) o.push(n);
                    }
                    return o;
                }
            }, {
                id: 'mixed',
                label: 'Mixed 1 – 1000',
                build: function() {
                    var o = [];
                    while (o.length < 10) {
                        var r = Math.random(),
                            n;
                        if (r < .35) n = randInt(1, 20);
                        else if (r < .55) n = randInt(2, 10) * 10;
                        else if (r < .85) n = randInt(21, 99);
                        else n = randInt(100, 1000);
                        if (o.indexOf(n) < 0) o.push(n);
                    }
                    return o;
                }
            }];

            var state = {
                set: SETS[0],
                queue: [],
                current: 1,
                solved: false,
                hints: 0,
                tries: 0,
                streak: 0,
                filled: 0,
                barNo: 1,
                pending: null,
                advance: null
            };

            var $ = function(id) { return document.getElementById(id); };
            var elNumeral = $('numeral'),
                elWord = $('word'),
                elAnswer = $('answer'),
                elFeedback = $('feedback'),
                elBar = $('bar'),
                elStreak = $('streak'),
                elHear = $('hear'),
                elHint = $('hint'),
                elCheck = $('check'),
                elSkip = $('skip'),
                elSets = $('sets'),
                elChart = $('chart'),
                elChartToggle = $('chartToggle'),
                elNote = $('note'),
                elMute = $('mute');

            var actx = null,
                muted = false;

            function audio() {
                if (muted) return null;
                if (actx === null) {
                    var AC = window.AudioContext || window.webkitAudioContext;
                    actx = AC ? new AC() : false;
                }
                if (actx && actx.state === 'suspended') actx.resume();
                return actx || null;
            }

            function tone(opts) {
                var ctx = audio();
                if (!ctx) return;
                var osc = ctx.createOscillator();
                var gain = ctx.createGain();
                var t0 = ctx.currentTime + (opts.at || 0);
                var dur = opts.dur || 0.16;

                osc.type = opts.type || 'triangle';
                osc.frequency.setValueAtTime(opts.freq, t0);
                if (opts.to) osc.frequency.exponentialRampToValueAtTime(opts.to, t0 + dur);

                gain.gain.setValueAtTime(0.0001, t0);
                gain.gain.exponentialRampToValueAtTime(opts.vol || 0.18, t0 + 0.012);
                gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);

                osc.connect(gain);
                gain.connect(ctx.destination);
                osc.start(t0);
                osc.stop(t0 + dur + 0.03);
            }

            function playWin() {
                var notes = [1046.5, 1318.5, 1568.0, 2093.0];
                for (var i = 0; i < notes.length; i++) {
                    tone({
                        freq: notes[i],
                        at: i * 0.075,
                        dur: i === 3 ? 0.36 : 0.15,
                        vol: i === 3 ? 0.2 : 0.15,
                        type: 'triangle'
                    });
                }
            }

            function playFail() {
                tone({ freq: 196, to: 150, dur: 0.16, vol: 0.16, type: 'square' });
                tone({ freq: 147, to: 104, dur: 0.30, vol: 0.16, type: 'square', at: 0.13 });
            }

            var voice = null,
                voiceChecked = false;

            function pickVoice() {
                if (!('speechSynthesis' in window)) return;
                var voices = window.speechSynthesis.getVoices() || [];
                for (var i = 0; i < voices.length; i++) {
                    var l = (voices[i].lang || '').toLowerCase();
                    if (l.indexOf('id') === 0 || l.indexOf('in-id') === 0) {
                        voice = voices[i];
                        break;
                    }
                }
                if (voices.length && !voiceChecked) {
                    voiceChecked = true;
                    if (!voice) {
                        elNote.textContent =
                            'No Indonesian voice installed on this device, so the audio uses your default voice and the pronunciation will be rough. Adding an Indonesian language pack in your system settings fixes it.';
                    }
                }
            }

            if ('speechSynthesis' in window) {
                pickVoice();
                window.speechSynthesis.onvoiceschanged = pickVoice;
            } else {
                elNote.textContent =
                    'This browser has no speech support, so the audio button will not play anything. Chrome, Edge, or Safari will.';
                elHear.disabled = true;
            }

            function speak(text, rate, done) {
                if (!('speechSynthesis' in window)) {
                    if (done) done();
                    return;
                }
                window.speechSynthesis.cancel();
                var u = new SpeechSynthesisUtterance(text);
                u.lang = 'id-ID';
                u.rate = rate || 0.82;
                u.pitch = 1;
                if (voice) u.voice = voice;
                u.onstart = function() {
                    elHear.classList.add('speaking');
                };
                u.onend = u.onerror = function() {
                    elHear.classList.remove('speaking');
                    if (done) done();
                };
                window.speechSynthesis.speak(u);
            }

            function normalise(s) {
                return String(s).toLowerCase()
                    .replace(/[^a-z\s]/g, '')
                    .replace(/\s+/g, ' ')
                    .trim();
            }

            function loose(s) {
                return normalise(s).replace(/\s/g, '');
            }

            function isCorrect(input, answer) {
                return loose(input) === loose(answer) && loose(input) !== '';
            }

            function drawBar() {
                elBar.innerHTML = '';
                for (var i = 0; i < 10; i++) {
                    var d = document.createElement('div');
                    d.className = 'sq' + (i < state.filled ? ' on' : '');
                    elBar.appendChild(d);
                }
            }

            function drawSets() {
                elSets.innerHTML = '';
                SETS.forEach(function(s) {
                    var lab = document.createElement('label');
                    lab.className = 'set';
                    var input = document.createElement('input');
                    input.type = 'radio';
                    input.name = 'set';
                    input.value = s.id;
                    input.checked = (s.id === state.set.id);
                    input.addEventListener('change', function() {
                        state.set = s;
                        state.queue = [];
                        nextCard();
                    });
                    var dot = document.createElement('span');
                    dot.className = 'dot';
                    var span = document.createElement('span');
                    span.textContent = s.label;
                    lab.appendChild(input);
                    lab.appendChild(dot);
                    lab.appendChild(span);
                    elSets.appendChild(lab);
                });
            }

            function drawChart() {
                elChart.innerHTML = '';
                var list = range(1, 20).concat([30, 40, 50, 60, 70, 80, 90, 100, 1000]);
                list.forEach(function(n) {
                    var b = document.createElement('button');
                    b.type = 'button';
                    b.className = 'chip';
                    b.innerHTML = '<b></b><span></span>';
                    b.querySelector('b').textContent = n;
                    b.querySelector('span').textContent = toIndo(n);
                    b.addEventListener('click', function() {
                        speak(toIndo(n));
                    });
                    elChart.appendChild(b);
                });
            }

            function refillQueue() {
                var items = state.set.build();
                for (var i = items.length - 1; i > 0; i--) {
                    var j = Math.floor(Math.random() * (i + 1));
                    var t = items[i];
                    items[i] = items[j];
                    items[j] = t;
                }
                if (items.length > 1 && items[0] === state.current) items.push(items.shift());
                state.queue = items;
            }

            function nextCard() {
                if (!state.queue.length) refillQueue();
                state.current = state.queue.shift();
                state.solved = false;
                state.hints = 0;
                state.tries = 0;

                clearPending();
                elNumeral.textContent = state.current;
                elWord.textContent = toIndo(state.current);
                elWord.className = 'display word blank';
                elAnswer.value = '';
                elAnswer.className = '';
                elFeedback.textContent = '';
                elFeedback.className = 'feedback';
                elCheck.textContent = 'Check';
                elHint.textContent = 'Hint';
                elHint.disabled = false;
                elAnswer.focus();
            }

            function clearPending() {
                if (state.pending) {
                    clearTimeout(state.pending);
                    state.pending = null;
                }
                state.advance = null;
            }

            function reveal(solvedByUser) {
                state.solved = true;
                elWord.className = 'display word' + (solvedByUser ? ' solved' : '');
                elCheck.textContent = 'Next number';
                elHint.disabled = true;
            }

            function advanceAfterWin() {
                clearPending();
                var done = false;
                var go = function() {
                    if (done) return;
                    done = true;
                    clearPending();
                    nextCard();
                };
                state.advance = go;
                state.pending = setTimeout(go, 2800);
                setTimeout(function() {
                    if (done) return;
                    speak(toIndo(state.current), 0.82, function() {
                        if (!done) state.pending = setTimeout(go, 500);
                    });
                    if (!('speechSynthesis' in window)) setTimeout(go, 600);
                }, 340);
            }

            function markRight() {
                playWin();
                state.streak++;
                elStreak.textContent = state.streak;
                if (state.hints === 0) {
                    state.filled++;
                    if (state.filled >= 10) {
                        state.filled = 0;
                        state.barNo++;
                        elFeedback.textContent = 'Bar ' + (state.barNo - 1) + ' finished. Ten in a row, clean.';
                    }
                    drawBar();
                }
                if (!elFeedback.textContent) {
                    elFeedback.textContent = state.hints ? 'Correct, with a hint.' : 'Correct.';
                }
                elFeedback.className = 'feedback ok';
                elAnswer.className = 'right';
                reveal(true);
                advanceAfterWin();
            }

            function markWrong() {
                playFail();
                state.tries++;
                state.streak = 0;
                elStreak.textContent = '0';
                elAnswer.classList.remove('wrong');
                void elAnswer.offsetWidth;
                elAnswer.classList.add('wrong');
                elFeedback.className = 'feedback no';
                elFeedback.textContent = state.tries >= 2 ?
                    'Still not it. Press hint, or listen again.' :
                    'Not that one. Check your spelling and try again.';
                elAnswer.select();
            }

            function onCheck() {
                if (state.solved) {
                    nextCard();
                    return;
                }
                var val = elAnswer.value;
                if (!normalise(val)) {
                    elFeedback.className = 'feedback no';
                    elFeedback.textContent = 'Type the word first.';
                    elAnswer.focus();
                    return;
                }
                if (isCorrect(val, toIndo(state.current))) markRight();
                else markWrong();
            }

            function onHint() {
                if (state.solved) return;
                state.hints++;
                var answer = toIndo(state.current);
                if (state.hints === 1) {
                    var masked = answer.split(' ').map(function(w) {
                        return w.charAt(0) + ' ' + new Array(w.length).join('_ ').trim();
                    }).join('   ');
                    elFeedback.className = 'feedback';
                    elFeedback.textContent = masked;
                    elAnswer.focus();
                } else {
                    state.streak = 0;
                    elStreak.textContent = '0';
                    elFeedback.className = 'feedback';
                    elFeedback.textContent = 'The answer is "' + answer + '". Type it once to lock it in.';
                    elAnswer.value = '';
                    elWord.className = 'display word';
                    elHint.disabled = true;
                    elAnswer.focus();
                }
            }

            function onSkip() {
                if (!state.solved) {
                    state.streak = 0;
                    elStreak.textContent = '0';
                }
                nextCard();
            }

            elCheck.addEventListener('click', onCheck);
            elHint.addEventListener('click', onHint);
            elSkip.addEventListener('click', onSkip);
            elHear.addEventListener('click', function() {
                speak(toIndo(state.current));
            });

            elMute.addEventListener('click', function() {
                muted = !muted;
                elMute.textContent = muted ? 'Sound off' : 'Sound on';
                elMute.setAttribute('aria-pressed', String(muted));
                elMute.title = muted ? 'Turn sound effects on' : 'Turn sound effects off';
                if (!muted) playWin();
            });

            elAnswer.addEventListener('keydown', function(e) {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    onCheck();
                }
            });
            elAnswer.addEventListener('input', function() {
                elAnswer.classList.remove('wrong');
            });

            document.addEventListener('keydown', function(e) {
                if (e.key === 'Enter' && state.solved && document.activeElement !== elAnswer) {
                    e.preventDefault();
                    nextCard();
                }
            });

            elChartToggle.addEventListener('click', function() {
                var open = elChart.hasAttribute('hidden');
                if (open) {
                    elChart.removeAttribute('hidden');
                } else {
                    elChart.setAttribute('hidden', '');
                }
                elChartToggle.setAttribute('aria-expanded', String(open));
            });

            drawSets();
            drawBar();
            drawChart();
            nextCard();
        })();
    </script>

</body>
</html>