[UserScript] Quick Answers

:warning: This is a third-party script/app and is not created by the WaniKani team. By using this, you understand that it can stop working at any time or be discontinued indefinitely.

This script is inspired by [UserScript] WaniKani Fast Abridged Wrong/Multiple Answer since this script has many bugs and is no longer maintained I decided to completely rewrite it and add some additional features. Also the name “Fast Abridged Wrong/Multiple Answer” never made sense to me so it is now Quick Answers :slight_smile:

edit: WaniKani Fast Abridged Wrong/Multiple Answer now has an official successor WaniKani Reviews Plus which focuses on improving reviews overall and has some additional features such as a live tally of your review items as you go through your reviews. I am sure he has more features planned.

I will continue to maintain this version with a focus on limiting the need to open the info box. There are already some features in this script not available in Reviews Plus and vice versa. You can think of this script as a lightweight version. You can decide for yourself which you want to use.

Installation:

Features

  • Shows a popup with the correct answer(s). The popup can be configured to show in the following cases:

    • Wrong answer inputted
    • Correct answer inputted
    • Correct answer but multiple answers available
    • Answer is accepted but a bit off

  • Kanji/Radical Breakdown that links to respective pages
  • Part of Speech (word type) information
  • Customizable hotkey to show/hide the popup (default: a)
  • Show Onyomi readings in Katakana
  • Works with and without Double Check
  • Works in lesson quiz, reviews, recent mistakes and extra study
Settings

Settings can be accessed on the Dashboard

Display:

  • Show On Wrong Answer - Shows the popup if you get the answer wrong.
  • Show On Correct Answer - Show the popup if you get the answer correct.
  • Show On Multiple Answers Available - Shows the popup if multiple answers are available.
  • Show On Your Answer was a bit off - Show the popup if you answer is accepted but not entirely correct.
  • Also Show Alternative Kanji Readings - Shows alternative kanji readings.
  • Toggle Quick Answer Key - The key used to toggle the quick answer popup when it is not shown (default is a)

Breakdown:

  • Show Breakdown - Whether or not to show the Kanji/Radical Breakdown.
  • Show Word Type - Whether or not to show the word type information.
  • Show All Meanings in Breakdown - Whether or not to show all the meanings in the kanji breakdown or just the primary reading.

Misc:

  • Use Katakana for Onyomi - Shows Kanji Onyomi readings in Katakana.
  • Hide Info Popup - Hides the info popup if the quick answer popup is shown.
2 Likes

If enough people request the the custom colours from the old script I’ll add it but personally I never used it

Works a treat, thank you!

It would be nice if it could kick in also for “Recent Mistakes” and “Extra Study” but no problem if it’s too much of a hassle, having it for regular reviews is good enough :slight_smile:

1 Like

Done, I have never actually used the recent mistakes or extra study so I forgot they existed :joy:

1 Like

Thanks much!

Yeah the name was not great. It started as something else and evolved into what it is now, I just never came up with a better name.
True, I haven’t touched it in a while. As for bugs, I can’t say I was aware of any. Oh well.

Besides the new turbo loading breaking it, there was also

I looked at the code and only this setting wkof.settings.fawa.alwaysShowOnlyMultiple seemed to do anything.

The script also relies on Double Check’s didAnswerQuestion event so it wouldn’t work without Double Check. The vanilla event doesn’t have results.passed

But yea the biggest thing for me was not being able to disable it for incorrect answers. I only really use it to see the other meanings/readings. I did actually just have local changes on your script for a long time before I decided to rewrite it, mostly because of the turbo loading.

Released v1.2:

New Features:

  • Kanji/Radical Breakdown - credit to @DaisukeJigen for the idea
    • Some improvements over his version:
      • The Kanji/Vocab stays centered and the breakdown appears to the right instead of recentering everything
      • You can click on a breakdown item to go to the radical/kanji page
      • @DaisukeJigen not sure if you took into account showing radicals that don’t have a unicode character?
  • Word Type (Part of speech) Information

1 Like

Hey there,

thanks for the effort! I get so much out of all the great userscripts. I wish Wanikani would integrate more of those features in the base system.

I finally got this one to work. There seemed to be some interference with your other script named Hide Meanings in Lessons. I’m unable to get Quick Answers work, when the other one is activated.
So just a heads-up in case you want to take a look.

Keep up the good work!

1 Like

One more thing:
“Quick Answers” doesn’t have “Wanikani” in the name on Greasyfork, which makes it quite hard to find, when you search for Wanikani scripts. I only found it indirectly through some references in this forum. Just in case you want to consider changing the name.

1 Like

Hey, thanks for the kinds words.

Is Hide Meanings in Lessons up to date? I had a similar issue before and fixed it then. Currently they do work together for me.

I never considered that people search for scripts on greasyfork. Sure I can change it

1 Like

I just reinstalled “Hide Meanings in Lessons”, just to be sure, but it still destroys “Quick Answers” somehow. Funny thing is, that it also prevents “Wanikani Reviews Plus” from working.

Anyway, never mind, it’s not a big deal. I got used to workarounds after the last big WK update a few months ago, when many userscripts started having issues.
F5 is my most used key. :sweat_smile:

That was actually my main way of finding useful scripts. I’m beginning to wonder, how many I might have missed because of that.

Ummm… the new Kanji/Radical breakdown is really nice (Thank you!), but unfortunately it conflicts with the Pitch Info script, replacing its display.
Would you consider moving the K/R info to the left-hand-side, or showing it only on the Meaning page not on Reading?
Pretty please :blush:

I’ll look into it but likely only this weekend. It is a bit more complicated than simply placing it on the right so it may not work even on the left side but I may learn something from how the Pitch Info script renders its info.

1 Like

I tested again now and I am still not able to reproduce this. Could you provide some more info:

  • Browser and OS
  • Steps to reproduce
  • What exactly do you mean by destroys, nothing happens at all? Hide meanings works but quick answers doesn’t? Do you get any errors in console?

Yea Reviews plus is largely based on Quick Answers from what I could tell looking a the code.

Possibly my most popular script: Better Lesson Picker :joy: (I’ll rename it as well)

1 Like

I updated all of my scripts, there is a good chance it works now. Interestingly I found a bug with WaniKani itself: if you reload the lessons page then when you switch tabs (meaing, reading, etc) it sometimes reloads the page and then this breaks hide meaning in lessons sometimes :man_shrugging:

I just updated all scripts again, but this time individually. It works now! Maybe the general update button in Tampermonkey doesn’t work properly, or I just overlooked something else.
Anyways, thanks a lot for looking into this! :pray:

This is not necessary anymore I guess. By “destroy” I meant, that it just didn’t show any additional meanings, as if the script were deactivated or not installed in the first place. Regarding error messages, I don’t know how to use or show the console.

Thats awesome, thanks! I indeed missed that and installed it right away, since I’m a keen user of the new WK lesson picker. :star_struck:

1 Like

You didn’t overlook anything it was an issue on my side.

To quickly summarise the issue without getting too technical; Modern scripts need to rely on a library to handle page loading correctly now. Hide meaning in lessons and quick answers were using different versions, which I didn’t think was a problem because it worked for me but it if the order of execution (the order on tampermonkey) was different it could lead to some issues.

tldr: I standardised the script loading across my scripts.

Thanks for reporting the issue!

1 Like

Sorry, I’ve been a bit busy, didn’t get a chance to look at it last weekend. I do have them working together now but it requires some changes in the Pitch Info Script:

- if (wkof.settings.wanikani_pitch_info?.display_pitch_beside_question && ev.detail.questionType == 'reading' && ev.detail.results.action == 'pass') {
- let divQuestion = document.querySelector("#turbo-body > div.quiz > div > div.character-header.character-header--vocabulary > div > div.character-header__characters");
+ if (wkof.settings.wanikani_pitch_info?.display_pitch_beside_question && ev.detail.questionType == 'reading' && ev.detail.subjectWithStats.subject.type == 'Vocabulary' && ev.detail.results.action == 'pass') {
+ let divQuestion = document.querySelector(".character-header__characters");

On lines 131 and 132.

Or if you want to just copy the entire editted script:
// ==UserScript==
// @name         WaniKani Pitch Info
// @match        https://www.wanikani.com/*
// @match        https://preview.wanikani.com/*
// @namespace    https://greasyfork.org/en/scripts/31070-wanikani-pitch-info
// @version      0.79
// @description  Displays pitch accent diagrams on WaniKani vocab and session pages.
// @author       Invertex
// @supportURL   http://invertex.xyz
// @run-at       document-idle
// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1416982
// @resource     accents https://raw.githubusercontent.com/mifunetoshiro/kanjium/94473cd69598abf54cc338a0b89f190a6c02a01c/data/source_files/raw/accents.txt
// @grant        GM_getResourceText
// @grant        unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/31070/WaniKani%20Pitch%20Info.user.js
// @updateURL https://update.greasyfork.org/scripts/31070/WaniKani%20Pitch%20Info.meta.js
// ==/UserScript==

var wkof = null;

(function() {
  'use strict';
  /* global wkItemInfo */
  /* eslint no-multi-spaces: off */
  wkof = unsafeWindow.wkof;

  const SHOW_PITCH_DESCRIPTION = true;
  const SQUASH_DIGRAPHS        = false;
  const PRE_PARSE              = false; // load entire "accents.txt" into an object for faster lookup (true: lookup takes ~0.06ms; false: lookup takes ~0.5ms)
  const DOT_RADIUS             = 0.2;
  const STROKE_WIDTH           = 0.1;

  const WEB_URL = 'http://www.gavo.t.u-tokyo.ac.jp/ojad/search/index/curve:fujisaki/word:%s';
  let digraphs = 'ぁぃぅぇぉゃゅょゎゕゖァィゥェォャュョヮヵヶ';

  let pitchLookup = null;

  // Get the color and the pitch pattern name
  let patternObj = {
    heiban: {
      name: '平板',
      nameEng: 'heiban',
      cssClass: 'heiban',
      color: '#d20ca3',
    },
    odaka: {
      name: '尾高',
      nameEng: 'odaka',
      cssClass: 'odaka',
      color: '#0cd24d',
    },
    nakadaka: {
      name: '中高',
      nameEng: 'nakadaka',
      cssClass: 'nakadaka',
      color: '#27a2ff',
    },
    atamadaka: {
      name: '頭高',
      nameEng: 'atamadaka',
      cssClass: 'atamadaka',
      color: '#EA9316',
    },
    unknown: {
      name: '不詳',
      nameEng: 'No pitch value found, click the number for more info.',
      cssClass: 'unknown',
      color: '#CCCCCC',
    },
  };

  const JAPANESE_TO_WORD_TYPE = {
    名: 'Noun',
    代: 'Pronoun',
    副: 'Adverb',
    形動: 'な Adjective',
    感: 'Interjection'
  };

  // Check for WaniKani Open Framework
  if (!wkof) {
      console.warn('WaniKani Pitch Info has extra features enabled by the WK Open Framework.\n Visit: https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549 to install.');
      startup();
  }
  else {
      wkof.include('Menu,Settings');
      wkof.ready('Menu,Settings')
          .then(install_menu)
          .then(startup).then(setupInjectPitchIntoReviewQuestionArea);

      function install_menu() {
          try {
              wkof.Menu.insert_script_link({
                  name:      'wanikani_pitch_info',
                  submenu:   'Settings',
                  title:     'WaniKani Pitch Info',
                  on_click:  open_settings
              });
          } catch (e) {
              console.error(e);
          }

          wkof.Settings.load('wanikani_pitch_info');
      }

      function open_settings() {
          var config = {
              script_id: 'wanikani_pitch_info',
              title: 'WaniKani Pitch Info Settings',
              autosave: true,
              content: {
                  display_pitch_beside_question: {
                      type: 'checkbox',
                      label: 'Display pitch beside question',
                      default: false,
                      hover_tip: 'After successfully completing a reading review, display pitch beside the question.',
                  },
              }
          }
          var dialog = new wkof.Settings(config);
          dialog.open();
      }

      function setupInjectPitchIntoReviewQuestionArea() {
          // Injects pitch accent and reading into question area.
          // Pitch is only displayed when the user enters a correct reading
          window.wkPitchInfoScriptObjectsToRemove = [];
          window.addEventListener('didAnswerQuestion', (ev) => {
              console.log("setup listener");
              // didAnswerQuestion will be triggered whenever the user answers a question
              if (wkof.settings.wanikani_pitch_info?.display_pitch_beside_question && ev.detail.questionType == 'reading' && ev.detail.subjectWithStats.subject.type == 'Vocabulary' && ev.detail.results.action == 'pass') {
                  let divQuestion = document.querySelector(".character-header__characters");
                  wkItemInfo.currentState.reading.forEach(reading => {
                      console.log("reading");
                      // Create a white box in the question area
                      var divOuter = document.createElement("div");
                      divOuter.setAttribute('class', 'additional-content__content additional-content__content--open subject-section subject-section--reading subject-section--collapsible subject-section__subsection subject-readings-with-audio subject-readings-with-audio__item');

                      // Create a div to store the reading
                      var divReading = document.createElement("div");
                      divReading.setAttribute('class', 'reading-with-audio__reading question-pitch-display');
                      divReading.setAttribute('lang', 'ja');
                      divReading.innerHTML = `${reading}`;
                      divOuter.appendChild(divReading);

                      divQuestion.insertAdjacentElement('afterend', divOuter);
                      window.wkPitchInfoScriptObjectsToRemove.push(divOuter);

                      injectPitchInfoToSingleElement(wkItemInfo.currentState, divReading);
                  });
              }
          })

          // Cleans up the objects that we inject into the question area
          window.addEventListener('willShowNextQuestion', (ev) => {
              // willShowNextQuestion will be triggered whenever a new question is to be loaded
              // Register a callback here to clean up the pitches that we insert into the question area.
              window.wkPitchInfoScriptObjectsToRemove.forEach(pObject => {
                  while (pObject.firstChild) { pObject.removeChild(pObject.firstChild); }
                  pObject.remove();
              });
              window.wkPitchInfoScriptObjectsToRemove = [];
          })
      }
  }
  function startup() {
    wkItemInfo.forType('vocabulary').under('reading').notifyWhenVisible(injectPitchInfo);
    wkItemInfo.forType('kanaVocabulary').under('meaning').notifyWhenVisible(injectPitchInfo);
    addCss();
    loadWhileIdle();
  }



  function injectPitchInfoToSingleElement(injectorState, pReading) {
    let reading = pReading.textContent;
    let pitchInfo = getPitchInfo(injectorState.characters, injectorState.type === 'kanaVocabulary' ? '' : reading);
    if (!pitchInfo) return;
    let dInfo = null;
    let wordTypes = [...new Set([...pitchInfo.matchAll(/[\(;]([^\);]*)/g)].flatMap(r => r[1]))];
    if (wordTypes.length > 0) {
      let wordTypeToPitch = wordTypes.map(w => [w, [...pitchInfo.matchAll(new RegExp(w + '[^\\)]*\\)([\\d,]+)', 'g'))].flatMap(r => r[1]).join('').split(',').filter(p => p).map(p => parseInt(p))]);
      dInfo = appendPitchPatternInfo(pReading, pitchByWordTypeToInfoElements(wordTypeToPitch, injectorState.characters, reading));
      pitchInfo = [...new Set([...pitchInfo.matchAll(/\d/g)].map(r => r[0]))].map(p => parseInt(p));
    } else {
      pitchInfo = pitchInfo.split(',').map(p => parseInt(p));
      dInfo = appendPitchPatternInfo(pReading, pitchToInfoElements(pitchInfo, injectorState.characters, reading));
    }
    let diagrams = pitchInfo.map(p => drawPitchDiagram(p, reading));
    pReading.before(...diagrams);
    if ("injector" in injectorState) {
      [...diagrams, dInfo].forEach(d => { if (d) injectorState.injector.registerAppendedElement(d); });
    }
    makeMonospaced(pReading.childNodes[0]);
  }

  function injectPitchInfo(injectorState) {
    document.querySelectorAll('.pronunciation-variant:not(.question-pitch-display), .subject-readings-with-audio__reading:not(.question-pitch-display), .reading-with-audio__reading:not(.question-pitch-display)').forEach(pReading => {
      injectPitchInfoToSingleElement(injectorState, pReading);
    });
  }

  function pitchByWordTypeToInfoElements(wordTypeToPitch, vocab, reading) {
    let result = wordTypeToPitch.flatMap(([wordType, pitch]) => [`${JAPANESE_TO_WORD_TYPE[wordType]}: `, ...pitchToInfoElements(pitch, vocab, reading), ', ']);
    result.pop();
    return result;
  }

  function pitchToInfoElements(pitch, vocab, reading) {
    return pitch.flatMap((p, i) => [i === 0 ? '' : ' or ', generatePatternText(p, vocab, reading)]);
  }

  function appendPitchPatternInfo(readingElement, infoElements) {
    if (!SHOW_PITCH_DESCRIPTION) return null;
    let dInfo = document.createElement('div');
    let hInfo = document.createElement('h3');
    let pInfo = document.createElement('p');
    hInfo.textContent = 'Pitch Pattern';
    dInfo.classList.add('pitch-pattern');
    pInfo.append(...infoElements);
    dInfo.append(hInfo, pInfo);
    readingElement.after(dInfo);
    return dInfo;
  }

  function loadWhileIdle() {
    // for some reason, requestIdleCallback executes loadPitchInfo() while the page is still loading => artificially delay it with setTimeout
    window.setTimeout(() => {
      if (window.requestIdleCallback) window.requestIdleCallback(loadPitchInfo);
      else loadPitchInfo();
    }, 4000);
  }

  function loadPitchInfo() {
    if (pitchLookup) return;
    let accents = GM_getResourceText('accents');
    if (!PRE_PARSE || wkItemInfo.currentState.on === 'itemPage') {
      pitchLookup = (vocab, reading) => pitchLookupTextfile(vocab, reading, accents);
      return;
    }
    let lookupObject = {};
    let matches = accents.matchAll(/^([^\t]+\t[^\t]+)\t(.+)$/gm);
    for (const match of matches) lookupObject[match[1]] = match[2];             // fastest
//  let matches = [...accents.matchAll(/^([^\t]+\t[^\t]+)\t(.+)$/gm)];
//  lookupObject = matches.reduce((o, m) => { o[m[1]] = m[2]; return o; }, {}); // faster
//  lookupObject = Object.fromEntries(matches.map(m => [m[1], m[2]]));          // slower
    pitchLookup = (vocab, reading) => pitchLookupObject(vocab, reading, lookupObject);
  }

  function pitchLookupTextfile(vocab, reading, accents) {
    let key = `\n${vocab}\t${reading}\t`;
    let start = accents.indexOf(key);
    if (start < 0) return null;
    start += key.length;
    let end = accents.indexOf('\n', start);
    return accents.substring(start, end);
  }

  function pitchLookupObject(vocab, reading, lookupObject) {
    return lookupObject[vocab + '\t' + reading];
  }

  function getPitchInfo(vocab, reading) {
    loadPitchInfo();
    let result = pitchLookup(vocab, reading);
    if (!result) result = pitchLookup(vocab.replace(/する$/, ''), reading.replace(/する$/, ''));
    if (!result) result = pitchLookup(toHiragana(vocab), toHiragana(reading));
    if (!result) result = pitchLookup(toKatakana(vocab), toKatakana(reading));
    return result;
  }

  function toHiragana(kana) {
    let arr = [...kana];
    return arr.map(c => c.charCodeAt(0)).map(c => (12449 <= c && c <= 12534) ? c - 96 : c).map(c => String.fromCharCode(c)).join('');
  }

  function toKatakana(kana) {
    let arr = [...kana];
    return arr.map(c => c.charCodeAt(0)).map(c => (12353 <= c && c <= 12438) ? c + 96 : c).map(c => String.fromCharCode(c)).join('');
  }

  function getPitchType(pitchNum, moraCount) {
    if (pitchNum == 0) return patternObj.heiban;
    if (pitchNum == 1) return patternObj.atamadaka;
    if (pitchNum == moraCount) return patternObj.odaka;
    if (pitchNum < moraCount) return patternObj.nakadaka;
    return patternObj.unknown;
  }

  function getMoraCount(reading) {
    return [...reading].filter(c => !digraphs.includes(c)).length;
  }

  function drawPitchDiagram(pitchNum, reading) {
    let moraCount = getMoraCount(reading);
    let width = SQUASH_DIGRAPHS ? moraCount : reading.length;
    let patternType = getPitchType(pitchNum, moraCount);

    let namespace = 'http://www.w3.org/2000/svg';
    let svg = document.createElementNS(namespace, 'svg');
    svg.setAttribute('viewBox', `-0.5 -0.25 ${width + 1} 1.5`);

    let xCoords = [];
    for (let i = 0; i <= reading.length; i++) { // using "<=" to get additional iteration for particle node
      if (!SQUASH_DIGRAPHS && digraphs.includes(reading[i])) {
        xCoords[xCoords.length - 1] += 0.5;
      } else {
        xCoords.push(i);
      }
    }
    let yCoords = new Array(moraCount + 1).fill(null);
    yCoords = yCoords.map((_, i) => {
      if (pitchNum == 0) return i === 0 ? 1 : 0;
      if (i + 1 == pitchNum) return 0;
      if (i === 0) return 1;
      return i < pitchNum ? 0 : 1;
    });
    let points = yCoords.map((y, i) => ({x: xCoords[i], y}));

    let polyline = document.createElementNS(namespace, 'polyline');
    polyline.setAttribute('fill', 'none');
    polyline.setAttribute('stroke', patternType.color);
    polyline.setAttribute('stroke-width', STROKE_WIDTH);
    polyline.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' '));
    svg.appendChild(polyline);

    points.forEach((p, i) => {
      let isParticle = i === points.length - 1;
      let circle = document.createElementNS(namespace, 'circle');
      circle.setAttribute('fill', isParticle ? '#eeeeee' : patternType.color);
      circle.setAttribute('stroke', isParticle ? 'black' : patternType.color);
      circle.setAttribute('stroke-width', isParticle ? STROKE_WIDTH / 2 : 0);
      circle.setAttribute('cx', p.x);
      circle.setAttribute('cy', p.y);
      circle.setAttribute('r', DOT_RADIUS);
      svg.appendChild(circle);
    });
    let p = document.createElement('p');
    p.classList.add('pitch-diagram');
    p.lang = 'ja'; // to match the WK CSS selector containing the reading font size
    p.appendChild(svg);
    return p;
    return svg;
  }

  function generatePatternText(pitchNum, vocab, reading) {
    let moraCount = getMoraCount(reading);
    let patternType = getPitchType(pitchNum, moraCount);
    let sName = document.createElement('span');
    let aLink = document.createElement('a');
    aLink.href = WEB_URL.replace('%s', vocab);
    aLink.target = '_blank';
    aLink.title = `Pitch Pattern: ${patternType.nameEng} (${patternType.name})`;
    aLink.textContent = `[${pitchNum}]`;
    sName.textContent = patternType.name + ' ';
    sName.classList.add(patternType.cssClass);
    sName.appendChild(aLink);
    return sName;
  }

  function makeMonospaced(textNode) {
    let characters = [...textNode.textContent];
    if (SQUASH_DIGRAPHS) {
      characters.forEach((c, i, a) => { if (digraphs.includes(c)) a[i - 1] += c; });
      characters = characters.filter(c => !digraphs.includes(c));
    }
    let spans = characters.map(c => {
      let span = document.createElement('span');
      span.textContent = c;
      span.classList.toggle('digraph', c.length > 1);
      return span;
    });
    textNode.replaceWith(...spans);
  }

  function addCss() {
    let style = document.createElement('style');
    style.textContent = `
      .pronunciation-group svg           , .subject-readings-with-audio__item svg             { height: 1.5em; width: auto; display: block; }
      .pronunciation-variant             , .subject-readings-with-audio__reading              { line-height: 2.2em; margin: 0; }
      .pronunciation-variant span        , .subject-readings-with-audio__reading span         { width: 1em; display: inline-block; text-align: center; white-space: nowrap; }
      .pronunciation-variant span.digraph, .subject-readings-with-audio__reading span.digraph { font-feature-settings: 'hwid' on; }
      .pitch-pattern                                                                          { display: flex; margin-bottom: 0; color: #999; text-transform: uppercase; }
      .pitch-pattern h3, #item-info .pitch-pattern h3                                         { margin: 0 1em 0 0; padding: 0; font-size: 11px; font-weight: bold; letter-spacing: 0; border-bottom: none; line-height: 1.6em; }
      .pitch-pattern p                                                                        { font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 11px; flex: 1 0 auto; margin: 0; }
      .pitch-diagram.pitch-diagram.pitch-diagram.pitch-diagram                                { margin: 0; display: block; font-size: 18px; }
      .pitch-pattern + .subject-readings-with-audio__audio-items                              { margin-top: 0.6em; }
      .character-header .question-pitch-display > span                                        { color: rgb(255 255 255); }
      .character-header .additional-content__content:has(> .pitch-diagram) { background-color: rgba(0.2, 0.2, 0.2, 0.2); border-style: none; padding: 6px 8px; box-shadow: rgb(227, 227, 227) 0px 2px 4px !important; text-shadow: 0 2px black; }
      ${Object.values(patternObj).map(({color, cssClass}) => `.${cssClass} { color: ${color}; }`).join('')}`;
    document.head.appendChild(style);
  }
})();

1 Like