Memrise card template [support thread]

The code contained the following script (it allows the Advanced Answer Sounds add-on, which I had previously installed, to play a sound when selecting “again/good,” so I wondered if it was possible to adjust this script so that instead, a sound would be played depending on whether the answer was correct or not). <script> // Trigger Advanced Answer Sounds by simulating button clicks function triggerAnswerSound(ease) { const button = document.querySelector(button[data-ease=“${ease}”]); if (button) { button.click(); console.log(Triggered sound for ease: ${ease}); } else { console.log("Answer button not found - using fallback"); // Fallback to original pycmd if buttons are missing if (platform === 'desk') { pycmd(ease${ease}); } else if (platform === 'android') { window[buttonAnswerEase${ease}`]();
} else if (platform === ‘ankiweb’) {
awRate(ease);
}
}
}

// Override autorate functions to trigger sounds
function autorateAgain() {
flipBtn.onclick = null;
triggerAnswerSound(1);
console.log(“:red_circle: autorated ‘again’ with sound”);
}

function autorateGood() {
flipBtn.onclick = null;
triggerAnswerSound(3);
console.log(“:green_circle: autorated ‘good’ with sound”);
}

It looks like you have some modified version of the template, not the original. I only recognize parts of the code here, but anything related to the sounds was written by someone else. Did you get this in a shared deck somewhere?

I suppose you could remove triggerAnswerSound(1); and triggerAnswerSound(3); from the functions above, and then call one of them from the “user posterior scripts” section depending on whether #backwrap element has .correct class (preferably on a small timeout). But that wouldn’t affect which sound is played (only move its timing from the next-card flip to the answer-submission flip), because answer auto rating is already determined by the same condition: cards are rated “good” if the answer is correct and “again” when it is incorrect. Unless this was also altered in the version of the template you have, of course.

Hi, everything going great so far. I just want to implement alternative answers for some questions. How can I enable displaying and selecting alternative answers? I know that i can type in the answer field something such as: hi hello hey.. and then select hello and hey and use the format as alternative option. When I type these they get accepted and show as alternative, but in the case of multiple choice they do not display as possible answers. does: class="mem-question no-alts memblob have anything to do with this?

1 Like

Multiple choices ignore alternative answers, that’s how they work on Memrise.

If you only want to add alternatives from a card to its own choices when a multiple-choice card is reviewed, this can be done with a bit of javascript. The card will still function as a single-choice test, in which any single correct answer will be accepted as good.

If, on the other hand, you’d like to make alternatives from one card appear on other cards, this will require more adjustments. The “Fill Choices” function ignores parts marked as alternatives as well (I plan to make them being appended as an option, but it’s not a top priority among the other todos). But there are several potential workarounds, depending on your card-making workflow:

  1. Make all the cards, placing the alternative answers separated by the pipe character in the main field without any other formatting. Then use “Fill Choices” to generate all multiple-choice cards. Only after that, remove the pipe character separating the main answer from the alternatives and apply the alts formatting.
  2. Make a separate field in your template for alternatives and store them there without any formatting. Use “Fill Choices” twice, first generating them from the main field, then appending choices from the alternatives field. This will also require you to modify the card template, adding the alternatives field and the proper formatting to every place the main field is mentioned.
    This way is more laborious to set up, but will allow adding new cards and appending their answers as choices to older cards in the future.
  3. Using any means available to you, make a single list of all potential choices you want to get on all cards as plain text (separated with the pipe character), and then bulk insert it into choices fields on all notes, using other addons, such as Advanced Copy Fields.

I can provide more detailed instructions on either of the options, as usual.


No, the “no-alts” class only determines which sections of the card template show only the main content of a field, omitting the alternatives. It doesn’t affect anything functional.

I downloaded this template from this website: https://ankiweb.net/shared/info/510199145. I didn’t think it was a modified version. But my problem is that I need to reproduce the sound of correct/wrong answers instead of again/good, and I can’t do it. Can I send you the script so you can tell me what needs to be replaced to achieve the desired result?

Thanks. Since this is a small set of cards with few alternative answers, I resorted to use the third option: created a list it notepad and then used the batch edit add on to add it to the choices for text answers field. The other workflows seem pretty handy though, especially the first two when creating from zero, so will keep them in mind. I was also experimenting with adding alt tags to the graphics I used, so that they can be described by screen readers and was pleasantly delighted to find out that they got copied over in the html for the multiple choice graphical answers when using the fill choices function. Since the edit fields really don’t work well in qt, at least the ones containing embedded graphics and the pop up html editor I had to create a separate field with the image descriptions (that will not be shown anywhere of course) and then tried my best at python to code a rudimentary add on that examined the html for the images field (would skip if it did not have an image) and then insert the alt tag on the field that contains the graphic. Made another function that clears that as well just in case something goes wrong or I would need to replace the tag, but all has worked fine, thankfully.

1 Like

This can’t be it. The respective piece of code there looks like this:

...
function autorateAgain() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease1');
		} else if (platform === 'android') {
			buttonAnswerEase1();
		} else if (platform === 'ankiweb') {
			awRate(1);
		}
		console.log("🔴 autorated 'again'");
}
function autorateGood() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease3');
		} else if (platform === 'android') {
			buttonAnswerEase3();
		} else if (platform === 'ankiweb') {
			awRate(3);
		}
		console.log("🟢 autorated 'good'");
}
...

Which is different from what you provided


I can take a look and will assist with what I can, but I cannot guarantee detailed support for the code written by someone else, for obvious reasons. If you recall where you get your version from, maybe the creator of that piece can be of more help to you.

I also don’t really understand what you mean by the “again/good” sounds and the “correct/wrong answers” sounds.

Yes, it is, but I sent one part of the code and you sent another, but the one you sent is in my code, which I downloaded from this site https://ankiweb.net/shared/info/510199145. The complete code looks like this, I just added a few new fields and minimally changed the design

<hr id="answer" class="sys">
<div id="backwrap" class="frontside">
	{{FrontSide}}
	<button id="mem-flip" class="sys">flip</button>
	<div class="card-content back">  

		<div class="mem-alert"></div>   

		<div class="mem-field">
			<label>Learnable</label>
			<h2>{{Learnable}}</h2>
			<span id="spelldiff" class=""></span>
		</div>
<!-- Added Transcription field here -->
		{{#Transcription}}
		<div style="margin-bottom: 15px; text-align: left; text-transform: lowercase;">
			{{Transcription}}
		</div>
		{{/Transcription}}
		<div class="mem-field no-alts">
			<label>Definition</label>
			<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
				<h3>{{Definition}}</h3>
				{{#Part of speech}}
				<div style="font-size: 14px; background-color: #e0e0e0; color: #333;
							padding: 4px 10px; border-radius: 12px; font-weight: bold; white-space: nowrap;">
					{{Part of speech}}
				</div>
				{{/Part of speech}}
			</div>
		</div>








<!-- Переміщене поле Picture -->
<div class="picture-container" style="display: flex; justify-content: left ; margin-bottom: 11px;">
    {{Picture}}
</div>
<!-- Стилі -->
<style>



.picture-container img {
    width: 400px;
    height: auto;
    max-height: 250px;
    object-fit: cover;
    border: none;
    border-radius: 16px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}


</style>
		<div class="sep"></div>

		{{#Extra}}
			<div class="mem-field no-alts">
				<label>Example</label>
				<h4>{{Extra}}</h4>      
			</div>
		{{/Extra}}
		{{#Extra 2}}
			<div class="mem-field no-alts">
				<label>Use</label>
				<h4>{{Extra 2}}</h4>
			</div>
		{{/Extra 2}}
		{{#Audio}}
			<div class="mem-field no-alts">
				<label>Audio</label>
				<h4>{{Audio}}</h4>
			</div>
		{{/Audio}}

	</div>	
</div>


<!-- Synonym під Mnemonic без проміжку -->
{{#Synonym}}
<div style="position: absolute; top: calc(80% + 8px); right: 52px; background-color: #3a3a3a;
            border-radius: 10px; padding: 8px 14px; font-size: 14px; font-weight: bold; max-width: 30ch;
            text-align: left; white-space: normal; word-wrap: break-word; color: #f0f0f0;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);">
    <strong style="display: block; margin-bottom: 6px;">Synonyms:</strong>
    <span style="white-space: pre-wrap; line-height: 1.3;">{{Synonym}}</span>
</div>
{{/Synonym}}

<!-- -------------------⚙️ user prior scripts ⚙️------------------ -->
<script>
  //place your scripts here
alwaysShowInfo = true;

</script>

<!-- ---------------------  template scripts  --------------------- -->
<!--
This section (up until the line containing "End of code by Eltaurus") is part of the Anki Card Type template.
Source: github.com/Eltaurus-Lt/Anki-Card-Templates

Copyright (C) 2023-2025 Eltaurus
Contact: 
    Email: Eltaurus@inbox.lt
    GitHub: github.com/Eltaurus-Lt
    Anki Forums: forums.ankiweb.net/u/Eltaurus

This template is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This template is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this template. If not, see <http://www.gnu.org/licenses/>.

You are free to use this template to create your own Anki cards and decks, modify the code, and openly share the derivative works, provided that this copyright notice and similar notices in other parts remain intact (as covered by section 7b of the GPLv3 license). 
Clarification: The copyright in this notice applies only to the above-stated section of the code. In particular, it does not extend to data contained within fields of Anki cards or any media files included in Anki decks created using this template. It also does not cover any scripts or HTML code that may be added to this HTML file (Back Template screen) by creators of derivative cards/templates. Creators are encouraged to add their own copyright statements alongside their code in a similar fashion.
-->

<script>
//generate random page id
pid = Array.from({length:16}, () => String.fromCharCode(Math.floor(Math.random() * 94) + 33)).join('');
//console.log("pageid: ", pid);
</script>

<script>
//Ratcliff-Obershelp
function stringDiff(s1, s2) {
    const n = s1.length;
    const m = s2.length;

    // Matrix of contiguous LCS lengths
    const M = Array.from({length: n + 1}, () => Array(m + 1).fill(0)); 
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (s1[i] === s2[j]) {
                M[i+1][j+1] = M[i][j] + 1;
            }
        }
    }

    function minorLCS(n1, n2, m1, m2) {
        let max = 0;
        let pos;
        for (let i = n1; i <= n2; i++) {
            for (let j = m1; j <= m2; j++) {
                const Mij = Math.min(M[i][j], j - m1 + 1, i - n1 + 1);
                if (Mij > max) {
                    max = Mij;
                    pos = [i, j];
                }
            }
        }
        return { "length": max, pos }
    }
  
    function Diff(n1, n2, m1, m2) {
      const LCS = minorLCS(n1, n2, m1, m2);
      const length0 = LCS.length;
      
      if (length0 === 0) {
        const diff1 = s1.substring(n1 - 1, n2);
        const diff2 = s2.substring(m1 - 1, m2);
        diff = '';
        if (diff2) {
          diff += '<span class="typeBad">' + htmlEscape(diff2) + '</span>';
        }
        if (diff1) {
          diff += '<span class="typeMissed">' + htmlEscape(diff1) + '</span>';
        }
        return {
          diff,
          CSlength: 0
        };
      } else {  
        const n0 = LCS.pos[0];
        const m0 = LCS.pos[1];
        const D1 = Diff(n1, n0 - length0, m1, m0 - length0);
        const D2 = Diff(n0 + 1, n2, m0 + 1, m2);
        const diff0 = '<span class="typeGood">' + htmlEscape(s1.substring(n0 - length0, n0)) + '</span>';
        return {
          diff: D1.diff + diff0 + D2.diff,
          CSlength: D1.CSlength + length0 + D2.CSlength
        }
      }
    }
  
    const D = Diff(1, n, 1, m);
    // classic similarity score: not typing a letter yields higher score than typing it incorrectly => discourages trying, not ideal
    //const score = 2 * D.CSlength / (n + m); 
    // corrected score: typing wrong letter yields the same score as skipping it
    const score = D.CSlength / Math.max(m, n);

    return { score, "CSlength": D.CSlength, "diff": D.diff };
}
</script>

<script>
//get user answer
userAns = sessionStorage.getItem("userAnswer");
if (userAns === null) {
	document.getElementsByClassName("mem-alert")[0].innerText = "#error loading answer!";
	userAns = '';
} else if (userAns.trim()) {
  if (Qmode === "tapping") {
    pressedSequence = userAns.split('|');
    userAns = pressedSequence.map(N => keys[N - 1]).join(' ');
  }
  if (Qmode === "mchoice") {
	  document.getElementsByClassName("mem-alert")[0].innerHTML = userAns; //text formatting, images and audio + latex in mcq
  } else if (!window.isMathJax) {
    setTimeout(()=>document.getElementsByClassName("mem-alert")[0].innerText = userAns, 100); // "a<b" and "b>a" in plain text
  } else {
    MJconvert(htmlEscape(userAns)).then(res => document.getElementsByClassName("mem-alert")[0].innerHTML = res);
  }
}
if (Qmode === "tapping" && userAns) { //pressedSequence can remain from previous cards
  pressedSequence.forEach((N) => tapKey(N));
}
if (typeAns?.tagName === 'INPUT') {
	typeAns.value = userAns;
	typeAns.disabled = true;
} else if (typeAns) {
	typeAns.innerHTML = '<span>' + htmlEscape(userAns) + '</span>'; //backward compatibility with stock Anki typing
}

console.log(`user answer: ${userAns} | correct answer: ${corrAns}`);
</script>

<script>
//answer comparison rules
function rmEnc(s) {
	//removes parts enclosed in the innermost parentheses
	return s.replace(/\x28[^()]*\x29/g, '');
}
function rmEncAll(s) {
	//remove everything between first and last brackets
	return s.replace(/\x28.*\x29/g, '');
}
function rmBrac(s) {
	//removes parentheses, keeping the contents
	return s.replace(/[()]/g, '');
}
function rmPunc(s) {
	//removes other punctuation
	return s.replace(/[.,\/#?!$%\^&\'"*;:{}=\-_`~ …〜~-。、・?!@#$%^&*()]/g, '');
}
function rmSpaces(s) {
	//removes spaces from the start and the end, replaces japanese and non-breaking spaces, removes repeated spaces
	return ansCleanUp(s).replace(/ /g, ' ').replace(/\s+/g, ' ');
}
function cfStrings(sQ, sA) {
 //unify case and remove punctuation
	var sq = rmSpaces(rmPunc(sQ.toLowerCase()));
	var sa = rmSpaces(rmPunc(sA.toLowerCase()));

	if (sq == sa) {
		return true;
	}
	if (rmSpaces(rmEnc(rmEnc(sq))) == sa) {
		return true;
	}
	if (rmSpaces(rmBrac(sq)) == sa) {
		return true;
	}
	return false;
}

//A (B) -> {〃, A, A B}
//A; B -> {〃, A, B}
//A; B (C) -> {〃, A, B, B C, B (C)}
//todo?A (B; C) -> {〃, A B, A C}

// grade answer
diff = "";
console.log(`altertnatives: ${allAlts.join(' | ')}`);

async function setCorrectClass() {
  if (!userAns.trim()) return;
  if (Qmode === "tapping") {
	//tapping (any kind)
    if (preTokenize(corrAns).replaceAll(" "," ") === userAns) {
      wrap.classList.add('correct');
    }
    return
  }
	if (!window.isMathJax) {
//text cf
		if (Qmode === "mchoice") {
			keyboardButtons.forEach(btn => {
				if (btn.innerHTML === userAns) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			})

	//type-in
		} else if (cfStrings(corrAns, userAns)) {
			wrap.classList.add('correct');
		} else if (allAlts.map((altAns) => cfStrings(altAns, userAns)).includes(true)) {
			wrap.classList.add('correct');
		}
	} else {
//latex cf
	userAns = MJunwrap(userAns);
	corrAns = MJunwrap(corrAns);
		if (Qmode === "mchoice") {
			const btnPromises = [...keyboardButtons].map(async btn => {
				if (btn.innerHTML === userAns || (await cfWithMathJax(btn.innerHTML, userAns))) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			});
			await Promise.all(btnPromises);

	//type-in
		} else if (corrAns === userAns) {
			wrap.classList.add('correct');
		} else {
			const altPromises = allAlts.map(async (altAns) => await cfWithMathJax(htmlEscape(altAns), htmlEscape(userAns)));
			
			await Promise.all(altPromises).then(results => {
				if (results.includes(true)) {
					wrap.classList.add('correct');
				}
			});
		}
	}
}
setCorrectClass().then(() => {
	if (wrap.classList.contains('correct')) {
		highlightGood();
	} else {
		highlightAgain();
		if (Qmode === "mchoice") {
				wrap.classList.add('wrong');
		} else {
	//type-in or tapping
			diff = stringDiff((Qmode !== "tapping") ? corrAns : preTokenize(corrAns).replaceAll(" "," "), userAns);
			if (diff.score > 0.67) {
				wrap.classList.add('soclose');
			} else {
				wrap.classList.add('wrong');
			}
			//spelling diffs
			const spellDiff = document.getElementById('spelldiff');
			if (spellDiff && userAns.trim()) {
				spellDiff.innerHTML = diff.diff;
			}
		}
	}
});

//timeout flip
infoOnCorrect = !!window.alwaysShowInfo; //without this redef impossible to reliably clear the hook if rated with Anki buttons
delete window.alwaysShowInfo;

flipBtn = document.getElementById('mem-flip');
function MemFlip(toInfo = false) {
	try {clearTimeout(activeTimeout)} catch (err) {};

	if (wrap.classList.contains("correct") && !toInfo && !infoOnCorrect && platform !== 'ios') {
		autorateGood();
	} else {
		putOnCD();

		wrap.classList.add("backside");
		wrap.classList.remove("frontside");
		setTimeout(()=>window.scrollTo(0, 0), 1);
		flipBtn.onclick = ()=>{
			if (wrap.classList.contains("correct")) {
				autorateGood();
			} else if (wrap.classList.contains("wrong") || wrap.classList.contains("soclose")) {
				autorateAgain();
			} else {
				console.log("the answer is not yet evaluated");
			}
		}
	}
}
flipBtn.onclick = MemFlip;

activeTimeout = setTimeout((pid0)=>{
//	console.log(pid, "|", pid0);
	if (pid !== pid0) return;
	MemFlip();
}, Math.round((window.flipDelay || 1.5) * 1000), pid);
delete window.flipDelay;

//Submit
function highlightAgain() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[0].classList.add('preselected');
			ankiwebButtons[0].focus();
			ankiwebButtons[0].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function highlightGood() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[2].classList.add('preselected');
			ankiwebButtons[2].focus();
			ankiwebButtons[2].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function autorateAgain() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease1');
		} else if (platform === 'android') {
			buttonAnswerEase1();
		} else if (platform === 'ankiweb') {
			awRate(1);
		}
		console.log("🔴 autorated 'again'");
}
function autorateGood() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease3');
		} else if (platform === 'android') {
			buttonAnswerEase3();
		} else if (platform === 'ankiweb') {
			awRate(3);
		}
		console.log("🟢 autorated 'good'");
}
</script>

<script>
//embedding backside audio buttons (android)
embeddedAudiosBack = [...document.querySelectorAll('audio:not(.off)')];
embeddedAudiosBack.forEach((audioL, i) => {
  const replayButtonHTML = `
    <a class="replay-button soundLink embedded" onclick="replayEmbedded(${i + embeddedAudios.length}, this)" href="../#">
      <svg class="playImage" viewBox="0 0 64 64" version="1.1">
        <circle cx="32" cy="32"></circle>
        <path></path>
      </svg>
    </a>
  `;
  
  const tempL = document.createElement('div');
  tempL.innerHTML = replayButtonHTML.trim();
  
  audioL.parentNode.insertBefore(tempL.firstChild, audioL.nextSibling);

  //move audio tag outside
  cardContF.appendChild(audioL);
  audioL.classList.add('off');
});
embeddedAudios = embeddedAudios.concat(embeddedAudiosBack);

</script>
<script>
//Audio buttons animation
document.querySelectorAll('.card-content.back a.replay-button').forEach((a) => {
	if (a.classList.contains("embedded")) return;
	a.addEventListener("click", ()=>audioAnimation(a));
});
</script>

<!-- End of code by Eltaurus -->

<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->



<script>
  // Trigger Advanced Answer Sounds by simulating button clicks
  function triggerAnswerSound(ease) {
    const button = document.querySelector(`button[data-ease="${ease}"]`);
    if (button) {
      button.click();
      console.log(`Triggered sound for ease: ${ease}`);
    } else {
      console.log("Answer button not found - using fallback");
      // Fallback to original pycmd if buttons are missing
      if (platform === 'desk') {
        pycmd(`ease${ease}`);
      } else if (platform === 'android') {
        window[`buttonAnswerEase${ease}`]();
      } else if (platform === 'ankiweb') {
        awRate(ease);
      }
    }
  }

  // Override autorate functions to trigger sounds
  function autorateAgain() {
    flipBtn.onclick = null;
    triggerAnswerSound(1);
    console.log("🔴 autorated 'again' with sound");
  }

  function autorateGood() {
    flipBtn.onclick = null;
    triggerAnswerSound(3);
    console.log("🟢 autorated 'good' with sound");
  }
</script>



<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->
<script>
  // Handle Ctrl key for Hint button - one character per press
  document.addEventListener('keydown', function(e) {
    // Check if Ctrl is pressed alone
    if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.key === 'Control') {
      const hintButton = document.getElementById('HintButton');
      if (hintButton) {
        // Get current state
        const userInput = document.querySelector('input#typeans')?.value || '';
        const correctAnswer = "{{Learnable}}".toLowerCase();
        
        // Find next character to reveal
        let nextCharIndex = 0;
        for (let i = 0; i < Math.min(userInput.length, correctAnswer.length); i++) {
          if (userInput[i] !== correctAnswer[i]) {
            nextCharIndex = i;
            break;
          }
          nextCharIndex = i + 1;
        }
        
        // Add only next character if available
        if (nextCharIndex < correctAnswer.length) {
          const newInput = correctAnswer.substring(0, nextCharIndex + 1);
          document.querySelector('input#typeans').value = newInput;
          storeAnswer(newInput);
        }
        
        e.preventDefault();
      }
    }
  });
</script>






{{tts en_US voices=AwesomeTTS:Learnable}}

Regarding the sound of the “again/good” buttons, it means that they are played using a script in the code, and I also use applications found on the website https://ankiweb.net/shared/info/1167194350, and I need these sounds not to be played when “again/good” is selected, but to be played depending on whether the answer was correct or incorrect. For greater clarity, I can send a video showing how this works in practice.

I’m not sure if you meant to link the specific page or the AnkiWeb site in general. The template available via the link has a different code for the backside of a card. Here is its full version for comparison:

Original backside code
<hr id="answer" class="sys">
<div id="backwrap" class="frontside">
	{{FrontSide}}
	<button id="mem-flip" class="sys">flip</button>
	<div class="card-content back">  

		<div class="mem-alert"></div>   

		<div class="mem-field">
			<label>Learnable</label>
			<h2>{{Learnable}}</h2>
			<span id="spelldiff" class=""></span>
		</div>

		<div class="mem-field no-alts">
			<label>Definition</label>
			<h3>{{Definition}}</h3>
		</div>

		<div class="sep"></div>

		{{#Extra}}
			<div class="mem-field no-alts">
				<label>Extra</label>
				<h4>{{Extra}}</h4>      
			</div>
		{{/Extra}}
		{{#Extra 2}}
			<div class="mem-field no-alts">
				<label>Extra 2</label>
				<h4>{{Extra 2}}</h4>
			</div>
		{{/Extra 2}}
		{{#Audio}}
			<div class="mem-field no-alts">
				<label>Audio</label>
				<h4>{{Audio}}</h4>
			</div>
		{{/Audio}}

	</div>	
</div>




<!-- -------------------⚙️ user prior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>

<!-- ---------------------  template scripts  --------------------- -->
<!--
This section (up until the line containing "End of code by Eltaurus") is part of the Anki Card Type template.
Source: github.com/Eltaurus-Lt/Anki-Card-Templates

Copyright (C) 2023-2025 Eltaurus
Contact: 
    Email: Eltaurus@inbox.lt
    GitHub: github.com/Eltaurus-Lt
    Anki Forums: forums.ankiweb.net/u/Eltaurus

This template is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This template is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this template. If not, see <http://www.gnu.org/licenses/>.

You are free to use this template to create your own Anki cards and decks, modify the code, and openly share the derivative works, provided that this copyright notice and similar notices in other parts remain intact (as covered by section 7b of the GPLv3 license). 
Clarification: The copyright in this notice applies only to the above-stated section of the code. In particular, it does not extend to data contained within fields of Anki cards or any media files included in Anki decks created using this template. It also does not cover any scripts or HTML code that may be added to this HTML file (Back Template screen) by creators of derivative cards/templates. Creators are encouraged to add their own copyright statements alongside their code in a similar fashion.
-->

<script>
//generate random page id
pid = Array.from({length:16}, () => String.fromCharCode(Math.floor(Math.random() * 94) + 33)).join('');
//console.log("pageid: ", pid);
</script>

<script>
//Ratcliff-Obershelp
function stringDiff(s1, s2) {
    const n = s1.length;
    const m = s2.length;

    // Matrix of contiguous LCS lengths
    const M = Array.from({length: n + 1}, () => Array(m + 1).fill(0)); 
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (s1[i] === s2[j]) {
                M[i+1][j+1] = M[i][j] + 1;
            }
        }
    }

    function minorLCS(n1, n2, m1, m2) {
        let max = 0;
        let pos;
        for (let i = n1; i <= n2; i++) {
            for (let j = m1; j <= m2; j++) {
                const Mij = Math.min(M[i][j], j - m1 + 1, i - n1 + 1);
                if (Mij > max) {
                    max = Mij;
                    pos = [i, j];
                }
            }
        }
        return { "length": max, pos }
    }
  
    function Diff(n1, n2, m1, m2) {
      const LCS = minorLCS(n1, n2, m1, m2);
      const length0 = LCS.length;
      
      if (length0 === 0) {
        const diff1 = s1.substring(n1 - 1, n2);
        const diff2 = s2.substring(m1 - 1, m2);
        diff = '';
        if (diff2) {
          diff += '<span class="typeBad">' + htmlEscape(diff2) + '</span>';
        }
        if (diff1) {
          diff += '<span class="typeMissed">' + htmlEscape(diff1) + '</span>';
        }
        return {
          diff,
          CSlength: 0
        };
      } else {  
        const n0 = LCS.pos[0];
        const m0 = LCS.pos[1];
        const D1 = Diff(n1, n0 - length0, m1, m0 - length0);
        const D2 = Diff(n0 + 1, n2, m0 + 1, m2);
        const diff0 = '<span class="typeGood">' + htmlEscape(s1.substring(n0 - length0, n0)) + '</span>';
        return {
          diff: D1.diff + diff0 + D2.diff,
          CSlength: D1.CSlength + length0 + D2.CSlength
        }
      }
    }
  
    const D = Diff(1, n, 1, m);
    // classic similarity score: not typing a letter yields higher score than typing it incorrectly => discourages trying, not ideal
    //const score = 2 * D.CSlength / (n + m); 
    // corrected score: typing wrong letter yields the same score as skipping it
    const score = D.CSlength / Math.max(m, n);

    return { score, "CSlength": D.CSlength, "diff": D.diff };
}
</script>

<script>
//get user answer
userAns = sessionStorage.getItem("userAnswer");
if (userAns === null) {
	document.getElementsByClassName("mem-alert")[0].innerText = "#error loading answer!";
	userAns = '';
} else if (userAns.trim()) {
  if (Qmode === "tapping") {
    pressedSequence = userAns.split('|');
    userAns = pressedSequence.map(N => keys[N - 1]).join(' ');
  }
  if (Qmode === "mchoice") {
	  document.getElementsByClassName("mem-alert")[0].innerHTML = userAns; //text formatting, images and audio + latex in mcq
  } else if (!window.isMathJax) {
    setTimeout(()=>document.getElementsByClassName("mem-alert")[0].innerText = userAns, 100); // "a<b" and "b>a" in plain text
  } else {
    MJconvert(htmlEscape(userAns)).then(res => document.getElementsByClassName("mem-alert")[0].innerHTML = res);
  }
}
if (Qmode === "tapping" && userAns) { //pressedSequence can remain from previous cards
  pressedSequence.forEach((N) => tapKey(N));
}
if (typeAns?.tagName === 'INPUT') {
	typeAns.value = userAns;
	typeAns.disabled = true;
} else if (typeAns) {
	typeAns.innerHTML = '<span>' + htmlEscape(userAns) + '</span>'; //backward compatibility with stock Anki typing
}

console.log(`user answer: ${userAns} | correct answer: ${corrAns}`);
</script>

<script>
//answer comparison rules
function rmEnc(s) {
	//removes parts enclosed in the innermost parentheses
	return s.replace(/\x28[^()]*\x29/g, '');
}
function rmEncAll(s) {
	//remove everything between first and last brackets
	return s.replace(/\x28.*\x29/g, '');
}
function rmBrac(s) {
	//removes parentheses, keeping the contents
	return s.replace(/[()]/g, '');
}
function rmPunc(s) {
	//removes other punctuation
	return s.replace(/[.,\/#?!$%\^&\'"*;:{}=\-_`~ …〜~-。、・?!@#$%^&*()]/g, '');
}
function rmSpaces(s) {
	//removes spaces from the start and the end, replaces japanese and non-breaking spaces, removes repeated spaces
	return ansCleanUp(s).replace(/ /g, ' ').replace(/\s+/g, ' ');
}
function cfStrings(sQ, sA) {
 //unify case and remove punctuation
	var sq = rmSpaces(rmPunc(sQ.toLowerCase()));
	var sa = rmSpaces(rmPunc(sA.toLowerCase()));

	if (sq == sa) {
		return true;
	}
	if (rmSpaces(rmEnc(rmEnc(sq))) == sa) {
		return true;
	}
	if (rmSpaces(rmBrac(sq)) == sa) {
		return true;
	}
	return false;
}

//A (B) -> {〃, A, A B}
//A; B -> {〃, A, B}
//A; B (C) -> {〃, A, B, B C, B (C)}
//todo?A (B; C) -> {〃, A B, A C}

// grade answer
diff = "";
console.log(`altertnatives: ${allAlts.join(' | ')}`);

async function setCorrectClass() {
  if (!userAns.trim()) return;
  if (Qmode === "tapping") {
	//tapping (any kind)
    if (preTokenize(corrAns).replaceAll(" "," ") === userAns) {
      wrap.classList.add('correct');
    }
    return
  }
	if (!window.isMathJax) {
//text cf
		if (Qmode === "mchoice") {
			keyboardButtons.forEach(btn => {
				if (btn.innerHTML === userAns) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			})

	//type-in
		} else if (cfStrings(corrAns, userAns)) {
			wrap.classList.add('correct');
		} else if (allAlts.map((altAns) => cfStrings(altAns, userAns)).includes(true)) {
			wrap.classList.add('correct');
		}
	} else {
//latex cf
	userAns = MJunwrap(userAns);
	corrAns = MJunwrap(corrAns);
		if (Qmode === "mchoice") {
			const btnPromises = [...keyboardButtons].map(async btn => {
				if (btn.innerHTML === userAns || (await cfWithMathJax(btn.innerHTML, userAns))) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			});
			await Promise.all(btnPromises);

	//type-in
		} else if (corrAns === userAns) {
			wrap.classList.add('correct');
		} else {
			const altPromises = allAlts.map(async (altAns) => await cfWithMathJax(htmlEscape(altAns), htmlEscape(userAns)));
			
			await Promise.all(altPromises).then(results => {
				if (results.includes(true)) {
					wrap.classList.add('correct');
				}
			});
		}
	}
}
setCorrectClass().then(() => {
	if (wrap.classList.contains('correct')) {
		highlightGood();
	} else {
		highlightAgain();
		if (Qmode === "mchoice") {
				wrap.classList.add('wrong');
		} else {
	//type-in or tapping
			diff = stringDiff((Qmode !== "tapping") ? corrAns : preTokenize(corrAns).replaceAll(" "," "), userAns);
			if (diff.score > 0.67) {
				wrap.classList.add('soclose');
			} else {
				wrap.classList.add('wrong');
			}
			//spelling diffs
			const spellDiff = document.getElementById('spelldiff');
			if (spellDiff && userAns.trim()) {
				spellDiff.innerHTML = diff.diff;
			}
		}
	}
});

//timeout flip
infoOnCorrect = !!window.alwaysShowInfo; //without this redef impossible to reliably clear the hook if rated with Anki buttons
delete window.alwaysShowInfo;

flipBtn = document.getElementById('mem-flip');
function MemFlip(toInfo = false) {
	try {clearTimeout(activeTimeout)} catch (err) {};

	if (wrap.classList.contains("correct") && !toInfo && !infoOnCorrect && platform !== 'ios') {
		autorateGood();
	} else {
		putOnCD();

		wrap.classList.add("backside");
		wrap.classList.remove("frontside");
		setTimeout(()=>window.scrollTo(0, 0), 1);
		flipBtn.onclick = ()=>{
			if (wrap.classList.contains("correct")) {
				autorateGood();
			} else if (wrap.classList.contains("wrong") || wrap.classList.contains("soclose")) {
				autorateAgain();
			} else {
				console.log("the answer is not yet evaluated");
			}
		}
	}
}
flipBtn.onclick = MemFlip;

activeTimeout = setTimeout((pid0)=>{
//	console.log(pid, "|", pid0);
	if (pid !== pid0) return;
	MemFlip();
}, Math.round((window.flipDelay || 1.5) * 1000), pid);
delete window.flipDelay;

//Submit
function highlightAgain() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[0].classList.add('preselected');
			ankiwebButtons[0].focus();
			ankiwebButtons[0].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function highlightGood() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[2].classList.add('preselected');
			ankiwebButtons[2].focus();
			ankiwebButtons[2].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function autorateAgain() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease1');
		} else if (platform === 'android') {
			buttonAnswerEase1();
		} else if (platform === 'ankiweb') {
			awRate(1);
		}
		console.log("🔴 autorated 'again'");
}
function autorateGood() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease3');
		} else if (platform === 'android') {
			buttonAnswerEase3();
		} else if (platform === 'ankiweb') {
			awRate(3);
		}
		console.log("🟢 autorated 'good'");
}
</script>

<script>
//embedding backside audio buttons (android)
embeddedAudiosBack = [...document.querySelectorAll('audio:not(.off)')];
embeddedAudiosBack.forEach((audioL, i) => {
  const replayButtonHTML = `
    <a class="replay-button soundLink embedded" onclick="replayEmbedded(${i + embeddedAudios.length}, this)" href="../#">
      <svg class="playImage" viewBox="0 0 64 64" version="1.1">
        <circle cx="32" cy="32"></circle>
        <path></path>
      </svg>
    </a>
  `;
  
  const tempL = document.createElement('div');
  tempL.innerHTML = replayButtonHTML.trim();
  
  audioL.parentNode.insertBefore(tempL.firstChild, audioL.nextSibling);

  //move audio tag outside
  cardContF.appendChild(audioL);
  audioL.classList.add('off');
});
embeddedAudios = embeddedAudios.concat(embeddedAudiosBack);

</script>
<script>
//Audio buttons animation
document.querySelectorAll('.card-content.back a.replay-button').forEach((a) => {
	if (a.classList.contains("embedded")) return;
	a.addEventListener("click", ()=>audioAnimation(a));
});
</script>

<!-- End of code by Eltaurus -->

<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>

In particular, everything past the line “End of code by Eltaurus” is left empty in the original, with the expectation that the space will be used by card authors for putting their scripts, adjusting the template for their specific purposes. The version you have seems to be one of such cases. There are many decks like that on AnkiWeb, but I’m not aware which one you got your script from. If you find the right deck, its page should have a “Contact Author” button, which can help you reach the true creator of the code you are interested in.


As far as I can tell, the functions redefined in that part do not do anything directly related to playing the sounds – they only emulate the presses of Anki rating buttons, which are then picked up by the answer sounds add-on, that, in turn, performs the real task at hand. I’m not sure why redefining those functions was necessary, because the original worked in the exact same way, but then again, I didn’t write the appended code, so I might be missing something.


I tried the add-on you linked, so I got an impression of how it works out of the box. What I don’t yet understand is what specifically you want to function differently. You are saying that you want “sounds not to be played when “again/good” is selected”, but “depending on whether the answer was correct or incorrect”. However, these two statements are not opposites of each other. As I explained earlier, for auto-rated answers the sounds should already be played “depending on whether the answer was correct or incorrect”. Assuming this part of your request is fulfilled by default, there is only a question of when the sounds are played, but you didn’t specify what the desired behavior in that aspect is.

I’ll describe the situation from the very beginning to make it clearer. I’m using an add-on (which I mentioned earlier). It used to automatically play a sound depending on whether the answer was correct or incorrect.

In addition to playing sounds for correct/incorrect answers, this add-on is also programmed to play sounds when clicking any of Anki’s default answer buttons (Again, Hard, Good, Easy).

In other words, it automatically played the appropriate sound both for the correctness of the answer and for the specific Anki button I pressed.

However, when I started using your template (or rather, as it turned out, not your original template), the add-on stopped playing the correct/incorrect answer sounds automatically — although it still works fine with other templates.

That means something in this template is interfering with the add-on’s ability to play those sounds. Despite that, the sounds still work when I press the default buttons (Again, Hard, Good, Easy).

I just need this add-on to work with this template the same way it does with the default one — that is, to automatically play the correct/incorrect answer sounds as before.

I understand it may be difficult for you to help me solve this issue just by looking at the script I sent, since it’s not originally yours. But if possible, could you take a look and see if there’s anything in your script in particular that might be preventing the add-on from functioning correctly?

1 Like

That clarified a general picture, thank you.

I assume you are exclusively referring to Anki typing cards? The add-on is probably searching for the standard Anki typing field on the back of a card, as I don’t see any other way it could assess the correctness of an answer, and playing the sound based on that.
Because the standard input is very limited in its functionality (it does not work on AnkiWeb, does not allow making multiple-choice cards, does not cooperate with the card’s on-screen keyboard on iphones, etc.), the template uses its own system, which a general-use add-on unlikely to be taking into account.

There are a few things you can try.

If you are fine with losing some of the functionality mentioned above, you can revert to the stock Anki typing by replacing the following line found on the front of the card:

<input id="typeans" type="text" inputmode="text" autocorrect="off" autocomplete="off" autocapitalize="off" spellcheck="false">

with the Anki’s default field replacement:

{{type:Learnable}}

The template is written in a way to have this as a possible fallback, although it wasn’t tested extensively.

If this modification causes any bugs, you can also try one of the older versions (if I remember correctly, v3.32 was the last one built on the stock Anki input). Naturally, this will come at a cost of all the functionality introduced in the later updates.

Alternatively, as I mentioned before, a bit of JS can be added to the “user posterior scripts” section to trigger a sound independently of any add-ons, based on the template card state.

It worked, the sound plays automatically, but the back of the card now looks like this…
Considering that the reverse card script remained unchanged. First, it analyzes whether the answer was correct or incorrect (screenshot 1), and then it is simply blank… (screenshot 2)



But it was supposed to be like this

Not sure what’s going on here. I tried reproducing it with the original template and also using your code for the backside of the card, but in both cases, everything looked just fine.
Can you share your full note type so that I can test it?

Sure but It’s just a template to which I still need to add the features I mentioned above

Front

{{#Learnable}}{{#Definition}}
<setting id="static_keys"></setting>
<setting id="random_keys"></setting>
<data id="correctAnswer">{{Learnable}}</data>
<data id="choices">{{Choices}}</data>

<div class="card-content front" theme="MemRise" mode="typing">
	<div class="overhead">
		<div class="mem-instruction" style="font-size: 18px; margin-bottom: 8px;">
			Type the correct answer
		</div>

		{{#Extra}}
			<div class="front-extra no-alts">
				<label>Extra</label>
				<span>{{Extra}}</span>
			</div>
		{{/Extra}}
	</div>
















<!-- Modified Definition section with Part of Speech -->
	<div class="mem-question no-alts memblob" style="font-size: 18px; line-height: 1.3;">
		<div style="margin: 1px auto 10px auto; display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; text-align: center; text-transform: lowercase;">
			<div style="font-weight: bold;">{{Definition}}</div>
			{{#Part of speech}}
			<div style="font-size: 14px; background-color: #e0e0e0; color: #333;
						padding: 4px 10px; border-radius: 12px; font-weight: bold; white-space: nowrap;">
				{{Part of speech}}
			</div>
			{{/Part of speech}}
		</div>
	</div>




<!-- Переміщене поле Picture -->
<div class="picture-container" style="display: flex; justify-content: center; margin-bottom: 1px;">
    {{Picture}}
</div>
<!-- Стилі -->
<style>



.picture-container img {
    width: 400px;
    height: auto;
    max-height: 250px;
    object-fit: cover;
    border: none;
    border-radius: 16px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}


</style>




	<div class="mem-typing" style="margin-top: 8px;">

	




		<label>Learnable</label>
		<input id="typeans" type="text" inputmode="text" autocorrect="off" autocomplete="off" autocapitalize="off" spellcheck="false">
	</div>
		       

	<timer class="off"></timer>

	<div id="scr-keyboard" class="">
		<div id="HintButton" class="membtn"><svg><path></path></svg>Hint</div>
	</div>



</div>
<a id="Lt" href="https://github.com/Eltaurus-Lt/Anki-Card-Templates" target="_blank"></a>
{{/Definition}}{{/Learnable}}


<!-- Synonym під Mnemonic без проміжку -->
{{#Synonym}}
<div style="position: absolute; top: calc(80% + 8px); right: 52px; background-color: #3a3a3a;
            border-radius: 10px; padding: 8px 14px; font-size: 14px; font-weight: bold; max-width: 30ch;
            text-align: left; white-space: normal; word-wrap: break-word; color: #f0f0f0;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);">
    <strong style="display: block; margin-bottom: 6px;">Synonyms:</strong>
    <span style="white-space: pre-wrap; line-height: 1.3;">{{Synonym}}</span>
</div>
{{/Synonym}}

<!-- -------------------⚙️ user prior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>

<!-- ---------------------  template scripts  --------------------- -->
<!--
This section (up until the line containing "End of code by Eltaurus") is part of the Anki Card Type template.
Source: github.com/Eltaurus-Lt/Anki-Card-Templates

Copyright (C) 2023-2025 Eltaurus
Contact: 
    Email: Eltaurus@inbox.lt
    GitHub: github.com/Eltaurus-Lt
    Anki Forums: forums.ankiweb.net/u/Eltaurus


<script>
//determine card side
wrap = document.getElementById('backwrap');
isFrontSide = !wrap;
if (isFrontSide) {
  console.log("------------- card -------------");
} else {
  console.log(" - - - - - -  back  - - - - - - ");
}
</script>

<script>
//determine input flags
cardContF = document.querySelector(".card-content.front");
Qmode = cardContF.getAttribute("mode") || (cardContF.classList.contains("mch") ? "mchoice" : "typing");
isMathJax = cardContF.classList.contains("eq");
</script>

<script>
//determine platform
platform = '';
if (!!document.getElementById('qa_box')) {
  platform = 'ankiweb';
} else if (!document.documentElement.classList.contains("mobile")) {
	platform = 'desk';
} else if (document.documentElement.classList.contains("android")) {
	//var jsApiContract = { version: "0.0.3", developer: "eltaurus@inbox.lt" };
	//var api = new AnkiDroidJS(jsApiContract);
	platform = 'android';
} else {
	platform = 'ios';
}
console.log("platform: ", platform);
</script>

<script>
//prevent ankiweb's default autorate with number keys
if (platform === 'ankiweb' && !window.awKeyBlocker) {
  awKeyBlocker = document.addEventListener("keyup", (event) => {
    if (!"1234".includes(event.key)) return;
    event.stopImmediatePropagation();
    event.preventDefault();
  }, true);
}
//prevent ankiweb from autofocusing "show answer" and rate buttons
if (platform === 'ankiweb' && !window.observer) {
  window.observer = new MutationObserver(() => {
    document.querySelectorAll('[autofocus]').forEach((L) => {
      L.removeAttribute('autofocus');
      L.blur();
    });
  });

  observer.observe(document.body, { childList: true, subtree: true });
};

//ankiweb rate function
function awRate(ease) {
	const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
	if (ankiwebButtons.length === 4) {
		ankiwebButtons[ease - 1].click();
	} else {
		console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
	}
}
</script>

<script>
//block key presses immediately after page loading
function putOnCD() {
  ongcd = true;
  setTimeout(()=>{ongcd = false}, 400);
}
if (isFrontSide) putOnCD();
</script>

<script>
function htmlEscape(string) {
  return string.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll("'", '&#39;').replaceAll('"', '&quot;');
}

function htmlUnEscape(string) {
  return string.replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&#39;', "'").replaceAll('&quot;', '"').replaceAll('&amp;', '&');
}

function ansCleanUp(ansString) {
  return ansString?.replaceAll("&nbsp;", " ")?.replaceAll(" \n", " ").replaceAll("\n ", " ").replaceAll("\n", " ")?.trim();
}

function preTokenize(ans) {
  return ans.replaceAll('?',' ?').replace(/\u0020+/g, ' ');
}
</script>

<script>
//elements
typeAns = document.getElementById('typeans');
tapAnsArea = document.querySelector(".mem-typing");
screenKeyboard = document.getElementById('scr-keyboard');
hintButton = document.getElementById('HintButton');
embeddedAudios = [...document.querySelectorAll('audio')];
</script>

<script>
function audioAnimation(aL) {
	if (!aL) return;
	aL.classList.remove('loading');
	aL.classList.remove('failed');
	document.querySelectorAll('.card-content a.replay-button').forEach((b) => {
		b.classList.remove('active');
	});
	aL.classList.add('active');
	aL.classList.remove('pulse');
	void aL.offsetHeight;
	aL.classList.add('pulse');
}

function replayEmbedded(i, aL, retry = 50) { //for embedded audio tags (ankiweb)
  if (!embeddedAudios[i]) return;
  embeddedAudios.forEach(audioL => audioL.pause());
  embeddedAudios[i].currentTime = 0;
  embeddedAudios[i].play().then(()=>{
    audioAnimation(aL);
  }).catch((err) => {
    if (err.name === "NotAllowedError" || err.message.includes("user didn't interact")) {
      console.warn('awaits interaction');

      const interactions = ["click", "keydown"];
      const handler = ()=> {
        console.log('interaction detected');
        interactions.forEach(evt => document.removeEventListener(evt, handler));
        replayEmbedded(i, aL, retry - 1);
      }
      interactions.forEach(event => {
          document.addEventListener(event, handler);
      });

    } else {
      console.warn(`playback error (${err})`);
      aL.classList.add('loading');
      if (retry > 0) {
        setTimeout(()=>replayEmbedded(i, aL, retry - 1), 100); 
      } else {
        console.warn('max retry attempts exceeded');
        aL.classList.remove('loading');
        aL.classList.add('failed');
      }
    }
  });
}

//embedding audio buttons
embeddedAudios.forEach((audioL, i) => {
  const replayButtonHTML = `
    <a class="replay-button soundLink embedded" onclick="replayEmbedded(${i}, this)" href="../#">
      <svg class="playImage" viewBox="0 0 64 64" version="1.1">
        <circle cx="32" cy="32"></circle>
        <path></path>
      </svg>
    </a>
  `;
  
  const tempL = document.createElement('div');
  tempL.innerHTML = replayButtonHTML.trim();
  
  audioL.parentNode.insertBefore(tempL.firstChild, audioL.nextSibling);

  //move audio tag outside
  cardContF.appendChild(audioL);
  audioL.classList.add('off');
});

</script>

<script>
//MathJax
function MJwrap(latexString) {
  if ((latexString.startsWith("\\\x28") && latexString.endsWith("\\\x29")) || !latexString) {
    return latexString;
  }
  return "\\\x28" + latexString + "\\\x29";
}

function MJunwrap(latexString) {
  if (latexString.startsWith("\\\x28") && latexString.endsWith("\\\x29")) {
    return latexString.slice(2, -2);
  }
  return latexString;
}

async function MJconvert(latexString) {
  if (!window.MathJax) {
    console.log("MathJax not found");
    return latexString;
  }
  if (latexString.includes("mjx-container")) {
    return latexString;
  }
  const latexL = document.createElement('div');
  latexL.innerHTML = MJwrap(latexString);
  await MathJax.typesetPromise([latexL]);

  return latexL.innerHTML;
}

async function cfWithMathJax(sQ, sA) {
  const mathJaxRegex = /<mjx-container[^>]*>/g;

  return sQ.replace(mathJaxRegex, '') === sA.replace(mathJaxRegex, '') || (await MJconvert(sQ)).replace(mathJaxRegex, '') === sA.replace(mathJaxRegex, '') || (await MJconvert(sQ)).replace(mathJaxRegex, '') === (await MJconvert(sA)).replace(mathJaxRegex, '');
}
</script>

<script>
function storeAnswer(ans = "") {
	if (!ans && typeAns) {ans = typeAns.value};
	sessionStorage.setItem("userAnswer", ans);
}

if (isFrontSide) {
	storeAnswer("");
}
</script>

<script>
//determine the correct answer
corrAnsL = document.getElementById('correctAnswer');
</script>
<script>
//cloze
clozes = corrAnsL.querySelectorAll(".cloze");
if (clozes.length > 0) {
  corrAnsL.innerHTML = [...clozes].map(L => L.getAttribute("data-cloze")).join(Qmode !== "tapping" ? ", " : " ");
  let inactCloze = corrAnsL.querySelector("span.cloze-inactive");
  while (inactCloze) { // remove inactive nested clozes
    const frag = document.createDocumentFragment();
    while (inactCloze.firstChild) { frag.appendChild(inactCloze.firstChild); }
    corrAnsL.replaceChild(frag, inactCloze);
    inactCloze = corrAnsL.querySelector("span.cloze-inactive");
  }
}
</script>
<script>
corrAns = ansCleanUp(Qmode === "tapping" ? corrAnsL?.innerHTML.replaceAll(" ", " ").replaceAll("&nbsp;", " ") : corrAnsL?.innerHTML) || ""; //including alts

//extract primary, excluding alts
try {
	const tempL = document.createElement('div');
	tempL.innerHTML = corrAns;
	tempL.querySelectorAll('[part="alt"], .alt').forEach((L) => {L.remove();});
	corrAns = (Qmode !== "mchoice") ? tempL.innerText : tempL.innerHTML;
	corrAns = corrAns.trim();
	tempL.remove();
} catch (err) {}

inlnAlts = corrAns.split(/[;;]/).map(ansCleanUp);

hintAns = ansCleanUp(inlnAlts[0]);
if (isMathJax) {
	hintAns = MJunwrap(hintAns);
	if (Qmode === "mchoice") {
		inlnAlts = [corrAns]; //otherwise is split at ';' in '&gt;', '&lt;', etc.
	}
}

//determine alt answers
partAlts = [];
try {
	const altsString = [...corrAnsL.querySelectorAll('[part="alt"], .alt')].map(L => L.innerText).join('|');
	partAlts = altsString ? altsString.split('|').map(ansCleanUp) : [];
} catch (err) {}

allAlts = [...inlnAlts, ...partAlts];
</script>

<script>
//consts
keysString = document.getElementById('static_keys')?.innerText || "";
fillerString = document.getElementById('random_keys')?.innerText || "";
choices = document.getElementById('choices')?.innerHTML.split('|').map(ansCleanUp) || '';
</script>

<script>
//keyboard navigation
tabSelected = null;
document.onkeyup = function (e) {
	var ev = window.event || e;
	
	if (ev.key === 'Tab') {
		tabSelected = document.activeElement;
	}
}

document.onkeydown = function (e) {
	if (window.ongcd) {console.log("on cd");return;}
	var ev = window.event || e;

	if (ev.key === 'Enter' && (tabSelected !== document.activeElement || tabSelected?.id === 'typeans')) {
		//last action was NOT selecting element with Tab -> Enter=flip (prevent audio activation)
		if (document.activeElement.matches('a.replay-button.soundLink')) {
			document.activeElement.blur();
		}
		if (!isFrontSide) {
			if (flipBtn && flipBtn.onclick) {
				flipBtn.onclick();
			}
		} else {
			flipToBack();
		}
	}

	if (!isFrontSide && ev.code === 'Space') {
		if (wrap.classList.contains("backside")) {
			return;
		}
		e.preventDefault();
		MemFlip(true);
	}

	if (isFrontSide && "1234567890".includes(ev.key) && (Qmode === "mchoice" || Qmode === "tapping")) {
		if (cardContF && !cardContF.classList.contains("nkeys")) {
			cardContF.classList.add("nkeys");
		} else {
			let numkey = parseInt(ev.key);
			if (numkey == 0) {numkey = 10};
			if (numkey <= keyboardButtons.length) {
				if (Qmode === "mchoice" || !keyboardButtons[numkey - 1].classList.contains("pressed")) {
					keyboardButtons[numkey - 1].onclick();
				} else {
					untapKey(numkey);
				}
			}
		}
	}

	if (!isFrontSide && "1234".includes(ev.key)) {
		if (platform === "desk") {
			pycmd('ease' + ev.key);
		} else if (platform === "ankiweb") {
			awRate(ev.key);
		}
	}
}
</script>

<script>
function androidAutoplay(chosenAudio, retry = 100) {
	if (retry <= 0) {
		chosenAudio.classList.add('failed');
		return;
	}
	if (window.open(chosenAudio.href) !== null) { // audio played successfully
		audioAnimation(chosenAudio);
	} else { // audio is not yet available
		chosenAudio.classList.add('loading');
		setTimeout(()=>{
			androidAutoplay(chosenAudio, retry - 1);
		}, 10);
	}
}
</script>

<script>
//Audio buttons animation
audioButtonsFront = cardContF.querySelectorAll('a.replay-button');

audioButtonsFront.forEach((a) => {
	if (a.classList.contains("embedded")) return;
	a.addEventListener("click", ()=>audioAnimation(a));
});

//autoplay Q audio
audioButtonsQ = cardContF.querySelectorAll('.mem-question a.replay-button');
if (audioButtonsQ && audioButtonsQ.length > 0) {
	//choose audio for Q
	chosen_i = Math.floor(Math.random() * audioButtonsQ.length);
	const chosenAudio = audioButtonsQ[chosen_i];
	chosenAudio.classList.add("chosen");
	if (chosenAudio.onclick) {
		chosenAudio.click();
	} else { // AnkiDroid #18235 bug
		androidAutoplay(chosenAudio);
	}
}
</script>


<script>
//on-screen keyboard | mult-choice buttons | tapping buttons

function shuffle(arr) {
  return arr.sort(() => 0.5 - Math.random());
}

if (screenKeyboard) {
	if (isFrontSide) {
		if (Qmode === "typing" && fillerString && corrAns) {
			if (typeof window.randomKeysN !== 'number') {randomKeysN = 10} //fallback
			neededKeys = shuffle(fillerString.split('')).slice(0, randomKeysN);
			neededKeys = shuffle([... new Set([...(corrAns.split('')), ...neededKeys])]);
			neededKeys = neededKeys.filter(key => !keysString.includes(key));
			keysString = neededKeys.join('') + keysString;
		} else if (Qmode === "mchoice") {
			choices = choices.filter(choice => (choice !== corrAns && !!choice));
			choices = shuffle([... new Set(choices)]);
			choices.splice((window.mchOptionsN || 6) - 1);
			choices = [corrAns, ...choices];
			shuffle(choices);
			keysString = choices.join('|');
		} else if (Qmode === "tapping") {
			tokens = preTokenize(corrAns).split(' ').map(token => token.trim());
			shuffle(tokens);
     keysString = tokens.join(' ');
		}

		sessionStorage.setItem("card-keyboard", keysString);
	} else {
		keysString = sessionStorage.getItem("card-keyboard") || "";
	}

	if (Qmode === "typing") {
	  keys = keysString.split('');
	} else if (Qmode === "mchoice") {
		keys = keysString ? keysString.split('|') : [];
		screenKeyboard.innerHTML = '';
	} else if (Qmode === "tapping") {
		keys = keysString ? keysString.split(' ') : [];
		keys = keys.map(key => key.replaceAll(" "," "));
		screenKeyboard.innerHTML = '';
	} else {
		keys = [];
		screenKeyboard.innerHTML = '';
	}

  keys.slice().reverse().forEach((key)=>{
    const keyButton = document.createElement("div");
    if (Qmode === "mchoice") {
      keyButton.innerHTML = key;
    } else if (Qmode === "tapping") {
      keyButton.innerText = key;
    } else {
      keyButton.innerText = key; // (Qmode === "typing")
    }
    keyButton.classList.add('membtn');
    if (Qmode === "typing" && (key === ' ' || key === ' ')) {keyButton.classList.add('space')}
    if (Qmode === "mchoice") {
      if (key === corrAns || allAlts.includes(key)) {
        keyButton.classList.add('correct')
      } else if (isMathJax && MJwrap(key) === corrAns) {
        keyButton.classList.add('correct')
      } else if (isMathJax) {
        allAlts.map(async (altAns) => cfWithMathJax(altAns, key).then((res) => {
          if (res) {keyButton.classList.add('correct')}
        }));
      }
    }
    screenKeyboard.prepend(keyButton);
  })
} 
</script>

<script>
delete window.randomKeysN;
delete window.mchOptionsN;
</script>

<script>
/*keyboard key functions*/
keyboardButtons = screenKeyboard.querySelectorAll('.membtn');

function flipToBack() {
	if (!isFrontSide) return;
	if (platform === 'desk') {
		pycmd("ans");
	} else if (platform === 'android') {
		showAnswer();
	} else if (platform === 'ankiweb') {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 1) {
			ankiwebButtons[0].click();
		} else {
			console.log(ankiwebButtons.length < 1 ? "can't flip to back: ankiweb button not found" : "can't flip to back: can't single out the ankiweb button");
		}
	}
	console.log('flip');
}
function lengthOfCommonPart(str1, str2) {
	const minLength = Math.min(str1.length, str2.length);
	let i; 
	for (i = 0; str1[i] == str2[i] && i < minLength; i++) {}
	return i;
}

function updInput(inputValue, cursorPos = null) {
	if (!typeAns) return;

	typeAns.value = inputValue;
	typeAns.setSelectionRange(cursorPos || inputValue.length, cursorPos || inputValue.length);

	storeAnswer();

	typeAns.focus(); // Android quirk (doesn't seem to update cursor position without this)
	if (platform !== 'desk') {
		typeAns.blur();
	}
}

function typeHint() {
	if (!hintAns || !typeAns) return;
	const tempAns = window.isMathJax && typeAns.value.startsWith('\\\x28') ? typeAns.value.slice(2) : typeAns.value;
	const correctLength = lengthOfCommonPart(tempAns, hintAns);

	if (correctLength < hintAns.length) {
		updInput(hintAns.slice(0, correctLength + 1));

	} else if (typeAns.value.length > correctLength) {
		updInput(hintAns);

	} else {
		flipToBack();
	}
}

function typeKey(keyContent) {
	if (!typeAns) return;
	const cursorStart = typeAns.selectionStart;
	const cursorEnd = typeAns.selectionEnd;
	const currentInput = typeAns.value;

	updInput(currentInput.slice(0, cursorStart) + htmlUnEscape(keyContent) + currentInput.slice(cursorEnd), cursorStart + 1);
}

function updTappedAnswer() {
  const tappedWords = tapAnsArea.querySelectorAll(".membtn");

  storeAnswer([...tappedWords].map(btn => btn.getAttribute("origin")).join("|"));
  //storeAnswer([...tappedWords].map(btn => btn.innerText).join(" "));
}

function untapKey(N) {
  if (!tapAnsArea) return;

  tapAnsArea.querySelector(`[origin="${N}"]`)?.onclick();
}

function tapKey(N, dynamic = false) {
  if (!tapAnsArea || !keyboardButtons || N > keyboardButtons.length || keyboardButtons[N - 1].classList.contains("pressed")) return;

  keyboardButtons[N - 1].classList.add("pressed");

  const tappedWordL = document.createElement("div");
  tappedWordL.classList.add('membtn');
  tappedWordL.innerText = keys[N - 1];
  tappedWordL.setAttribute("origin", N);
  tapAnsArea.append(tappedWordL);

  if (!dynamic) return;
  
  tappedWordL.onclick = ()=>{
    tappedWordL.remove();
    keyboardButtons[N - 1].classList.remove("pressed");
    updTappedAnswer();
  }

  updTappedAnswer();
}

if (isFrontSide) {
	keyboardButtons.forEach( btn => {
		if (btn.id === 'HintButton') {
			btn.onclick = typeHint;
		} else if (Qmode === "mchoice") {
			btn.onclick = ()=>{
				if (btn.classList.contains('pressed')) {
					storeAnswer("");
					btn.classList.remove('pressed');
				} else {
					storeAnswer(btn.innerHTML);
					keyboardButtons.forEach((b) => {
						b.classList.remove('pressed');
					});
					btn.classList.add('pressed');
				}
				flipToBack();
			};
		} else if (Qmode === "typing" && typeAns) {
			btn.onclick = ()=>{
				typeKey(btn.innerHTML); //innerText does not work for space
			};
		} else if (Qmode === "tapping") {
			const N = 1 + Array.prototype.indexOf.call(btn.parentNode.children, btn);
			btn.onclick = ()=>tapKey(N, true);
		}
	});

	if (typeAns) {
		typeAns.addEventListener('input', (event) => {storeAnswer();});
		if (platform === 'ankiweb') {
			setTimeout(()=>{
				document.activeElement.blur();
				typeAns.focus();
			}, 25);
		}
	}
}
</script>

<script>
//iOS AnkiWeb + embedded audio svg paths

rootStyles = getComputedStyle(document.documentElement);
memPlayPath = rootStyles.getPropertyValue('--mem-play').trim().replace(/^path\(["']?|["']?\)$/g, '');
document.querySelectorAll('svg.playImage path').forEach(path =>{
if (path.getAttribute('d')) return;
  path.setAttribute('d', memPlayPath);
});
</script>
<!-- End of code by Eltaurus -->

<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>



<!-- -------------------⚙️ user prior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>

<!-- ---------------------  template scripts  --------------------- -->
<!--
This section (up until the line containing "End of code by Eltaurus") is part of the Anki Card Type template.
Source: github.com/Eltaurus-Lt/Anki-Card-Templates

Copyright (C) 2023-2025 Eltaurus
Contact: 
    Email: Eltaurus@inbox.lt
    GitHub: github.com/Eltaurus-Lt
    Anki Forums: forums.ankiweb.net/u/Eltaurus

This template is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This template is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this template. If not, see <http://www.gnu.org/licenses/>.

You are free to use this template to create your own Anki cards and decks, modify the code, and openly share the derivative works, provided that this copyright notice and similar notices in other parts remain intact (as covered by section 7b of the GPLv3 license). 
Clarification: The copyright in this notice applies only to the above-stated section of the code. In particular, it does not extend to data contained within fields of Anki cards or any media files included in Anki decks created using this template. It also does not cover any scripts or HTML code that may be added to this HTML file (Front Template screen) by creators of derivative cards/templates. Creators are encouraged to add their own copyright statements alongside their code in a similar fashion.
-->

<script>
//determine card side
wrap = document.getElementById('backwrap');
isFrontSide = !wrap;
if (isFrontSide) {
  console.log("------------- card -------------");
} else {
  console.log(" - - - - - -  back  - - - - - - ");
}
</script>

<script>
//determine input flags
cardContF = document.querySelector(".card-content.front");
Qmode = cardContF.getAttribute("mode") || (cardContF.classList.contains("mch") ? "mchoice" : "typing");
isMathJax = cardContF.classList.contains("eq");
</script>

<script>
//determine platform
platform = '';
if (!!document.getElementById('qa_box')) {
  platform = 'ankiweb';
} else if (!document.documentElement.classList.contains("mobile")) {
	platform = 'desk';
} else if (document.documentElement.classList.contains("android")) {
	//var jsApiContract = { version: "0.0.3", developer: "eltaurus@inbox.lt" };
	//var api = new AnkiDroidJS(jsApiContract);
	platform = 'android';
} else {
	platform = 'ios';
}
console.log("platform: ", platform);
</script>

<script>
//prevent ankiweb's default autorate with number keys
if (platform === 'ankiweb' && !window.awKeyBlocker) {
  awKeyBlocker = document.addEventListener("keyup", (event) => {
    if (!"1234".includes(event.key)) return;
    event.stopImmediatePropagation();
    event.preventDefault();
  }, true);
}
//prevent ankiweb from autofocusing "show answer" and rate buttons
if (platform === 'ankiweb' && !window.observer) {
  window.observer = new MutationObserver(() => {
    document.querySelectorAll('[autofocus]').forEach((L) => {
      L.removeAttribute('autofocus');
      L.blur();
    });
  });

  observer.observe(document.body, { childList: true, subtree: true });
};

//ankiweb rate function
function awRate(ease) {
	const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
	if (ankiwebButtons.length === 4) {
		ankiwebButtons[ease - 1].click();
	} else {
		console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
	}
}
</script>

<script>
//block key presses immediately after page loading
function putOnCD() {
  ongcd = true;
  setTimeout(()=>{ongcd = false}, 400);
}
if (isFrontSide) putOnCD();
</script>

<script>
function htmlEscape(string) {
  return string.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll("'", '&#39;').replaceAll('"', '&quot;');
}

function htmlUnEscape(string) {
  return string.replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&#39;', "'").replaceAll('&quot;', '"').replaceAll('&amp;', '&');
}

function ansCleanUp(ansString) {
  return ansString?.replaceAll("&nbsp;", " ")?.replaceAll(" \n", " ").replaceAll("\n ", " ").replaceAll("\n", " ")?.trim();
}

function preTokenize(ans) {
  return ans.replaceAll('?',' ?').replace(/\u0020+/g, ' ');
}
</script>

<script>
//elements
typeAns = document.getElementById('typeans');
tapAnsArea = document.querySelector(".mem-typing");
screenKeyboard = document.getElementById('scr-keyboard');
hintButton = document.getElementById('HintButton');
embeddedAudios = [...document.querySelectorAll('audio')];
</script>

<script>
function audioAnimation(aL) {
	if (!aL) return;
	aL.classList.remove('loading');
	aL.classList.remove('failed');
	document.querySelectorAll('.card-content a.replay-button').forEach((b) => {
		b.classList.remove('active');
	});
	aL.classList.add('active');
	aL.classList.remove('pulse');
	void aL.offsetHeight;
	aL.classList.add('pulse');
}

function replayEmbedded(i, aL, retry = 50) { //for embedded audio tags (ankiweb)
  if (!embeddedAudios[i]) return;
  embeddedAudios.forEach(audioL => audioL.pause());
  embeddedAudios[i].currentTime = 0;
  embeddedAudios[i].play().then(()=>{
    audioAnimation(aL);
  }).catch((err) => {
    if (err.name === "NotAllowedError" || err.message.includes("user didn't interact")) {
      console.warn('awaits interaction');

      const interactions = ["click", "keydown"];
      const handler = ()=> {
        console.log('interaction detected');
        interactions.forEach(evt => document.removeEventListener(evt, handler));
        replayEmbedded(i, aL, retry - 1);
      }
      interactions.forEach(event => {
          document.addEventListener(event, handler);
      });

    } else {
      console.warn(`playback error (${err})`);
      aL.classList.add('loading');
      if (retry > 0) {
        setTimeout(()=>replayEmbedded(i, aL, retry - 1), 100); 
      } else {
        console.warn('max retry attempts exceeded');
        aL.classList.remove('loading');
        aL.classList.add('failed');
      }
    }
  });
}

//embedding audio buttons
embeddedAudios.forEach((audioL, i) => {
  const replayButtonHTML = `
    <a class="replay-button soundLink embedded" onclick="replayEmbedded(${i}, this)" href="../#">
      <svg class="playImage" viewBox="0 0 64 64" version="1.1">
        <circle cx="32" cy="32"></circle>
        <path></path>
      </svg>
    </a>
  `;
  
  const tempL = document.createElement('div');
  tempL.innerHTML = replayButtonHTML.trim();
  
  audioL.parentNode.insertBefore(tempL.firstChild, audioL.nextSibling);

  //move audio tag outside
  cardContF.appendChild(audioL);
  audioL.classList.add('off');
});

</script>

<script>
//MathJax
function MJwrap(latexString) {
  if ((latexString.startsWith("\\\x28") && latexString.endsWith("\\\x29")) || !latexString) {
    return latexString;
  }
  return "\\\x28" + latexString + "\\\x29";
}

function MJunwrap(latexString) {
  if (latexString.startsWith("\\\x28") && latexString.endsWith("\\\x29")) {
    return latexString.slice(2, -2);
  }
  return latexString;
}

async function MJconvert(latexString) {
  if (!window.MathJax) {
    console.log("MathJax not found");
    return latexString;
  }
  if (latexString.includes("mjx-container")) {
    return latexString;
  }
  const latexL = document.createElement('div');
  latexL.innerHTML = MJwrap(latexString);
  await MathJax.typesetPromise([latexL]);

  return latexL.innerHTML;
}

async function cfWithMathJax(sQ, sA) {
  const mathJaxRegex = /<mjx-container[^>]*>/g;

  return sQ.replace(mathJaxRegex, '') === sA.replace(mathJaxRegex, '') || (await MJconvert(sQ)).replace(mathJaxRegex, '') === sA.replace(mathJaxRegex, '') || (await MJconvert(sQ)).replace(mathJaxRegex, '') === (await MJconvert(sA)).replace(mathJaxRegex, '');
}
</script>

<script>
function storeAnswer(ans = "") {
	if (!ans && typeAns) {ans = typeAns.value};
	sessionStorage.setItem("userAnswer", ans);
}

if (isFrontSide) {
	storeAnswer("");
}
</script>

<script>
//determine the correct answer
corrAnsL = document.getElementById('correctAnswer');
</script>
<script>
//cloze
clozes = corrAnsL.querySelectorAll(".cloze");
if (clozes.length > 0) {
  corrAnsL.innerHTML = [...clozes].map(L => L.getAttribute("data-cloze")).join(Qmode !== "tapping" ? ", " : " ");
  let inactCloze = corrAnsL.querySelector("span.cloze-inactive");
  while (inactCloze) { // remove inactive nested clozes
    const frag = document.createDocumentFragment();
    while (inactCloze.firstChild) { frag.appendChild(inactCloze.firstChild); }
    corrAnsL.replaceChild(frag, inactCloze);
    inactCloze = corrAnsL.querySelector("span.cloze-inactive");
  }
}
</script>
<script>
corrAns = ansCleanUp(Qmode === "tapping" ? corrAnsL?.innerHTML.replaceAll(" ", " ").replaceAll("&nbsp;", " ") : corrAnsL?.innerHTML) || ""; //including alts

//extract primary, excluding alts
try {
	const tempL = document.createElement('div');
	tempL.innerHTML = corrAns;
	tempL.querySelectorAll('[part="alt"], .alt').forEach((L) => {L.remove();});
	corrAns = (Qmode !== "mchoice") ? tempL.innerText : tempL.innerHTML;
	corrAns = corrAns.trim();
	tempL.remove();
} catch (err) {}

inlnAlts = corrAns.split(/[;;]/).map(ansCleanUp);

hintAns = ansCleanUp(inlnAlts[0]);
if (isMathJax) {
	hintAns = MJunwrap(hintAns);
	if (Qmode === "mchoice") {
		inlnAlts = [corrAns]; //otherwise is split at ';' in '&gt;', '&lt;', etc.
	}
}

//determine alt answers
partAlts = [];
try {
	const altsString = [...corrAnsL.querySelectorAll('[part="alt"], .alt')].map(L => L.innerText).join('|');
	partAlts = altsString ? altsString.split('|').map(ansCleanUp) : [];
} catch (err) {}

allAlts = [...inlnAlts, ...partAlts];
</script>

<script>
//consts
keysString = document.getElementById('static_keys')?.innerText || "";
fillerString = document.getElementById('random_keys')?.innerText || "";
choices = document.getElementById('choices')?.innerHTML.split('|').map(ansCleanUp) || '';
</script>

<script>
//keyboard navigation
tabSelected = null;
document.onkeyup = function (e) {
	var ev = window.event || e;
	
	if (ev.key === 'Tab') {
		tabSelected = document.activeElement;
	}
}

document.onkeydown = function (e) {
	if (window.ongcd) {console.log("on cd");return;}
	var ev = window.event || e;

	if (ev.key === 'Enter' && (tabSelected !== document.activeElement || tabSelected?.id === 'typeans')) {
		//last action was NOT selecting element with Tab -> Enter=flip (prevent audio activation)
		if (document.activeElement.matches('a.replay-button.soundLink')) {
			document.activeElement.blur();
		}
		if (!isFrontSide) {
			if (flipBtn && flipBtn.onclick) {
				flipBtn.onclick();
			}
		} else {
			flipToBack();
		}
	}

	if (!isFrontSide && ev.code === 'Space') {
		if (wrap.classList.contains("backside")) {
			return;
		}
		e.preventDefault();
		MemFlip(true);
	}

	if (isFrontSide && "1234567890".includes(ev.key) && (Qmode === "mchoice" || Qmode === "tapping")) {
		if (cardContF && !cardContF.classList.contains("nkeys")) {
			cardContF.classList.add("nkeys");
		} else {
			let numkey = parseInt(ev.key);
			if (numkey == 0) {numkey = 10};
			if (numkey <= keyboardButtons.length) {
				if (Qmode === "mchoice" || !keyboardButtons[numkey - 1].classList.contains("pressed")) {
					keyboardButtons[numkey - 1].onclick();
				} else {
					untapKey(numkey);
				}
			}
		}
	}

	if (!isFrontSide && "1234".includes(ev.key)) {
		if (platform === "desk") {
			pycmd('ease' + ev.key);
		} else if (platform === "ankiweb") {
			awRate(ev.key);
		}
	}
}
</script>

<script>
function androidAutoplay(chosenAudio, retry = 100) {
	if (retry <= 0) {
		chosenAudio.classList.add('failed');
		return;
	}
	if (window.open(chosenAudio.href) !== null) { // audio played successfully
		audioAnimation(chosenAudio);
	} else { // audio is not yet available
		chosenAudio.classList.add('loading');
		setTimeout(()=>{
			androidAutoplay(chosenAudio, retry - 1);
		}, 10);
	}
}
</script>

<script>
//Audio buttons animation
audioButtonsFront = cardContF.querySelectorAll('a.replay-button');

audioButtonsFront.forEach((a) => {
	if (a.classList.contains("embedded")) return;
	a.addEventListener("click", ()=>audioAnimation(a));
});

//autoplay Q audio
audioButtonsQ = cardContF.querySelectorAll('.mem-question a.replay-button');
if (audioButtonsQ && audioButtonsQ.length > 0) {
	//choose audio for Q
	chosen_i = Math.floor(Math.random() * audioButtonsQ.length);
	const chosenAudio = audioButtonsQ[chosen_i];
	chosenAudio.classList.add("chosen");
	if (chosenAudio.onclick) {
		chosenAudio.click();
	} else { // AnkiDroid #18235 bug
		androidAutoplay(chosenAudio);
	}
}
</script>


<script>
//on-screen keyboard | mult-choice buttons | tapping buttons

function shuffle(arr) {
  return arr.sort(() => 0.5 - Math.random());
}

if (screenKeyboard) {
	if (isFrontSide) {
		if (Qmode === "typing" && fillerString && corrAns) {
			if (typeof window.randomKeysN !== 'number') {randomKeysN = 10} //fallback
			neededKeys = shuffle(fillerString.split('')).slice(0, randomKeysN);
			neededKeys = shuffle([... new Set([...(corrAns.split('')), ...neededKeys])]);
			neededKeys = neededKeys.filter(key => !keysString.includes(key));
			keysString = neededKeys.join('') + keysString;
		} else if (Qmode === "mchoice") {
			choices = choices.filter(choice => (choice !== corrAns && !!choice));
			choices = shuffle([... new Set(choices)]);
			choices.splice((window.mchOptionsN || 6) - 1);
			choices = [corrAns, ...choices];
			shuffle(choices);
			keysString = choices.join('|');
		} else if (Qmode === "tapping") {
			tokens = preTokenize(corrAns).split(' ').map(token => token.trim());
			shuffle(tokens);
     keysString = tokens.join(' ');
		}

		sessionStorage.setItem("card-keyboard", keysString);
	} else {
		keysString = sessionStorage.getItem("card-keyboard") || "";
	}

	if (Qmode === "typing") {
	  keys = keysString.split('');
	} else if (Qmode === "mchoice") {
		keys = keysString ? keysString.split('|') : [];
		screenKeyboard.innerHTML = '';
	} else if (Qmode === "tapping") {
		keys = keysString ? keysString.split(' ') : [];
		keys = keys.map(key => key.replaceAll(" "," "));
		screenKeyboard.innerHTML = '';
	} else {
		keys = [];
		screenKeyboard.innerHTML = '';
	}

  keys.slice().reverse().forEach((key)=>{
    const keyButton = document.createElement("div");
    if (Qmode === "mchoice") {
      keyButton.innerHTML = key;
    } else if (Qmode === "tapping") {
      keyButton.innerText = key;
    } else {
      keyButton.innerText = key; // (Qmode === "typing")
    }
    keyButton.classList.add('membtn');
    if (Qmode === "typing" && (key === ' ' || key === ' ')) {keyButton.classList.add('space')}
    if (Qmode === "mchoice") {
      if (key === corrAns || allAlts.includes(key)) {
        keyButton.classList.add('correct')
      } else if (isMathJax && MJwrap(key) === corrAns) {
        keyButton.classList.add('correct')
      } else if (isMathJax) {
        allAlts.map(async (altAns) => cfWithMathJax(altAns, key).then((res) => {
          if (res) {keyButton.classList.add('correct')}
        }));
      }
    }
    screenKeyboard.prepend(keyButton);
  })
} 
</script>

<script>
delete window.randomKeysN;
delete window.mchOptionsN;
</script>

<script>
/*keyboard key functions*/
keyboardButtons = screenKeyboard.querySelectorAll('.membtn');

function flipToBack() {
	if (!isFrontSide) return;
	if (platform === 'desk') {
		pycmd("ans");
	} else if (platform === 'android') {
		showAnswer();
	} else if (platform === 'ankiweb') {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 1) {
			ankiwebButtons[0].click();
		} else {
			console.log(ankiwebButtons.length < 1 ? "can't flip to back: ankiweb button not found" : "can't flip to back: can't single out the ankiweb button");
		}
	}
	console.log('flip');
}

function lengthOfCommonPart(str1, str2) {
	const minLength = Math.min(str1.length, str2.length);
	let i; 
	for (i = 0; str1[i] == str2[i] && i < minLength; i++) {}
	return i;
}

function updInput(inputValue, cursorPos = null) {
	if (!typeAns) return;

	typeAns.value = inputValue;
	typeAns.setSelectionRange(cursorPos || inputValue.length, cursorPos || inputValue.length);

	storeAnswer();

	typeAns.focus(); // Android quirk (doesn't seem to update cursor position without this)
	if (platform !== 'desk') {
		typeAns.blur();
	}
}

function typeHint() {
	if (!hintAns || !typeAns) return;
	const tempAns = window.isMathJax && typeAns.value.startsWith('\\\x28') ? typeAns.value.slice(2) : typeAns.value;
	const correctLength = lengthOfCommonPart(tempAns, hintAns);

	if (correctLength < hintAns.length) {
		updInput(hintAns.slice(0, correctLength + 1));

	} else if (typeAns.value.length > correctLength) {
		updInput(hintAns);

	} else {
		flipToBack();
	}
}

function typeKey(keyContent) {
	if (!typeAns) return;
	const cursorStart = typeAns.selectionStart;
	const cursorEnd = typeAns.selectionEnd;
	const currentInput = typeAns.value;

	updInput(currentInput.slice(0, cursorStart) + htmlUnEscape(keyContent) + currentInput.slice(cursorEnd), cursorStart + 1);
}

function updTappedAnswer() {
  const tappedWords = tapAnsArea.querySelectorAll(".membtn");

  storeAnswer([...tappedWords].map(btn => btn.getAttribute("origin")).join("|"));
  //storeAnswer([...tappedWords].map(btn => btn.innerText).join(" "));
}

function untapKey(N) {
  if (!tapAnsArea) return;

  tapAnsArea.querySelector(`[origin="${N}"]`)?.onclick();
}

function tapKey(N, dynamic = false) {
  if (!tapAnsArea || !keyboardButtons || N > keyboardButtons.length || keyboardButtons[N - 1].classList.contains("pressed")) return;

  keyboardButtons[N - 1].classList.add("pressed");

  const tappedWordL = document.createElement("div");
  tappedWordL.classList.add('membtn');
  tappedWordL.innerText = keys[N - 1];
  tappedWordL.setAttribute("origin", N);
  tapAnsArea.append(tappedWordL);

  if (!dynamic) return;
  
  tappedWordL.onclick = ()=>{
    tappedWordL.remove();
    keyboardButtons[N - 1].classList.remove("pressed");
    updTappedAnswer();
  }

  updTappedAnswer();
}

if (isFrontSide) {
	keyboardButtons.forEach( btn => {
		if (btn.id === 'HintButton') {
			btn.onclick = typeHint;
		} else if (Qmode === "mchoice") {
			btn.onclick = ()=>{
				if (btn.classList.contains('pressed')) {
					storeAnswer("");
					btn.classList.remove('pressed');
				} else {
					storeAnswer(btn.innerHTML);
					keyboardButtons.forEach((b) => {
						b.classList.remove('pressed');
					});
					btn.classList.add('pressed');
				}
				flipToBack();
			};
		} else if (Qmode === "typing" && typeAns) {
			btn.onclick = ()=>{
				typeKey(btn.innerHTML); //innerText does not work for space
			};
		} else if (Qmode === "tapping") {
			const N = 1 + Array.prototype.indexOf.call(btn.parentNode.children, btn);
			btn.onclick = ()=>tapKey(N, true);
		}
	});

	if (typeAns) {
		typeAns.addEventListener('input', (event) => {storeAnswer();});
		if (platform === 'ankiweb') {
			setTimeout(()=>{
				document.activeElement.blur();
				typeAns.focus();
			}, 25);
		}
	}
}
</script>

<script>
//iOS AnkiWeb + embedded audio svg paths

rootStyles = getComputedStyle(document.documentElement);
memPlayPath = rootStyles.getPropertyValue('--mem-play').trim().replace(/^path\(["']?|["']?\)$/g, '');
document.querySelectorAll('svg.playImage path').forEach(path =>{
if (path.getAttribute('d')) return;
  path.setAttribute('d', memPlayPath);
});
</script>
<!-- End of code by Eltaurus -->

<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->
<script>
  //place your scripts here


</script>

BACK


<hr id="answer" class="sys">
<div id="backwrap" class="frontside">
	{{FrontSide}}
	<button id="mem-flip" class="sys">flip</button>
	<div class="card-content back">  

		<div class="mem-alert"></div>   

		<div class="mem-field">
			<label>Learnable</label>
			<h2>{{Learnable}}</h2>
			<span id="spelldiff" class=""></span>
		</div>
<!-- Added Transcription field here -->
		{{#Transcription}}
		<div style="margin-bottom: 15px; text-align: left; text-transform: lowercase;">
			{{Transcription}}
		</div>
		{{/Transcription}}
		<div class="mem-field no-alts">
			<label>Definition</label>
			<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
				<h3>{{Definition}}</h3>
				{{#Part of speech}}
				<div style="font-size: 14px; background-color: #e0e0e0; color: #333;
							padding: 4px 10px; border-radius: 12px; font-weight: bold; white-space: nowrap;">
					{{Part of speech}}
				</div>
				{{/Part of speech}}
			</div>
		</div>








<!-- Переміщене поле Picture -->
<div class="picture-container" style="display: flex; justify-content: left ; margin-bottom: 11px;">
    {{Picture}}
</div>
<!-- Стилі -->
<style>



.picture-container img {
    width: 400px;
    height: auto;
    max-height: 250px;
    object-fit: cover;
    border: none;
    border-radius: 16px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}


</style>
		<div class="sep"></div>

		{{#Extra}}
			<div class="mem-field no-alts">
				<label>Example</label>
				<h4>{{Extra}}</h4>      
			</div>
		{{/Extra}}
		{{#Extra 2}}
			<div class="mem-field no-alts">
				<label>Use</label>
				<h4>{{Extra 2}}</h4>
			</div>
		{{/Extra 2}}
		{{#Audio}}
			<div class="mem-field no-alts">
				<label>Audio</label>
				<h4>{{Audio}}</h4>
			</div>
		{{/Audio}}

	</div>	
</div>


<!-- Synonym під Mnemonic без проміжку -->
{{#Synonym}}
<div style="position: absolute; top: calc(80% + 8px); right: 52px; background-color: #3a3a3a;
            border-radius: 10px; padding: 8px 14px; font-size: 14px; font-weight: bold; max-width: 30ch;
            text-align: left; white-space: normal; word-wrap: break-word; color: #f0f0f0;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);">
    <strong style="display: block; margin-bottom: 6px;">Synonyms:</strong>
    <span style="white-space: pre-wrap; line-height: 1.3;">{{Synonym}}</span>
</div>
{{/Synonym}}

<!-- -------------------⚙️ user prior scripts ⚙️------------------ -->
<script>
  //place your scripts here
alwaysShowInfo = true;

</script>

<!-- ---------------------  template scripts  --------------------- -->
<!--
This section (up until the line containing "End of code by Eltaurus") is part of the Anki Card Type template.
Source: github.com/Eltaurus-Lt/Anki-Card-Templates

Copyright (C) 2023-2025 Eltaurus
Contact: 
    Email: Eltaurus@inbox.lt
    GitHub: github.com/Eltaurus-Lt
    Anki Forums: forums.ankiweb.net/u/Eltaurus

This template is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This template is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this template. If not, see <http://www.gnu.org/licenses/>.

You are free to use this template to create your own Anki cards and decks, modify the code, and openly share the derivative works, provided that this copyright notice and similar notices in other parts remain intact (as covered by section 7b of the GPLv3 license). 
Clarification: The copyright in this notice applies only to the above-stated section of the code. In particular, it does not extend to data contained within fields of Anki cards or any media files included in Anki decks created using this template. It also does not cover any scripts or HTML code that may be added to this HTML file (Back Template screen) by creators of derivative cards/templates. Creators are encouraged to add their own copyright statements alongside their code in a similar fashion.
-->

<script>
//generate random page id
pid = Array.from({length:16}, () => String.fromCharCode(Math.floor(Math.random() * 94) + 33)).join('');
//console.log("pageid: ", pid);
</script>

<script>
//Ratcliff-Obershelp
function stringDiff(s1, s2) {
    const n = s1.length;
    const m = s2.length;

    // Matrix of contiguous LCS lengths
    const M = Array.from({length: n + 1}, () => Array(m + 1).fill(0)); 
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (s1[i] === s2[j]) {
                M[i+1][j+1] = M[i][j] + 1;
            }
        }
    }

    function minorLCS(n1, n2, m1, m2) {
        let max = 0;
        let pos;
        for (let i = n1; i <= n2; i++) {
            for (let j = m1; j <= m2; j++) {
                const Mij = Math.min(M[i][j], j - m1 + 1, i - n1 + 1);
                if (Mij > max) {
                    max = Mij;
                    pos = [i, j];
                }
            }
        }
        return { "length": max, pos }
    }
  
    function Diff(n1, n2, m1, m2) {
      const LCS = minorLCS(n1, n2, m1, m2);
      const length0 = LCS.length;
      
      if (length0 === 0) {
        const diff1 = s1.substring(n1 - 1, n2);
        const diff2 = s2.substring(m1 - 1, m2);
        diff = '';
        if (diff2) {
          diff += '<span class="typeBad">' + htmlEscape(diff2) + '</span>';
        }
        if (diff1) {
          diff += '<span class="typeMissed">' + htmlEscape(diff1) + '</span>';
        }
        return {
          diff,
          CSlength: 0
        };
      } else {  
        const n0 = LCS.pos[0];
        const m0 = LCS.pos[1];
        const D1 = Diff(n1, n0 - length0, m1, m0 - length0);
        const D2 = Diff(n0 + 1, n2, m0 + 1, m2);
        const diff0 = '<span class="typeGood">' + htmlEscape(s1.substring(n0 - length0, n0)) + '</span>';
        return {
          diff: D1.diff + diff0 + D2.diff,
          CSlength: D1.CSlength + length0 + D2.CSlength
        }
      }
    }
  
    const D = Diff(1, n, 1, m);
    // classic similarity score: not typing a letter yields higher score than typing it incorrectly => discourages trying, not ideal
    //const score = 2 * D.CSlength / (n + m); 
    // corrected score: typing wrong letter yields the same score as skipping it
    const score = D.CSlength / Math.max(m, n);

    return { score, "CSlength": D.CSlength, "diff": D.diff };
}
</script>

<script>
//get user answer
userAns = sessionStorage.getItem("userAnswer");
if (userAns === null) {
	document.getElementsByClassName("mem-alert")[0].innerText = "#error loading answer!";
	userAns = '';
} else if (userAns.trim()) {
  if (Qmode === "tapping") {
    pressedSequence = userAns.split('|');
    userAns = pressedSequence.map(N => keys[N - 1]).join(' ');
  }
  if (Qmode === "mchoice") {
	  document.getElementsByClassName("mem-alert")[0].innerHTML = userAns; //text formatting, images and audio + latex in mcq
  } else if (!window.isMathJax) {
    setTimeout(()=>document.getElementsByClassName("mem-alert")[0].innerText = userAns, 100); // "a<b" and "b>a" in plain text
  } else {
    MJconvert(htmlEscape(userAns)).then(res => document.getElementsByClassName("mem-alert")[0].innerHTML = res);
  }
}
if (Qmode === "tapping" && userAns) { //pressedSequence can remain from previous cards
  pressedSequence.forEach((N) => tapKey(N));
}
if (typeAns?.tagName === 'INPUT') {
	typeAns.value = userAns;
	typeAns.disabled = true;
} else if (typeAns) {
	typeAns.innerHTML = '<span>' + htmlEscape(userAns) + '</span>'; //backward compatibility with stock Anki typing
}

console.log(`user answer: ${userAns} | correct answer: ${corrAns}`);
</script>

<script>
//answer comparison rules
function rmEnc(s) {
	//removes parts enclosed in the innermost parentheses
	return s.replace(/\x28[^()]*\x29/g, '');
}
function rmEncAll(s) {
	//remove everything between first and last brackets
	return s.replace(/\x28.*\x29/g, '');
}
function rmBrac(s) {
	//removes parentheses, keeping the contents
	return s.replace(/[()]/g, '');
}
function rmPunc(s) {
	//removes other punctuation
	return s.replace(/[.,\/#?!$%\^&\'"*;:{}=\-_`~ …〜~-。、・?!@#$%^&*()]/g, '');
}
function rmSpaces(s) {
	//removes spaces from the start and the end, replaces japanese and non-breaking spaces, removes repeated spaces
	return ansCleanUp(s).replace(/ /g, ' ').replace(/\s+/g, ' ');
}
function cfStrings(sQ, sA) {
 //unify case and remove punctuation
	var sq = rmSpaces(rmPunc(sQ.toLowerCase()));
	var sa = rmSpaces(rmPunc(sA.toLowerCase()));

	if (sq == sa) {
		return true;
	}
	if (rmSpaces(rmEnc(rmEnc(sq))) == sa) {
		return true;
	}
	if (rmSpaces(rmBrac(sq)) == sa) {
		return true;
	}
	return false;
}

//A (B) -> {〃, A, A B}
//A; B -> {〃, A, B}
//A; B (C) -> {〃, A, B, B C, B (C)}
//todo?A (B; C) -> {〃, A B, A C}

// grade answer
diff = "";
console.log(`altertnatives: ${allAlts.join(' | ')}`);

async function setCorrectClass() {
  if (!userAns.trim()) return;
  if (Qmode === "tapping") {
	//tapping (any kind)
    if (preTokenize(corrAns).replaceAll(" "," ") === userAns) {
      wrap.classList.add('correct');
    }
    return
  }
	if (!window.isMathJax) {
//text cf
		if (Qmode === "mchoice") {
			keyboardButtons.forEach(btn => {
				if (btn.innerHTML === userAns) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			})

	//type-in
		} else if (cfStrings(corrAns, userAns)) {
			wrap.classList.add('correct');
		} else if (allAlts.map((altAns) => cfStrings(altAns, userAns)).includes(true)) {
			wrap.classList.add('correct');
		}
	} else {
//latex cf
	userAns = MJunwrap(userAns);
	corrAns = MJunwrap(corrAns);
		if (Qmode === "mchoice") {
			const btnPromises = [...keyboardButtons].map(async btn => {
				if (btn.innerHTML === userAns || (await cfWithMathJax(btn.innerHTML, userAns))) {
					btn.classList.add('pressed');
					if (btn.classList.contains('correct')) {
						wrap.classList.add('correct');
					}
				}
			});
			await Promise.all(btnPromises);

	//type-in
		} else if (corrAns === userAns) {
			wrap.classList.add('correct');
		} else {
			const altPromises = allAlts.map(async (altAns) => await cfWithMathJax(htmlEscape(altAns), htmlEscape(userAns)));
			
			await Promise.all(altPromises).then(results => {
				if (results.includes(true)) {
					wrap.classList.add('correct');
				}
			});
		}
	}
}
setCorrectClass().then(() => {
	if (wrap.classList.contains('correct')) {
		highlightGood();
	} else {
		highlightAgain();
		if (Qmode === "mchoice") {
				wrap.classList.add('wrong');
		} else {
	//type-in or tapping
			diff = stringDiff((Qmode !== "tapping") ? corrAns : preTokenize(corrAns).replaceAll(" "," "), userAns);
			if (diff.score > 0.67) {
				wrap.classList.add('soclose');
			} else {
				wrap.classList.add('wrong');
			}
			//spelling diffs
			const spellDiff = document.getElementById('spelldiff');
			if (spellDiff && userAns.trim()) {
				spellDiff.innerHTML = diff.diff;
			}
		}
	}
});

//timeout flip
infoOnCorrect = !!window.alwaysShowInfo; //without this redef impossible to reliably clear the hook if rated with Anki buttons
delete window.alwaysShowInfo;

flipBtn = document.getElementById('mem-flip');
function MemFlip(toInfo = false) {
	try {clearTimeout(activeTimeout)} catch (err) {};

	if (wrap.classList.contains("correct") && !toInfo && !infoOnCorrect && platform !== 'ios') {
		autorateGood();
	} else {
		putOnCD();

		wrap.classList.add("backside");
		wrap.classList.remove("frontside");
		setTimeout(()=>window.scrollTo(0, 0), 1);
		flipBtn.onclick = ()=>{
			if (wrap.classList.contains("correct")) {
				autorateGood();
			} else if (wrap.classList.contains("wrong") || wrap.classList.contains("soclose")) {
				autorateAgain();
			} else {
				console.log("the answer is not yet evaluated");
			}
		}
	}
}
flipBtn.onclick = MemFlip;

activeTimeout = setTimeout((pid0)=>{
//	console.log(pid, "|", pid0);
	if (pid !== pid0) return;
	MemFlip();
}, Math.round((window.flipDelay || 1.5) * 1000), pid);
delete window.flipDelay;

//Submit
function highlightAgain() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[0].classList.add('preselected');
			ankiwebButtons[0].focus();
			ankiwebButtons[0].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function highlightGood() {
	if (platform === "ankiweb") {
		const ankiwebButtons = document.querySelectorAll('.btn.btn-primary.btn-lg');
		if (ankiwebButtons.length === 4) {
			ankiwebButtons[2].classList.add('preselected');
			ankiwebButtons[2].focus();
			ankiwebButtons[2].blur();
		} else {
			console.log(`incorrect number of answer buttons (&{ankiwebButtons.length})`);
		}
	}
}
function autorateAgain() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease1');
		} else if (platform === 'android') {
			buttonAnswerEase1();
		} else if (platform === 'ankiweb') {
			awRate(1);
		}
		console.log("🔴 autorated 'again'");
}
function autorateGood() {
		flipBtn.onclick = null;
		if (platform === 'desk') {
			pycmd('ease3');
		} else if (platform === 'android') {
			buttonAnswerEase3();
		} else if (platform === 'ankiweb') {
			awRate(3);
		}
		console.log("🟢 autorated 'good'");
}
</script>

<script>
//embedding backside audio buttons (android)
embeddedAudiosBack = [...document.querySelectorAll('audio:not(.off)')];
embeddedAudiosBack.forEach((audioL, i) => {
  const replayButtonHTML = `
    <a class="replay-button soundLink embedded" onclick="replayEmbedded(${i + embeddedAudios.length}, this)" href="../#">
      <svg class="playImage" viewBox="0 0 64 64" version="1.1">
        <circle cx="32" cy="32"></circle>
        <path></path>
      </svg>
    </a>
  `;
  
  const tempL = document.createElement('div');
  tempL.innerHTML = replayButtonHTML.trim();
  
  audioL.parentNode.insertBefore(tempL.firstChild, audioL.nextSibling);

  //move audio tag outside
  cardContF.appendChild(audioL);
  audioL.classList.add('off');
});
embeddedAudios = embeddedAudios.concat(embeddedAudiosBack);

</script>
<script>
//Audio buttons animation
document.querySelectorAll('.card-content.back a.replay-button').forEach((a) => {
	if (a.classList.contains("embedded")) return;
	a.addEventListener("click", ()=>audioAnimation(a));
});
</script>

<!-- End of code by Eltaurus -->

<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->



<script>
  // Trigger Advanced Answer Sounds by simulating button clicks
  function triggerAnswerSound(ease) {
    const button = document.querySelector(`button[data-ease="${ease}"]`);
    if (button) {
      button.click();
      console.log(`Triggered sound for ease: ${ease}`);
    } else {
      console.log("Answer button not found - using fallback");
      // Fallback to original pycmd if buttons are missing
      if (platform === 'desk') {
        pycmd(`ease${ease}`);
      } else if (platform === 'android') {
        window[`buttonAnswerEase${ease}`]();
      } else if (platform === 'ankiweb') {
        awRate(ease);
      }
    }
  }

  // Override autorate functions to trigger sounds
  function autorateAgain() {
    flipBtn.onclick = null;
    triggerAnswerSound(1);
    console.log("🔴 autorated 'again' with sound");
  }

  function autorateGood() {
    flipBtn.onclick = null;
    triggerAnswerSound(3);
    console.log("🟢 autorated 'good' with sound");
  }
</script>



<!-- -------------------⚙️ user posterior scripts ⚙️------------------ -->
<script>
  // Handle Ctrl key for Hint button - one character per press
  document.addEventListener('keydown', function(e) {
    // Check if Ctrl is pressed alone
    if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.key === 'Control') {
      const hintButton = document.getElementById('HintButton');
      if (hintButton) {
        // Get current state
        const userInput = document.querySelector('input#typeans')?.value || '';
        const correctAnswer = "{{Learnable}}".toLowerCase();
        
        // Find next character to reveal
        let nextCharIndex = 0;
        for (let i = 0; i < Math.min(userInput.length, correctAnswer.length); i++) {
          if (userInput[i] !== correctAnswer[i]) {
            nextCharIndex = i;
            break;
          }
          nextCharIndex = i + 1;
        }
        
        // Add only next character if available
        if (nextCharIndex < correctAnswer.length) {
          const newInput = correctAnswer.substring(0, nextCharIndex + 1);
          document.querySelector('input#typeans').value = newInput;
          storeAnswer(newInput);
        }
        
        e.preventDefault();
      }
    }
  });
</script>






{{tts en_US voices=AwesomeTTS:Learnable}}

This also works fine on my side. Although this is still lacking your styling tab code and field setup.
You can share the whole Note Type more easily if you just export a single card and upload it somewhere.
Also, for testing purposes, what Anki version do you use?

Hi. I am rounding my musical studying system quite a bit with other basic and intermediate concepts and it works a charm. I use the template we had done for intervals, with the three card/question types and the multiple choice for anki add on and its just the perfect combination and balance. The variety in ways that the info is reviewed, and the interactivity of the entire thing definitely engages just about any student. However, I have noticed that
The mem instruction, which can be a bit redundant (but definitely helps) inn some cases is being announced by my screen reader but apparently does not actually display? I suspect I am missing a closing tag or something somewhere
here is the snippet front side of the cards, for reference.. I am still struggling some with html coding in general but have learned a great deal from this exercise:

{{#Question diagram}}{{#answer text}}{{#Choices answer text}}
<setting id="static_keys"></setting>
<setting id="random_keys"></setting>
<data id="correctAnswer">{{answer text}}</data>
<data id="choices">{{Choices answer text}}</data>

<div class="card-content front nkeys" theme="Memrise" mode="mchoice">

    <div class="overhead">
        <div class="mem-instruction">
            Select the correct name for:
</div></div>
    <div class="mem-question no-alts memblob">
        <div>{{Question diagram}}</div>
    </div>
		{{#Audio}}
			<div class="mem-field no-alts">
				<label>Audio</label>
				<h4>{{Audio}}</h4>
			</div>
		{{/Audio}}
    <div class="mem-typing">
        <label>answer text</label>
        <input id="typeans" type="text" inputmode="text" autocorrect="off" autocomplete="off" autocapitalize="off" spellcheck="false">
    </div>

any hint is appreciated. Thanks!

I’m not sure how much of the code the provided piece is supposed to represent. It can’t be the whole code, because there are no closing field conditions, and Anki wouldn’t even allow it to be saved in the current form. But if it is supposed to be only the beginning, then it doesn’t differ from the version you sent previously in anything other than the wording of the instruction, so I don’t see any issues with it.

There is another factor at play, though. The “Select the correct name for:” text is supposed to be displayed only when the window the card is opened in is wide enough. When it is narrow (which, for example, is almost always the case for mobile platforms), the instruction is hidden.
This is the behavior of the original Memrise layout. It can be adjusted in the styling tab, if necessary.

I would believe I will have to adjust css/styling, since it shows fine on desktop. Any pointers?