diff --git a/.npmrc b/.npmrc
index ded82e2f63f..a8365e17408 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1,4 @@
auto-install-peers = true
+# Perseus v75 uses SWC-compiled code that requires @swc/helpers at runtime,
+# but doesn't declare it as a dependency. Hoisting makes it resolvable by webpack.
+public-hoist-pattern[]=@swc/helpers
diff --git a/kolibri/plugins/perseus_viewer/.gitignore b/kolibri/plugins/perseus_viewer/.gitignore
index 4cdf84f3f5e..ca9992156b6 100644
--- a/kolibri/plugins/perseus_viewer/.gitignore
+++ b/kolibri/plugins/perseus_viewer/.gitignore
@@ -1,4 +1,2 @@
-submodules
-!static/mathjax
-!frontend/dist
-!frontend/dist/fonts/*.ttf
+# MathJax fonts are copied from the mathjax-full node_modules dep at build time
+static/assets/mathjax/fonts/
diff --git a/kolibri/plugins/perseus_viewer/buildConfig.js b/kolibri/plugins/perseus_viewer/buildConfig.js
index 1607cf6de99..4829b288635 100644
--- a/kolibri/plugins/perseus_viewer/buildConfig.js
+++ b/kolibri/plugins/perseus_viewer/buildConfig.js
@@ -2,16 +2,103 @@
* This file defines additional webpack configuration for this plugin.
* It will be bundled into the webpack configuration at build time.
*/
+var fs = require('fs');
+var path = require('path');
var webpack = require('webpack');
+// mathjax-full is a transitive dependency of @khanacademy/mathjax-renderer
+// and lives as a sibling in pnpm's virtual store.
+var mathjaxRendererDir = path.dirname(
+ require.resolve('@khanacademy/mathjax-renderer/package.json'),
+);
+var mathjaxFontsSource = path.join(
+ mathjaxRendererDir,
+ '../../mathjax-full/ts/output/chtml/fonts/tex-woff-v2',
+);
+var mathjaxFontsTarget = path.resolve(__dirname, 'static/assets/mathjax/fonts');
+
+// Copy MathJax fonts into the plugin's static assets directory before each
+// build. Django serves them at runtime via urls.static('assets/mathjax/fonts'),
+// and MathJaxRenderer fetches them by bare filename, so they must keep their
+// original names. The target directory is gitignored.
+function copyMathJaxFonts() {
+ fs.mkdirSync(mathjaxFontsTarget, { recursive: true });
+ for (const file of fs.readdirSync(mathjaxFontsSource)) {
+ if (!file.endsWith('.woff')) continue;
+ fs.copyFileSync(
+ path.join(mathjaxFontsSource, file),
+ path.join(mathjaxFontsTarget, file),
+ );
+ }
+}
+
+class CopyMathJaxFontsPlugin {
+ apply(compiler) {
+ const run = (_, cb) => {
+ try {
+ copyMathJaxFonts();
+ cb();
+ } catch (err) {
+ cb(err);
+ }
+ };
+ compiler.hooks.beforeRun.tapAsync('CopyMathJaxFontsPlugin', run);
+ compiler.hooks.watchRun.tapAsync('CopyMathJaxFontsPlugin', run);
+ }
+}
+
module.exports = {
bundle_id: 'main',
webpack_config: {
entry: 'frontend/module.js',
+ resolve: {
+ alias: {
+ // Alias for MathJax fonts so the Perseus CDN URL rewriter can
+ // reference them via ~mathjax-fonts/... in CSS.
+ 'mathjax-fonts': mathjaxFontsSource,
+ },
+ },
+ module: {
+ rules: [
+ {
+ // Rewrite the KA CDN font URL in Perseus CSS to our local copy.
+ // Runs as a pre-loader before css-loader processes url() refs.
+ test: /@khanacademy[/\\]perseus[/\\]dist[/\\]index\.css$/,
+ enforce: 'pre',
+ use: [
+ path.resolve(__dirname, 'rewritePerseusUrls.js'),
+ path.resolve(__dirname, 'rewritePerseusRem.js'),
+ ],
+ },
+ {
+ // Convert rem→px in Wonder Blocks design tokens CSS.
+ // These tokens assume 1rem = 10px (KA's root font-size convention).
+ test: /@khanacademy[/\\]wonder-blocks-tokens[/\\].*\.css$/,
+ enforce: 'pre',
+ loader: path.resolve(__dirname, 'rewritePerseusRem.js'),
+ },
+ {
+ // Convert rem→px in math-input CSS (same KA rem convention).
+ test: /@khanacademy[/\\]math-input[/\\].*\.css$/,
+ enforce: 'pre',
+ loader: path.resolve(__dirname, 'rewritePerseusRem.js'),
+ },
+ ],
+ },
plugins: [
+ new CopyMathJaxFontsPlugin(),
new webpack.NormalModuleReplacementPlugin(
/react\/jsx-runtime/,
- require.resolve('react/jsx-runtime.js'),
+ require.resolve('react/jsx-runtime'),
+ ),
+ // Wonder Blocks components import from react-router-dom-v5-compat,
+ // which re-exports from react-router@6. The Perseus plugin only has
+ // react-router@5, so the v6 APIs (useInRouterContext, useNavigate)
+ // are undefined. Replace with a shim that returns "no router" so
+ // Wonder Blocks falls back to plain tags.
+ new webpack.NormalModuleReplacementPlugin(
+ /react-router-dom-v5-compat/,
+ path.resolve(__dirname, 'frontend', 'reactRouterShim.js'),
),
],
},
diff --git a/kolibri/plugins/perseus_viewer/buildPerseus.js b/kolibri/plugins/perseus_viewer/buildPerseus.js
index 6ae3c79eb6c..8a7fa57de34 100644
--- a/kolibri/plugins/perseus_viewer/buildPerseus.js
+++ b/kolibri/plugins/perseus_viewer/buildPerseus.js
@@ -1,44 +1,3 @@
-/* eslint-disable import-x/no-commonjs, import-x/no-amd, import-x/no-import-module-exports */
-const fs = require('fs');
-const path = require('path');
const extractPerseusMessages = require('./extractPerseusMessages');
-const target = path.resolve(__dirname, './frontend/dist');
-// A regex for detecting paths inside `url` in CSS, paths can either be quoted or unquoted.
-const cssPathRegex = /(url\(['"]?)([^"')]+)?(['"]?\),? ?)/g;
-const cssNonWoffRegex = /, (url\(['"]?)([^"')]+)?(['"]?\),? ?) format\((?!['"]woff['"])['"][a-z0-9]+['"]\)/g;
-// These are the css files that we are modifying to remap static assets.
-const cssFiles = [
- [
- path.join(path.dirname(require.resolve('@khanacademy/perseus')), 'index.css'),
- path.join(target, 'index.css'),
- ],
- [
- path.join(path.dirname(require.resolve('@khanacademy/math-input')), 'index.css'),
- path.join(target, 'math-input.css'),
- ],
-];
-
-for (const [indexCssFile, targetCssLocation] of cssFiles) {
- console.log('Copying file and editing references for: ', indexCssFile);
- const cssFileContents = fs.readFileSync(indexCssFile, { encoding: 'utf-8' });
- const modifiedCssContents = cssFileContents.replace(cssPathRegex, function(match, p1, p2, p3) {
- // Special case for MathJax font loaded from KA CDN
- if (p2 === 'https://cdn.kastatic.org/fonts/mathjax/MathJax_Main-Regular.woff') {
- return `${p1}fonts/MathJax_Main-Regular.woff${p3}`;
- }
- // Make absolute paths relative
- const absolute = p2.startsWith('/');
- const newUrl = absolute ? p2.slice(1) : p2;
- if (newUrl) {
- // If so, replace the instance with the new URL.
- return `${p1}${newUrl}${p3}`;
- }
- // Otherwise just return empty string so that we remove the unfound file from the CSS.
- return '';
- }).replace(cssNonWoffRegex, '').replace(/\s+src: url\(fonts\/Symbola\.eot\);/, '');
- fs.writeFileSync(targetCssLocation, modifiedCssContents, { encoding: 'utf-8' });
-}
-
-// Now that the file has been built, we can extract all the perseus messages.
extractPerseusMessages();
diff --git a/kolibri/plugins/perseus_viewer/extractPerseusMessages.js b/kolibri/plugins/perseus_viewer/extractPerseusMessages.js
index 0d203d3cb6c..5da8f009e77 100644
--- a/kolibri/plugins/perseus_viewer/extractPerseusMessages.js
+++ b/kolibri/plugins/perseus_viewer/extractPerseusMessages.js
@@ -1,42 +1,23 @@
/* eslint-disable import-x/no-commonjs, import-x/no-amd, import-x/no-import-module-exports */
/*
- * A utility that extracts Perseus messages into a Javascript file for compatibility
- * with our i18n machinery. Also converts them into ICU format in the process.
+ * Extracts Perseus and math-input strings into a translator module compatible
+ * with our i18n machinery. Reads the strings objects straight out of each
+ * package's built dist, converts gettext-style %(name)s tokens to ICU syntax,
+ * and preserves any `context` values already present in the existing
+ * translator.js so hand-maintained context notes survive re-runs.
*/
const fs = require('node:fs');
const path = require('node:path');
-const https = require('node:https');
-
-// We already have lodash installed, so use it for templating the code we generate
const lodash = require('lodash');
-const typescript = require('typescript');
-
const { writeSourceToFile } = require('kolibri-format');
const { replacePiText } = require('./frontend/translationUtils');
-const packageJson = require('./package.json');
-
-const perseusVersion = packageJson.dependencies['@khanacademy/perseus'];
-
-// Auto generate a module that creates the translator so that it can
-// be imported into our special i18n code for Perseus.
-
-const perseusStringFileUrl = `https://raw.githubusercontent.com/Khan/perseus/@khanacademy/perseus@${perseusVersion}/packages/perseus/src/strings.ts`;
-const mathInputStringFileUrl = `https://raw.githubusercontent.com/Khan/perseus/@khanacademy/perseus@${perseusVersion}/packages/math-input/src/strings.ts`;
// Regex taken from perseus/lib/i18n.js interpolationMarker variable
const gettextRegex = /%\(([\w_]+)\)s/g;
-/*
- * A function to transform Perseus' gettext formatted messages to ICU message syntax
- * Can be used replace all strings in a source file,
- * Or on a string by string basis to convert gettext formatted strings into ICU syntax,
- * For example when importing Khan Academy's gettext format translated strings.
- * It also normalizes the way pi is represented to make it not cause errors for ICU message syntax.
- * Finally, it escapes all backslashes to prevent errors in ICU token parsing.
- */
function normalizeString(string) {
return replacePiText(string.replace(gettextRegex, '{ $1 }')).replace(/\\/g, '\\\\');
}
@@ -45,15 +26,15 @@ function normalizeStringObject(stringObject) {
const normalizedObject = {};
for (const key in stringObject) {
if (lodash.isPlainObject(stringObject[key])) {
- if (stringObject[key]['message']) {
+ if (stringObject[key].message) {
normalizedObject[key] = {
- message: normalizeString(stringObject[key]['message']),
- context: stringObject[key]['context'],
+ message: normalizeString(stringObject[key].message),
+ context: stringObject[key].context,
};
- } else if (stringObject[key]['one'] && stringObject[key]['other']) {
- const oneMessage = normalizeString(stringObject[key]['one']).trim();
- const otherMessage = normalizeString(stringObject[key]['other']).trim();
- const varName = gettextRegex.exec(stringObject[key]['one'])[1];
+ } else if (stringObject[key].one && stringObject[key].other) {
+ const oneMessage = normalizeString(stringObject[key].one).trim();
+ const otherMessage = normalizeString(stringObject[key].other).trim();
+ const varName = gettextRegex.exec(stringObject[key].one)[1];
normalizedObject[key] = `{${varName}, plural, one {${oneMessage}} other {${otherMessage}}}`;
} else {
console.error('Unrecognized string object:', stringObject[key]);
@@ -65,52 +46,71 @@ function normalizeStringObject(stringObject) {
return normalizedObject;
}
-
-async function downloadFileAndGetMessages(urlPath, moduleName) {
+const translatorPath = path.join(__dirname, 'frontend/translator.js');
+
+// Capture contexts from the existing translator.js by stubbing createTranslator
+// and evaluating the file. Upstream flattened several strings to plain values in
+// v75, so preserving our own contexts is the only way to keep them.
+function readExistingContexts() {
+ if (!fs.existsSync(translatorPath)) return {};
+ const source = fs.readFileSync(translatorPath, 'utf8');
+ const captured = {};
+ const stub = (_name, strings) => Object.assign(captured, strings);
+ const evaluate = new Function('createTranslator', `
+ ${source.replace(/^import\s+.*$/gm, '').replace(/export\s+default\s+/, 'return ')}
+ `);
try {
- const data = await new Promise((resolve, reject) => {
- const req = https.get(encodeURI(urlPath), (res) => {
- if (res.statusCode !== 200) {
- console.error('Error downloading file:', res);
- return;
- }
- const chunks = [];
- res.on('data', (chunk) => chunks.push(chunk));
- res.on('end', () => {
- const buffer = Buffer.concat(chunks);
- resolve(buffer);
- });
- });
- req.on('error', (error) => reject(error));
- req.end();
- });
- const tempFilePath = path.join(__dirname, moduleName + 'tempFile.js');
- const jsSource = typescript.transpileModule(data.toString(), { compilerOptions: { module: typescript.ModuleKind.CommonJS }});
- fs.writeFileSync(tempFilePath, Buffer.from(jsSource.outputText));
- const module = require(tempFilePath);
- fs.unlinkSync(tempFilePath);
- return normalizeStringObject(module.strings);
- } catch (error) {
- console.error('Error downloading file:', error);
- throw error;
+ evaluate(stub);
+ } catch (err) {
+ console.error('Could not parse existing translator.js for context preservation:', err);
+ return {};
+ }
+ const contexts = {};
+ for (const key of Object.keys(captured)) {
+ if (lodash.isPlainObject(captured[key]) && captured[key].context) {
+ contexts[key] = captured[key].context;
+ }
+ }
+ return contexts;
+}
+
+function applyPreservedContexts(allStrings, existingContexts) {
+ for (const key of Object.keys(existingContexts)) {
+ const ctx = existingContexts[key];
+ const current = allStrings[key];
+ if (typeof current === 'string') {
+ allStrings[key] = { message: current, context: ctx };
+ } else if (lodash.isPlainObject(current) && !current.context) {
+ current.context = ctx;
+ }
}
}
-module.exports = async function() {
- const perseusStrings = await downloadFileAndGetMessages(perseusStringFileUrl, 'perseus');
- const mathInputStrings = await downloadFileAndGetMessages(mathInputStringFileUrl, 'mathInput');
+module.exports = function extractPerseusMessages() {
+ const perseusStrings = normalizeStringObject(
+ require('@khanacademy/perseus/strings').strings,
+ );
+ const mathInputStrings = normalizeStringObject(
+ require('@khanacademy/math-input/strings').strings,
+ );
const allStrings = {
...perseusStrings,
- // There is one duplicate key between the two files
- // we will prefer the math input one for now, as it seems more useful.
+ // There is one duplicate key between the two files; prefer math-input.
...mathInputStrings,
};
for (const key in allStrings) {
- if (perseusStrings[key] && mathInputStrings[key] && perseusStrings[key] !== mathInputStrings[key]) {
+ if (
+ perseusStrings[key] &&
+ mathInputStrings[key] &&
+ perseusStrings[key] !== mathInputStrings[key]
+ ) {
if (lodash.isPlainObject(perseusStrings[key]) && lodash.isPlainObject(mathInputStrings[key])) {
- if (perseusStrings[key].message === mathInputStrings[key].message && perseusStrings[key].context === mathInputStrings[key].context) {
+ if (
+ perseusStrings[key].message === mathInputStrings[key].message &&
+ perseusStrings[key].context === mathInputStrings[key].context
+ ) {
continue;
}
}
@@ -120,18 +120,12 @@ module.exports = async function() {
}
}
- // Use lodash template to fill in the above 'messages' into the template
- let outputCode = `
+ applyPreservedContexts(allStrings, readExistingContexts());
- import { createTranslator } from 'kolibri.utils.i18n';
-
-
- export default createTranslator('PerseusInternalMessages',
- `;
- outputCode += JSON.stringify(allStrings, null, 2);
- outputCode += ');'
-
- // Write out the module to src files
+ const outputCode =
+ "\n\n import { createTranslator } from 'kolibri/utils/i18n';\n\n\n export default createTranslator('PerseusInternalMessages',\n " +
+ JSON.stringify(allStrings, null, 2) +
+ ');';
writeSourceToFile('./frontend/translator.js', outputCode);
-}
+};
diff --git a/kolibri/plugins/perseus_viewer/frontend/__tests__/fixtures/sorter-item.json b/kolibri/plugins/perseus_viewer/frontend/__tests__/fixtures/sorter-item.json
new file mode 100644
index 00000000000..256a4eb0eed
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/__tests__/fixtures/sorter-item.json
@@ -0,0 +1,29 @@
+{
+ "question": {
+ "content": "Sort the numbers in ascending order.\n\n[[☃ sorter 1]]",
+ "images": {},
+ "widgets": {
+ "sorter 1": {
+ "type": "sorter",
+ "alignment": "default",
+ "static": false,
+ "graded": true,
+ "options": {
+ "correct": ["1", "2", "3", "4", "5"],
+ "layout": "horizontal",
+ "padding": true
+ },
+ "version": { "major": 0, "minor": 0 }
+ }
+ }
+ },
+ "answerArea": {
+ "calculator": false,
+ "chi2Table": false,
+ "periodicTable": false,
+ "tTable": false,
+ "zTable": false
+ },
+ "itemDataVersion": { "major": 0, "minor": 1 },
+ "hints": []
+}
diff --git a/kolibri/plugins/perseus_viewer/frontend/__tests__/numeralNormalization.spec.js b/kolibri/plugins/perseus_viewer/frontend/__tests__/numeralNormalization.spec.js
new file mode 100644
index 00000000000..911afec041e
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/__tests__/numeralNormalization.spec.js
@@ -0,0 +1,286 @@
+import {
+ normalizeNumerals,
+ normalizeUserInput,
+ getLocalizedDigits,
+ localizeNumerals,
+ localizeUserInput,
+} from '../numeralNormalization';
+
+describe('normalizeNumerals', () => {
+ it('converts Eastern Arabic digits to ASCII', () => {
+ expect(normalizeNumerals('٠١٢٣٤٥٦٧٨٩')).toBe('0123456789');
+ });
+
+ it('converts Extended Arabic-Indic digits to ASCII', () => {
+ expect(normalizeNumerals('۰۱۲۳۴۵۶۷۸۹')).toBe('0123456789');
+ });
+
+ it('converts Devanagari digits to ASCII', () => {
+ expect(normalizeNumerals('०१२३४५६७८९')).toBe('0123456789');
+ });
+
+ it('converts Bengali digits to ASCII', () => {
+ expect(normalizeNumerals('০১২৩৪৫৬৭৮৯')).toBe('0123456789');
+ });
+
+ it('converts Thai digits to ASCII', () => {
+ expect(normalizeNumerals('๐๑๒๓๔๕๖๗๘๙')).toBe('0123456789');
+ });
+
+ it('converts Myanmar digits to ASCII', () => {
+ expect(normalizeNumerals('၀၁၂၃၄၅၆၇၈၉')).toBe('0123456789');
+ });
+
+ it('converts Khmer digits to ASCII', () => {
+ expect(normalizeNumerals('០១២៣៤៥៦៧៨៩')).toBe('0123456789');
+ });
+
+ it('leaves ASCII digits unchanged', () => {
+ expect(normalizeNumerals('0123456789')).toBe('0123456789');
+ });
+
+ it('leaves non-digit characters unchanged', () => {
+ expect(normalizeNumerals('abc + xyz')).toBe('abc + xyz');
+ });
+
+ it('handles mixed ASCII and non-Western digits', () => {
+ expect(normalizeNumerals('٤2')).toBe('42');
+ });
+
+ it('handles expressions with non-Western digits', () => {
+ expect(normalizeNumerals('٢x+٣')).toBe('2x+3');
+ });
+
+ it('handles decimal numbers with Eastern Arabic digits', () => {
+ expect(normalizeNumerals('٣.١٤')).toBe('3.14');
+ });
+
+ it('handles fractions with Devanagari digits', () => {
+ expect(normalizeNumerals('२१/३')).toBe('21/3');
+ });
+
+ it('returns non-string values unchanged', () => {
+ expect(normalizeNumerals(42)).toBe(42);
+ expect(normalizeNumerals(null)).toBe(null);
+ expect(normalizeNumerals(undefined)).toBe(undefined);
+ expect(normalizeNumerals(true)).toBe(true);
+ });
+
+ it('returns empty string unchanged', () => {
+ expect(normalizeNumerals('')).toBe('');
+ });
+
+ it('handles negative numbers', () => {
+ expect(normalizeNumerals('-٤٢')).toBe('-42');
+ });
+});
+
+describe('normalizeUserInput', () => {
+ it('normalizes numeric-input widget state', () => {
+ const input = {
+ 'numeric-input 1': { currentValue: '٤٢' },
+ };
+ expect(normalizeUserInput(input)).toEqual({
+ 'numeric-input 1': { currentValue: '42' },
+ });
+ });
+
+ it('normalizes expression widget state (plain string)', () => {
+ const input = {
+ 'expression 1': '٢x+٣',
+ };
+ expect(normalizeUserInput(input)).toEqual({
+ 'expression 1': '2x+3',
+ });
+ });
+
+ it('leaves radio widget state unchanged (no digits in choice IDs)', () => {
+ const input = {
+ 'radio 1': { selectedChoiceIds: ['radio-choice-1'] },
+ };
+ expect(normalizeUserInput(input)).toEqual({
+ 'radio 1': { selectedChoiceIds: ['radio-choice-1'] },
+ });
+ });
+
+ it('leaves dropdown widget state unchanged (numeric value)', () => {
+ const input = {
+ 'dropdown 1': { value: 2 },
+ };
+ expect(normalizeUserInput(input)).toEqual({
+ 'dropdown 1': { value: 2 },
+ });
+ });
+
+ it('handles multiple widgets in one input', () => {
+ const input = {
+ 'numeric-input 1': { currentValue: '٢١' },
+ 'numeric-input 2': { currentValue: '٧' },
+ 'expression 1': '٥x',
+ };
+ expect(normalizeUserInput(input)).toEqual({
+ 'numeric-input 1': { currentValue: '21' },
+ 'numeric-input 2': { currentValue: '7' },
+ 'expression 1': '5x',
+ });
+ });
+
+ it('handles null and undefined gracefully', () => {
+ expect(normalizeUserInput(null)).toBe(null);
+ expect(normalizeUserInput(undefined)).toBe(undefined);
+ });
+
+ it('handles nested arrays', () => {
+ const input = ['٤', ['٢', '٣']];
+ expect(normalizeUserInput(input)).toEqual(['4', ['2', '3']]);
+ });
+});
+
+describe('normalizeNumerals selectivity', () => {
+ it('normalizes Eastern Arabic digits but not surrounding text', () => {
+ expect(normalizeNumerals('٤٢')).toBe('42');
+ });
+
+ it('does not modify ASCII digits', () => {
+ expect(normalizeNumerals('42')).toBe('42');
+ });
+
+ it('does not modify letters', () => {
+ expect(normalizeNumerals('abc')).toBe('abc');
+ });
+});
+
+describe('getLocalizedDigits', () => {
+ it('returns null for English locale', () => {
+ expect(getLocalizedDigits('en')).toBeNull();
+ });
+
+ it('returns null for null/undefined locale', () => {
+ expect(getLocalizedDigits(null)).toBeNull();
+ expect(getLocalizedDigits(undefined)).toBeNull();
+ });
+
+ it('returns localized digits for Arabic locale', () => {
+ const digits = getLocalizedDigits('ar-EG');
+ // ar-EG uses Eastern Arabic numerals
+ if (digits) {
+ expect(digits).toHaveLength(10);
+ expect(digits[0]).toBe('٠');
+ expect(digits[1]).toBe('١');
+ expect(digits[9]).toBe('٩');
+ }
+ });
+
+ it('returns null for locales whose digits match ASCII', () => {
+ // French, German, Spanish all use the same digits as ASCII
+ expect(getLocalizedDigits('fr')).toBeNull();
+ expect(getLocalizedDigits('de')).toBeNull();
+ expect(getLocalizedDigits('es')).toBeNull();
+ });
+});
+
+describe('localizeNumerals', () => {
+ it('converts ASCII digits to Eastern Arabic for ar-EG', () => {
+ expect(localizeNumerals('42', 'ar-EG')).toBe('٤٢');
+ });
+
+ it('converts all 10 digits for Arabic locale', () => {
+ expect(localizeNumerals('0123456789', 'ar-EG')).toBe('٠١٢٣٤٥٦٧٨٩');
+ });
+
+ it('leaves non-digit characters unchanged', () => {
+ expect(localizeNumerals('x+y', 'ar-EG')).toBe('x+y');
+ });
+
+ it('handles mixed digits and text', () => {
+ expect(localizeNumerals('2x+3', 'ar-EG')).toBe('٢x+٣');
+ });
+
+ it('handles decimal numbers', () => {
+ expect(localizeNumerals('3.14', 'ar-EG')).toBe('٣.١٤');
+ });
+
+ it('returns string unchanged for English locale', () => {
+ expect(localizeNumerals('42', 'en')).toBe('42');
+ });
+
+ it('returns string unchanged for null locale', () => {
+ expect(localizeNumerals('42', null)).toBe('42');
+ });
+
+ it('returns non-string values unchanged', () => {
+ expect(localizeNumerals(42, 'ar-EG')).toBe(42);
+ expect(localizeNumerals(null, 'ar-EG')).toBe(null);
+ expect(localizeNumerals(undefined, 'ar-EG')).toBe(undefined);
+ });
+
+ it('returns empty string unchanged', () => {
+ expect(localizeNumerals('', 'ar-EG')).toBe('');
+ });
+});
+
+describe('localizeUserInput', () => {
+ it('localizes numeric-input widget state', () => {
+ const input = {
+ 'numeric-input 1': { currentValue: '42' },
+ };
+ expect(localizeUserInput(input, 'ar-EG')).toEqual({
+ 'numeric-input 1': { currentValue: '٤٢' },
+ });
+ });
+
+ it('localizes expression widget state (plain string)', () => {
+ const input = {
+ 'expression 1': '2x+3',
+ };
+ expect(localizeUserInput(input, 'ar-EG')).toEqual({
+ 'expression 1': '٢x+٣',
+ });
+ });
+
+ it('leaves dropdown widget state unchanged (numeric value)', () => {
+ const input = {
+ 'dropdown 1': { value: 2 },
+ };
+ expect(localizeUserInput(input, 'ar-EG')).toEqual({
+ 'dropdown 1': { value: 2 },
+ });
+ });
+
+ it('handles multiple widgets', () => {
+ const input = {
+ 'numeric-input 1': { currentValue: '21' },
+ 'numeric-input 2': { currentValue: '7' },
+ 'expression 1': '5x',
+ };
+ expect(localizeUserInput(input, 'ar-EG')).toEqual({
+ 'numeric-input 1': { currentValue: '٢١' },
+ 'numeric-input 2': { currentValue: '٧' },
+ 'expression 1': '٥x',
+ });
+ });
+
+ it('returns input unchanged for English locale', () => {
+ const input = { 'numeric-input 1': { currentValue: '42' } };
+ expect(localizeUserInput(input, 'en')).toEqual(input);
+ });
+
+ it('handles null and undefined gracefully', () => {
+ expect(localizeUserInput(null, 'ar-EG')).toBe(null);
+ expect(localizeUserInput(undefined, 'ar-EG')).toBe(undefined);
+ });
+
+ it('handles nested arrays', () => {
+ const input = ['4', ['2', '3']];
+ expect(localizeUserInput(input, 'ar-EG')).toEqual(['٤', ['٢', '٣']]);
+ });
+
+ it('is the inverse of normalizeUserInput for Arabic', () => {
+ const original = {
+ 'numeric-input 1': { currentValue: '42' },
+ 'expression 1': '2x+3',
+ };
+ const localized = localizeUserInput(original, 'ar-EG');
+ expect(normalizeUserInput(localized)).toEqual(original);
+ });
+});
diff --git a/kolibri/plugins/perseus_viewer/frontend/__tests__/stateSerialization.spec.js b/kolibri/plugins/perseus_viewer/frontend/__tests__/stateSerialization.spec.js
new file mode 100644
index 00000000000..69ea87992d4
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/__tests__/stateSerialization.spec.js
@@ -0,0 +1,87 @@
+import { parseAndMigratePerseusItem, isFailure } from '@khanacademy/perseus-core';
+import { deriveUserInputFromSerializedState } from '@khanacademy/perseus';
+import sorterItem from './fixtures/sorter-item.json';
+
+function migrateItem(rawItem) {
+ const result = parseAndMigratePerseusItem(rawItem);
+ if (isFailure(result)) {
+ throw new Error('Failed to migrate item: ' + result.detail.message);
+ }
+ return result.value;
+}
+
+describe('state serialization', () => {
+ // The sorter widget required special handling in the old Perseus integration.
+ // Pre-v75, getSerializedState() didn't include the sorter's current ordering,
+ // so addSorterState() manually extracted it via refs.sortable.getOptions().
+ // This means saved answer states in the wild have sorter state with an
+ // `options` array that needs to be properly converted by
+ // deriveUserInputFromSerializedState.
+
+ describe('sorter widget backward compatibility', () => {
+ let item;
+ beforeAll(() => {
+ item = migrateItem(sorterItem);
+ });
+
+ it('preserves the options ordering from old serialized state', () => {
+ // Old format: addSorterState injected the options array into the
+ // serialized state from the sortable component's getOptions().
+ // The correct answer order for the fixture is ['1', '2', '3', '4', '5'].
+ const oldSorterState = {
+ 'sorter 1': { options: ['1', '2', '3', '4', '5'] },
+ };
+ const userInput = deriveUserInputFromSerializedState(oldSorterState, item.question.widgets);
+ expect(userInput['sorter 1']).toBeDefined();
+ expect(userInput['sorter 1'].options).toEqual(['1', '2', '3', '4', '5']);
+ });
+
+ it('preserves a scrambled ordering from old serialized state', () => {
+ // A user who hadn't finished sorting would have a different order saved.
+ const oldSorterState = {
+ 'sorter 1': { options: ['5', '3', '1', '4', '2'] },
+ };
+ const userInput = deriveUserInputFromSerializedState(oldSorterState, item.question.widgets);
+ expect(userInput['sorter 1'].options).toEqual(['5', '3', '1', '4', '2']);
+ });
+
+ it('requires unwrapped question state, not the { question, hints } wrapper', () => {
+ // restoreAnswerState() in PerseusRendererIndex.vue passes answerState.question
+ // (the inner widget state map) to deriveUserInputFromSerializedState, NOT the
+ // full { question, hints } wrapper.
+ const wrapperResult = deriveUserInputFromSerializedState(
+ { question: { 'sorter 1': { options: ['1', '2', '3', '4', '5'] } }, hints: [] },
+ item.question.widgets,
+ );
+ expect(wrapperResult['sorter 1']).toBeUndefined();
+
+ const unwrappedResult = deriveUserInputFromSerializedState(
+ { 'sorter 1': { options: ['1', '2', '3', '4', '5'] } },
+ item.question.widgets,
+ );
+ expect(unwrappedResult['sorter 1'].options).toEqual(['1', '2', '3', '4', '5']);
+ });
+ });
+
+ describe('blob URL handling', () => {
+ it('replaces blob URLs with LOCALPATH placeholders in serialized state', () => {
+ // Test the pattern used in restoreImageUrls
+ const blobImageRegex = /blob:[^)^"]+/g;
+ const stateWithBlob = JSON.stringify({
+ userInput: {
+ 'image 1': { url: 'blob:http://localhost/abc123' },
+ },
+ });
+ const hasBlob = blobImageRegex.test(stateWithBlob);
+ expect(hasBlob).toBe(true);
+ });
+
+ it('matches LOCALPATH placeholders for restoration', () => {
+ const allImageRegex = /((web\+graphie:)?)\$\{☣ LOCALPATH\}\/([^)^"]+)/g;
+ const stateWithPlaceholder = '${☣ LOCALPATH}/images/test.png';
+ const matches = Array.from(stateWithPlaceholder.matchAll(allImageRegex));
+ expect(matches.length).toBe(1);
+ expect(matches[0][3]).toBe('images/test.png');
+ });
+ });
+});
diff --git a/kolibri/plugins/perseus_viewer/frontend/composables/useKeypad.js b/kolibri/plugins/perseus_viewer/frontend/composables/useKeypad.js
new file mode 100644
index 00000000000..7dc5064926d
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/composables/useKeypad.js
@@ -0,0 +1,108 @@
+import { ref, readonly, provide, inject } from 'vue';
+
+const KeypadSymbol = Symbol('keypad');
+
+/**
+ * Composable that owns the keypad state and exposes the KeypadAPI interface
+ * expected by Perseus widgets.
+ *
+ * Call this in the parent component's setup(). It provides keypad state to
+ * descendants via provide/inject. Child components use injectKeypad() to
+ * access the keypad.
+ *
+ * Returns the keypadAPI (for passing to Perseus as keypadElement) and a
+ * keypadContextValue (for bridging React's KeypadContext.Provider).
+ */
+export default function useKeypad() {
+ const active = ref(false);
+ const cursor = ref(null);
+ const keypadConfig = ref(null);
+ const keyHandler = ref(null);
+
+ function activate() {
+ active.value = true;
+ }
+
+ function dismiss() {
+ active.value = false;
+ }
+
+ function configure(configuration, cb) {
+ keypadConfig.value = configuration;
+ if (cb) {
+ setTimeout(cb);
+ }
+ }
+
+ function setCursor(c) {
+ cursor.value = c;
+ }
+
+ function setKeyHandler(handler) {
+ keyHandler.value = handler;
+ }
+
+ function handleKeyPress(keyId) {
+ if (keyHandler.value) {
+ cursor.value = keyHandler.value(keyId);
+ }
+ }
+
+ // The API object passed to Perseus as keypadElement.
+ // This must remain a stable reference — Perseus stores it.
+ const keypadAPI = {
+ activate,
+ dismiss,
+ configure,
+ setCursor,
+ setKeyHandler,
+ // getDOMNode is called by MathInput to detect clicks outside the keypad.
+ // We set this later once the keypad component mounts.
+ getDOMNode: () => keypadAPI._domNode || null,
+ _domNode: null,
+ };
+
+ // Bridge React's KeypadContext to our Vue keypad state.
+ // MathInput calls setKeypadActive(true) on focus — this must
+ // reach our Vue keypad's activate/dismiss.
+ const keypadContextValue = {
+ keypadActive: false,
+ setKeypadActive: isActive => {
+ if (isActive) {
+ activate();
+ } else {
+ dismiss();
+ }
+ keypadContextValue.keypadActive = isActive;
+ },
+ keypadElement: keypadAPI,
+ setKeypadElement: () => {},
+ renderer: null,
+ setRenderer: () => {},
+ scrollableElement: null,
+ setScrollableElement: () => {},
+ };
+
+ provide(KeypadSymbol, {
+ active: readonly(active),
+ keypadConfig: readonly(keypadConfig),
+ dismiss,
+ handleKeyPress,
+ setDOMNode(el) {
+ keypadAPI._domNode = el;
+ },
+ });
+
+ return {
+ keypadAPI,
+ keypadContextValue,
+ };
+}
+
+/**
+ * Inject keypad state provided by a parent useKeypad() call.
+ * Use this in child components that need to interact with the keypad.
+ */
+export function injectKeypad() {
+ return inject(KeypadSymbol);
+}
diff --git a/kolibri/plugins/perseus_viewer/frontend/constants.js b/kolibri/plugins/perseus_viewer/frontend/constants.js
deleted file mode 100644
index aa1a021ba1b..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const ConfigFileName = 'KAthJax-730d56e87e9c926b91584f6030314815.js';
diff --git a/kolibri/plugins/perseus_viewer/frontend/dist/README.md b/kolibri/plugins/perseus_viewer/frontend/dist/README.md
deleted file mode 100644
index 1eaf432afb1..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/dist/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-The files in this directory are generated using the buildPerseus.js script inside this plugin.
-They should be regenerated using the same command in the unlikely event that Perseus ever needs
-to be updated.
-The script is run using the following command: pnpm --filter kolibri-perseus-viewer run build-perseus.
-This will automatically pull files from Perseus and make appropriate edits,
-and then copy and build relevant files from there into the static and assets/dist folders in this plugin.
diff --git a/kolibri/plugins/perseus_viewer/frontend/dist/fonts/MathJax_Main-Regular.woff b/kolibri/plugins/perseus_viewer/frontend/dist/fonts/MathJax_Main-Regular.woff
deleted file mode 100644
index 55b46265cc9..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/dist/fonts/MathJax_Main-Regular.woff
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1cb1c39ea642f26a4dfed230b4aea1c3c218689421f6e9c0a7c1811693c4fa07
-size 34160
diff --git a/kolibri/plugins/perseus_viewer/frontend/dist/fonts/Symbola.woff b/kolibri/plugins/perseus_viewer/frontend/dist/fonts/Symbola.woff
deleted file mode 100644
index ddb768b348f..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/dist/fonts/Symbola.woff
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:dab128b179e963c4e57c82d98ce0de6525bfeac6a651270820d3a2ad2119b72d
-size 467208
diff --git a/kolibri/plugins/perseus_viewer/frontend/dist/index.css b/kolibri/plugins/perseus_viewer/frontend/dist/index.css
deleted file mode 100644
index b09dee73560..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/dist/index.css
+++ /dev/null
@@ -1,4547 +0,0 @@
-.exercise,
-.vars,
-#next {
- display: none;
-}
-p.question {
- font-weight: bold;
-}
-var {
- font-style: normal;
-}
-
-.hint.last-hint {
- .paragraph {
- font-weight: bold;
- }
-}
-
-.hint_blue {
- color: #6495ed;
-}
-.hint_orange {
- color: #ffa500;
-}
-.hint_pink {
- color: #ff00af;
-}
-.hint_red {
- color: #df0030;
-}
-.hint_green {
- color: #28ae7b;
-}
-.hint_gray {
- color: gray;
-}
-.hint_purple {
- color: #9d38bd;
-}
-
-div.subhint {
- border: 1px solid #aaaaaa;
- background: #f9f9f9;
- display: none;
- border-radius: 4px;
- margin-left: 20px;
- margin-right: 20px;
- padding: 10px;
-}
-
-a.show-subhint {
- font-size: 12px;
- font-style: italic;
- background-color: #fdfdfd;
-}
-
-a.show-definition {
- background-color: #fdfdfd;
-}
-
-div.definition {
- position: absolute;
- border: 1px solid #aaaaaa;
- background: #f9f9f9;
- display: none;
- border-radius: 4px;
- margin-left: 20px;
- margin-right: 20px;
- padding: 10px;
- z-index: 1000;
-}
-
-.solutionarea ul {
- list-style: none;
-}
-.solutionarea li {
- padding: 7px 0;
-}
-.solutionarea li label {
- display: block;
-}
-.solutionarea li input[type="radio"] {
- float: left;
- margin-top: 4px;
-}
-.solutionarea li .value {
- display: block;
- min-height: 22px;
- margin-left: 18px;
-}
-
-#extras li {
- display: inline;
-}
-#extras li:before {
- content: "| ";
-}
-#extras li:first-child:before {
- content: "";
-}
-
-#scratchpad-show {
- position: relative;
- z-index: 1;
-}
-
-#answer_area #check-answer-results {
- overflow: hidden;
- margin: 5px 0;
-}
-#answer_area #check-answer-results .check-answer-message {
- font-size: 12px;
- line-height: 20px;
- margin: 0;
-}
-#sad,
-#happy {
- float: left;
- margin: 0 6px 4px 0;
-}
-
-.examples {
- color: #777;
- margin-left: 20px;
- list-style-type: disc;
-}
-.examples li {
- margin: 5px 0;
-}
-
-#problemarea {
- font-size: 14px;
- position: relative;
- float: left;
- padding-bottom: 38px;
-}
-
-#solution {
- font-size: 14px;
-}
-#solution label {
- display: block;
- white-space: nowrap;
-}
-
-#tester-info {
- border: 1px solid #aaa;
- background: #f0f0f0;
- padding: 10px;
- margin: 10px 0;
-}
-#debug var {
- font:
- 14px Menlo,
- Courier,
- monospace;
- word-wrap: break-word;
-}
-
-code {
- font-family: Courier, monospace;
-}
-
-pre {
- background-color: #f0f1f2;
- border-radius: 4px;
- color: #21242c;
- font-size: 18px;
- padding: 16px;
- white-space: pre;
- overflow: auto;
-}
-
-table.limit {
- margin: 5px;
-}
-table.limit th {
- font-weight: bold;
- text-align: center;
-}
-table.limit td {
- border: 1px solid #aaa;
-}
-table.limit th,
-table.limit td {
- padding: 5px;
-}
-table.limit th:first-child {
- text-align: right;
-}
-
-.solutionarea input[type="text"],
-.solutionarea input[type="number"] {
- width: 80px;
-}
-.solutionarea .short20 input[type="text"],
-.solutionarea .short20 input[type="number"] {
- width: 20px;
-}
-.solutionarea .short25 input[type="text"],
-.solutionarea .short25 input[type="number"] {
- width: 25px;
-}
-.solutionarea .short28 input[type="text"],
-.solutionarea .short28 input[type="number"] {
- width: 28px;
-}
-.solutionarea .short30 input[type="text"],
-.solutionarea .short30 input[type="number"] {
- width: 30px;
-}
-.solutionarea .short32 input[type="text"],
-.solutionarea .short32 input[type="number"] {
- width: 32px;
-}
-.solutionarea .short35 input[type="text"],
-.solutionarea .short35 input[type="number"] {
- width: 35px;
-}
-.solutionarea .short40 input[type="text"],
-.solutionarea .short40 input[type="number"] {
- width: 40px;
-}
-.solutionarea .short50 input[type="text"],
-.solutionarea .short50 input[type="number"] {
- width: 50px;
-}
-#readonly {
- display: none;
-}
-
-.radical .surd {
- font: 150% Arial;
- padding: 0 0 0 5px;
-}
-.radical .overline {
- border-top: 1px solid #000;
- padding: 6px 1px 0 3px;
- margin-left: -1px;
-}
-.solutionarea .radical input[type="text"],
-.solutionarea .radical input[type="number"] {
- width: 40px;
-}
-
-body.debug .graphie {
- outline: 1px dashed #fdd;
-}
-.graphie svg {
- position: absolute;
- top: 0;
- left: 0;
-}
-
-#scratchpad {
- display: none;
- margin: 0 10px;
- overflow: hidden;
- padding-bottom: 40px;
-}
-#scratchpad div {
- box-sizing: border-box;
- border: 1px solid #ccc;
- height: 100%;
- position: absolute;
- width: 100%;
- z-index: 1;
-}
-
-#scratchpad-not-available {
- display: none;
-}
-#extras .report-issue-link {
- float: right;
-}
-
-#tester-info.info-box {
- background: #f2e4bf;
-}
-#tester-info .box {
- border: 1px solid black;
- padding: 2px 4px;
- margin-left: 5px;
-}
-#tester-info .group-box {
- border: 1px solid #aaa;
- padding: 6px 4px 6px 0px;
- margin-left: 2px;
-}
-
-#browserwarning {
- background: white;
- margin: 0 1px;
- padding: 18px;
- font-size: 120%;
- text-align: center;
-}
-
-#answer_area .answer-buttons input.simple-button,
-#answer_area input.simple-button.full-width {
- width: 100%;
-}
-#answer_area #answercontent {
- position: relative;
- z-index: 2;
-} /* above-scratchpad */
-#answer_area .hint-box {
- position: relative;
- z-index: 1;
-}
-
-#problemarea a:link,
-#problemarea input,
-#problemarea label, /* HACK(aria): for radios */
-#problemarea select {
- /* for dropdowns */
- position: relative;
- z-index: 3; /* interactive-content */
-}
-
-#answer_area .calculator {
- width: 181px;
- margin: 0 auto;
-}
-
-.calculator-angle-mode .selected-anglemode {
- color: #050505;
-}
-
-.calculator-angle-mode .unselected-anglemode {
- color: #bbbbbb;
-}
-
-#answer_area .calculator .history {
- font:
- 14px/1.5 Menlo,
- Monaco,
- Courier,
- monospace;
- margin: 0 0 10px;
- width: 183px;
-
- background-color: white;
- border-radius: 5px;
-}
-
-#answer_area .calculator .history #calc-output-content {
- position: absolute;
- bottom: 0px;
- width: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
- max-height: 80px;
-}
-
-#answer_area .calculator .history #calc-output {
- height: 80px;
- text-align: left;
- overflow-y: hidden;
- overflow-x: hidden;
- position: relative;
-}
-
-#answer_area .calculator .history .output {
- text-align: right;
- margin-right: 3px;
-}
-
-#answer_area .calculator .history input {
- box-sizing: border-box;
- display: block;
- font: inherit;
- margin: 5px 3px 3px;
- width: 98%;
- padding-left: 25px;
- padding-right: 8px;
- text-align: right;
- border: none;
- outline-width: 0;
-}
-
-#answer_area .calculator .history .input {
- position: relative;
-}
-
-#answer_area .calculator .history .input-history {
- position: relative;
- margin-left: 3px;
-}
-
-#answer_area .calculator .history .status a {
- position: absolute;
- font:
- 10px/1.5 Menlo,
- Monaco,
- Courier,
- monospace;
- color: #999;
- text-decoration: none;
- line-height: 9px;
- height: 19px;
- top: 5px;
- left: 6px;
-}
-
-#answer_area .calculator .keypad .calc-row {
- margin: 5px 0;
-}
-
-#answer_area .calculator .keypad .calc-row a {
- background: #ccc;
- border-radius: 2px;
- color: #000;
- display: inline-block;
- margin: 0 5px 0 0;
- text-align: center;
- text-decoration: none;
- width: 31px;
-}
-
-#answer_area .calculator .keypad .calc-row a:hover {
- background: #bbb;
-}
-
-#answer_area .calculator .keypad .calc-row a:active {
- background: #aaa;
-}
-
-#answer_area .calculator .keypad .calc-row a.dark {
- background: #aaa;
-}
-
-#answer_area .calculator .keypad .calc-row a.dark:hover {
- background: #999;
-}
-
-#answer_area .calculator .keypad .calc-row a.dark:active {
- background: #888;
-}
-
-#answer_area .calculator .keypad a sup {
- vertical-align: super;
- font-size: 80%;
- line-height: 0;
-}
-
-#answer_area .calculator .keypad a.wide {
- width: 67px;
-}
-
-#solutionarea {
- min-height: 35px;
- padding: 10px;
- margin: 0 -10px;
- border-bottom: 1px solid #c3c3c3;
- overflow: visible;
-}
-
-.workarea #solutionarea {
- border-bottom: 0;
- padding-bottom: 0;
- padding-top: 0;
-}
-
-#answer_area .answer-buttons {
- margin: 0 -10px;
- padding: 10px 10px 0;
- position: relative;
-}
-
-#show-prereqs-button {
- margin-top: 15px;
-}
-
-#positive-reinforcement img {
- width: 28px;
- position: absolute;
- top: 7px;
- left: 5px;
- cursor: pointer;
-}
-
-#answer_area input.simple-button[disabled="disabled"] {
- opacity: 0.5;
- filter: alpha(opacity = 50);
- cursor: default;
-}
-#answer_area input.simple-button[disabled="disabled"]:hover {
- color: #fff !important;
-}
-#answer_area input.simple-button.orange[disabled="disabled"]:hover {
- border-color: #bf4f04 !important;
- border-bottom-color: #803503 !important;
-}
-#answer_area input.simple-button.green[disabled="disabled"]:hover {
- border-color: #76a005 !important;
- border-bottom-color: #557303 !important;
-}
-
-.simple-button.disabled {
- opacity: 0.5;
- filter: alpha(opacity = 50);
- cursor: default;
-}
-
-#hint-remainder {
- color: #777;
-}
-
-#footer .simple-button,
-.info-box .simple-button {
- padding: 3px 10px;
- top: -1px;
-}
-.info-box .simple-button {
- top: 0;
-}
-
-#issue #issue-status.error {
- font-weight: bold;
- color: #a21;
- font-size: 1.2em;
-}
-#issue-link {
- font-style: italic;
-}
-#issue .issue-form input[type="text"] {
- display: block;
- width: 98%;
-}
-#issue .issue-form textarea {
- display: block;
- width: 98%;
- height: 100px;
-}
-#issue-cancel {
- float: right;
-}
-#issue fieldset {
- list-style: none;
- border: 1px solid rgba(0, 0, 0, 0.3);
- padding-left: 10px;
- border-radius: 5px;
- line-height: 22px;
- margin-bottom: 3px;
- min-inline-size: auto;
-}
-#issue fieldset legend {
- padding-left: 5px;
- padding-right: 5px;
-}
-#issue fieldset label {
- display: inline;
-}
-#issue fieldset li {
- margin-top: 0px;
- margin-bottom: 0px;
-}
-
-var,
-div.graphie {
- white-space: pre;
- /**
- * Graphie didn't have a fixed font-size and was just using
- * the surrounding font-size. However some labels are banking on
- * specific dimensions, so we need to lock it to a specific size.
- */
- font-size: 14px;
-}
-
-#spinner {
- position: relative;
- top: 4px;
- left: 4px;
-}
-#issue-spinner {
- position: relative;
- top: 3px;
-}
-
-.exp input {
- vertical-align: super;
- font-size: 9px;
- height: 11px;
-}
-
-.correct-activity {
- background-color: #69bb00;
- text-shadow: 0 -1px 0 #557303;
-}
-
-.incorrect-activity {
- background-color: #e12c2d;
- text-shadow: 0 -1px 0 #921118;
-}
-
-.hint-activity {
- background-color: #f19726;
- text-shadow: 0 -1px 0 #b55c00;
-}
-
-.user-activity {
- margin: 8px;
- padding: 2px 5px;
- border: 1px solid #999;
- border-radius: 4px;
- float: left;
- cursor: pointer;
- color: white;
-}
-
-.user-activity input {
- cursor: pointer;
-}
-
-div.timeline-time {
- float: left;
- padding-top: 10px;
-}
-
-div.timeline-time:before {
- padding: 3px;
- content: "~";
-}
-
-div.timeline-time:after {
- padding: 3px;
- content: "~";
-}
-
-div.timeline-total {
- border-top: 1px solid #999;
-}
-
-#timelinecontainer {
- border: 1px solid #c6d1ad;
- border-top: 0px;
- position: relative;
-}
-
-#timelinecontainer:before,
-#timelinecontainer:after {
- content: "";
- display: table;
-}
-
-#timelinecontainer:after {
- clear: both;
-}
-
-#timelinecontainer {
- zoom: 1;
-}
-
-#timeline {
- overflow: hidden;
- position: absolute;
- left: 265px;
- right: 225px;
- border-left: 1px solid #c6d1ad;
- border-right: 1px solid #c6d1ad;
-}
-
-#timeline-events {
- width: 10000px;
-}
-
-/* Allow the timeline to scroll all the way to the end with a little margin */
-#timeline-events:after,
-#timeline-events:before {
- content: "";
- display: block;
- width: 1px;
- height: 1px;
- float: left;
-}
-
-#timeline p {
- margin: 0;
-}
-
-#previous-problem {
- margin: 5px;
- cursor: pointer;
- float: left;
- width: 100px;
-}
-
-#next-problem {
- margin: 5px;
- cursor: pointer;
- float: right;
- width: 80px;
-}
-
-.user-activity.activated {
- border: 2px solid #888;
-}
-
-/* Version of the site used by Khan/exercise-browser for the iframe preview */
-
-html.exercise-browser {
- overflow-x: hidden;
- overflow-y: hidden;
-}
-
-.exercise-browser body {
- min-width: 0;
- width: 790px;
- overflow-x: hidden;
- overflow-y: auto;
-}
-
-.exercise-browser header,
-.exercise-browser footer,
-.exercise-browser .topic-exercise-badge,
-.exercise-browser #extras,
-.exercise-browser .exercises-stack,
-.exercise-browser .related-video-box {
- display: none !important;
-}
-
-.exercise-browser #outer-wrapper,
-.exercise-browser #page-container,
-.exercise-browser #page-container-inner {
- background: #ffffff;
-}
-
-.exercise-browser article {
- border: none;
-}
-
-.exercise-browser #container.single-exercise {
- min-width: 0;
-}
-
-.exercise-browser #page-container {
- width: 790px;
- min-width: 0;
-}
-
-.exercise-browser .problem-types {
- margin-right: 20px;
-}
-
-.exercise-browser .problem-type-link {
- display: block;
- margin-bottom: 5px;
- padding: 6px;
- background: #36a6c4;
- border-radius: 5px;
- border: 1px solid #7fc8e6;
- color: white;
-}
-
-.exercise-browser .problem-type-link:hover {
- color: white;
- background: #1c758c;
- text-decoration: none;
-}
-
-.exercise-browser .problem-type-link:visited {
- color: #c2eaff;
- text-decoration: none;
-}
-
-.lite header,
-.lite footer,
-.lite #extras,
-.lite .exercise-badge,
-.lite .hint-box,
-.lite .related-video-box {
- display: none !important;
-}
-
-.lite #page-container,
-.lite #container {
- min-width: 0;
- border-width: 0;
-}
-
-.lite #streak-bar-container {
- position: absolute;
- top: 10px;
- left: 15px;
-}
-
-.lite #answercontent {
- position: absolute;
- right: 5px;
- top: 12px;
- padding: 0px;
- border: none;
- box-shadow: none;
- overflow: visible;
-}
-
-.lite #answercontent > * {
- float: left;
- margin-right: 10px;
-}
-
-/* TODO: Find a better way to display these. */
-.lite #spinner,
-.lite #check-answer-results {
- display: none !important;
-}
-
-.lite #answercontent .info-box-header {
- font-size: 16px;
-}
-
-.lite #answercontent .examples {
- display: none !important;
-}
-
-.lite .ui-icon {
- width: 18px;
- height: 18px;
-}
-
-.lite h1 {
- font-family: inherit;
-}
-
-.lite #solutionarea input {
- font-size: 14px;
-}
-
-.lite #answercontent .simple-button {
- margin-top: -5px;
- color: #fff !important;
- font-size: 14px;
- text-shadow: none;
-}
-
-#warning-bar {
- width: 100%;
- height: 35px;
- text-align: center;
- font-size: 15px;
- display: none;
- padding: 6px 0;
- position: relative;
- z-index: 2;
-}
-
-.bibliotron-exercise #warning-bar {
- height: auto;
-}
-
-#warning-bar span {
- position: relative;
- top: 5px;
-}
-
-.bibliotron-exercise #warning-bar span,
-.bibliotron-exercise #warning-bar #warning-bar-close {
- top: auto;
-}
-
-#warning-bar-close {
- top: 5px;
- float: right;
- right: 20px;
- position: relative;
-}
-
-#warning-bar-content a {
- text-decoration: underline;
-}
-
-#warning-bar.error {
- background-color: #d61914;
- color: #eee;
-}
-
-#warning-bar.error a {
- color: #eee;
-}
-
-#warning-bar.warning {
- background-color: #f5e722;
- color: #222;
-}
-
-#warning-bar.warning a {
- color: #222;
-}
-
-/* Faux table styles that allow revealing data row by row in hints */
-
-.fake_header > span {
- font-weight: bold;
- display: inline-block;
- padding-left: 10px;
- border-bottom: 2px solid #cccccc;
-}
-
-.fake_row > span {
- display: inline-block;
- padding-top: 5px;
- padding-bottom: 5px;
- padding-left: 10px;
- border-bottom: 1px solid #ddd;
-}
-
-.fake_row:nth-child(n) > span {
- border-bottom: none;
-}
-
-.fake_row:nth-child(2n + 1) > span {
- background-color: #f3f3f3;
-}
-
-#timelinecontainer .simple-button {
- -webkit-user-select: none;
- user-select: none;
- white-space: nowrap;
-}
-
-.thumbnail a {
- outline: none;
- color: #fff;
-}
-
-.thumbnail a:hover {
- text-decoration: none;
-}
-
-.thumbnail div.thumb {
- background-size: 200px 150px;
- background-position: no-repeat top left;
- background-position: 0px -25px;
- background-position-y: -80px\9;
- background-position-x: -100px\9;
- width: 200px;
- height: 100px;
- border: 1px solid #aaa;
- box-shadow: 0 0 3px #ccc;
- margin-left: 14px; /* to line up with other titles */
- margin-bottom: 8px;
- margin-top: 4px;
-}
-
-.thumbnail div.thumbnail_label {
- padding: 5px 10px;
- max-width: 180px;
- margin: 0;
- text-align: left;
- margin-top: 68px;
- background: #333;
- background-color: rgba(30, 30, 30, 0.9);
- color: #fff;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.thumbnail div.thumbnail_desc {
- width: 180px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.thumbnail div.thumbnail_teaser {
- height: 0px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: normal;
- text-decoration: none;
- font-size: 11px;
-}
-
-.sortable {
- background: #eee;
- border: 1px solid #ccc;
- border-bottom: 1px solid #aaa;
- padding: 13px;
- position: relative;
- z-index: 2;
-
- box-shadow: 0 1px 2px #ccc;
-}
-
-.sortable > ul {
- list-style-type: none;
-}
-
-.sortable > ul > li {
- background-color: #fff;
- border: 1px solid #b9b9b9;
- border-bottom-color: #939393;
- border-radius: 4px;
- cursor: pointer;
- margin-right: 4px;
- min-width: 65px;
- height: 65px;
- text-align: center;
- font-size: 1.2em;
- float: left;
- -webkit-user-select: none;
- user-select: none;
-}
-
-.sortable > ul > li.placeholder {
- background: #ddd;
- border: 0;
- border: 1px solid #ccc;
- float: left;
-}
-
-.sortable code {
- line-height: 65px;
-}
-
-.sortable > ul > li:hover {
- border-color: #ffa500;
- box-shadow: 0 0 4px #c56f00;
-}
-
-.sortable > ul > li:active,
-.sortable > ul > li.dragging {
- background-color: #ffedcd;
- opacity: 0.8;
- filter: alpha(opacity = 80);
-}
-
-.box-whisker-sortable > .sortable {
- background: #f8f8f8;
- border: 0;
- border-bottom: 0;
- box-shadow: 0 0 0;
- padding-right: 0;
-}
-
-.box-whisker-sortable > .sortable > ul > li {
- width: 28px;
- min-width: 28px;
- height: 28px;
- margin-right: 2px;
- font-size: 1em;
- border: 1px solid #ddd;
-}
-.box-whisker-sortable > .sortable code {
- line-height: 32px;
-}
-
-#problemarea div#congruent-triangles {
- outline-color: #999;
- outline-style: dashed;
- outline-width: 1px;
-}
-
-/* Video hints */
-
-.video-hint {
- margin-bottom: 20px;
-}
-
-/* Question-based hints */
-
-.qhint {
- border: 1px solid #aaaaaa;
- background: #f9f9f9;
- border-radius: 4px;
- margin-right: 20px;
- margin-bottom: 20px;
- padding: 10px;
-}
-
-.qhint-answer {
- display: none;
-}
-
-.qhint-feedback {
- font-weight: bold;
- color: #6495ed;
-}
-
-.qhint-feedback.correct {
- color: #28ae7b;
-}
-
-.qhint-feedback.incorrect {
- color: #ce4444;
-}
-
-/*Worked example hints*/
-.example-hint {
- margin-bottom: 15px;
-}
-
-/* Fancy matrix input - goes along with matrix-input.js */
-.matrix-input {
- padding: 15px;
- margin-top: 10px;
- background-color: #eee;
- width: 165px;
-}
-
-.solutionarea .matrix-row {
- float: left; /* contain inner floats */
- clear: both;
-}
-
-.solutionarea .matrix-row .sol {
- margin: 0;
- float: left;
-}
-
-.solutionarea .matrix-row .sol input[type="text"],
-.solutionarea .matrix-row .sol input[type="number"] {
- width: 45px;
- height: 30px;
- border: none;
- margin: 3px;
- padding: 1px;
-}
-
-.matrix-input .matrix-bracket {
- width: 6px;
- position: absolute;
- border-top: 2px solid #888;
- border-bottom: 2px solid #888;
- /* margin-top must have the same magnitude
- as the border widths */
- margin-top: -2px;
-}
-
-.matrix-input .matrix-bracket.bracket-left {
- border-left: 2px solid #888;
- /* margin-left for the left bracket must have
- the same magnitude as the border widths */
- margin-left: -2px;
-}
-
-.matrix-input .matrix-bracket.bracket-right {
- border-right: 2px solid #888;
-}
-
-.matrix-input:after {
- content: "";
- display: block;
- clear: both;
- visibility: hidden;
- overflow: hidden;
- height: 0;
-}
-
-table.periodic-table td.element {
- border: 1px solid;
- min-width: 37px;
- min-height: 37px;
- text-align: center;
- line-height: 1.2;
-}
-
-table.periodic-table td.element div.atomic-num {
-}
-table.periodic-table td.element div.symbol {
- font-weight: bold;
-}
-table.periodic-table td.element div.weight {
- font-size: 10px;
-}
-
-.plugging_in_values > span {
- width: 80px;
-}
-table.plugging_in_values_hint td {
- padding-left: 40px;
-}
-
-span.hover-hint {
- color: #005987;
- border-bottom: 1px dashed #005987;
-}
-span.hover-hint:hover {
- color: #678d00;
- border-bottom: 1px dashed #678d00;
- cursor: pointer;
-}
-
-.vis-deriv-hint-graph {
- display: block;
- float: left;
- margin-right: 50px;
-}
-
-#problemarea .quarter-graph {
- float: left;
- margin-bottom: 45px;
- margin-right: 45px;
-}
-
-#problemarea .estimation span {
- width: 100px;
-}
-#problemarea .estimation span:first-child {
- width: 50px;
-}
-
-#solutionarea table.rational-exp td {
- text-align: center;
- vertical-align: middle;
- padding-left: 4px;
- padding-right: 4px;
-}
-#solutionarea table.rational-exp td.soln-top {
- padding-bottom: 1px;
-}
-#solutionarea table.rational-exp td.soln-bot {
- padding-top: 1px;
- border-top: 1px solid black;
-}
-
-#solutionarea table.rational-exp td.soln-dom {
- padding-left: 3px;
-}
-
-#problemarea div.z-score-table > span {
- width: 32px;
- font-size: 11px;
- padding: 5px;
-}
-
-#problemarea div.z-score-table > span:first-child {
- font-weight: bold;
- width: 22px;
- border-right: 2px solid #cccccc;
-}
-
-#problemarea div.focus-information {
- padding-bottom: 12px;
- height: 70px;
- clear: both;
-}
-
-#problemarea div.focus-information div {
- display: block;
-}
-#problemarea div.focus-information-column-left {
- float: left;
- width: 30%;
-}
-#problemarea div.focus-information-column-right {
- float: right;
- width: 55%;
-}
-
-#problemarea .reading-tables > span {
- text-align: center;
- width: 65px;
-}
-#problemarea .reading-tables > span:first-child {
- text-align: left;
- width: 100px;
-}
-
-#problemarea .reading > span {
- width: 32px;
- font-size: 11px;
- padding: 5px;
-}
-
-#problemarea .reading.fake_header > span {
- text-align: center;
-}
-
-#problemarea .reading > span:first-child {
- font-weight: bold;
- width: 22px;
- border-right: 2px solid #cccccc;
-}
-
-#problemarea .problem .graph-caption {
- display: block;
- margin-top: 10px;
- margin-bottom: 30px;
-}
-
-#solutionarea .intuition-equation td {
- text-align: right;
- padding-left: 5px;
-}
-
-.question li span.sort-key {
- visibility: hidden;
- position: absolute;
-}
-
-.problem table.problem-equation td {
- padding-left: 3px;
- padding-right: 3px;
- text-align: center;
-}
-.problem table.problem-equation input {
- width: 25px;
- margin-right: 5px;
- vertical-align: super;
-}
-
-#coin-flip-sequence-charts {
- height: 324px;
- position: relative;
- width: 496px;
-}
-
-#coin-flip-sequence-charts .graphie {
- height: 324px;
- width: 496px;
- position: absolute !important;
-}
-
-#coin-flip-sequence-chart-1 {
- left: 496px;
-}
-
-#coin-flip-sequence-chart-2 {
- left: 992px;
-}
-
-#coin-flip-sequence-chart-3 {
- left: 1488px;
-}
-
-.view-coin-flip-sequence-patterns {
- margin-top: 12px;
-}
-
-.view-coin-flip-sequence-patterns span {
- color: #ccc;
-}
-
-.view-coin-flip-sequence-patterns a {
- margin: 0 5px;
-}
-
-.view-coin-flip-sequence-patterns a.selected {
- font-weight: bold;
- color: #444 !important;
- cursor: default;
- text-decoration: none !important;
-}
-
-.coin-flip-sequence-heads {
- background: #a6a6a6;
- color: #333;
-}
-
-.coin-flip-sequence-heads,
-.coin-flip-sequence-tails {
- border-right: 1px solid #888;
- float: left;
- font-family: Verdana, sans-serif;
- font-size: 14px;
- margin-bottom: 5px;
- padding: 0 2px;
- text-align: center;
- width: 16px;
-}
-
-.coin-flip-sequence-tails {
- background: #595959;
- color: #ccc;
-}
-
-body.debug span.error {
- font-weight: bold;
- color: #fff;
- background: #f00;
- font-size: 1.4em;
- padding: 0.2em;
- text-decoration: line-through;
-}
-
-/* Work around bug added in https://khanacademy.kilnhg.com/Review/K109777 */
-.new-header .MathJax .math {
- color: inherit;
-}
-
-.solutionarea .info-box .mini-button {
- margin: 0px 10px 0px 0px;
- padding: 0px 5px;
- width: 25px;
- height: 18px;
-}
-
-.solutionarea #number-label {
- clear: both;
- margin: 0px 0px;
- padding: 14px 0px 0px 0px;
-}
-
-/* Expression/equation input (see "expression" answer type) */
-.solutionarea .expression {
- box-sizing: border-box;
- display: block;
- max-width: 240px;
- width: 100%;
-}
-
-.solutionarea .expression > .output {
- background: #f2f2f2;
- display: inline-block;
- border-radius: 5px;
- padding-top: 10px;
- margin: 10px 0;
- /* HACK(aria) above scratchpad so that users have somewhere to
- * "click off of" to hide the expression dialog.
- * Also important because it can get a silly scrollbar and we
- * want users to be able to scroll even if the scratchpad is
- * open. We should probably make this wider eventually. */
- position: relative;
- z-index: 2;
-}
-
-.solutionarea .expression > .output > .tex {
- display: block;
- min-height: 42px;
- overflow-x: scroll;
- padding: 2px 10px 16px 10px;
-}
-
-.solutionarea .expression > .input {
- position: relative;
- z-index: 3; /* interactive-content */
-}
-
-.solutionarea .expression > .input > input {
- border: 1px solid #a4a4a4;
- border-radius: 5px;
- direction: ltr;
- font-size: 14px;
- margin-bottom: 5px;
- padding: 6px;
-}
-
-.solutionarea .expression > .output,
-.solutionarea .expression > .output > .tex,
-.solutionarea .expression > .input > input {
- box-sizing: border-box;
- max-width: 240px;
- width: 100%;
-}
-
-.solutionarea .expression > .input > input.error {
- padding-right: 25px;
-}
-
-.solutionarea .expression > .input > .error-div {
- position: absolute;
- right: 6px;
- top: -3px;
-}
-
-.solutionarea .expression > .input > .error-div > .error-icon {
- color: #fcc335;
- font-size: 20px;
-}
-
-body:not(.mobile) ul.inequalities-one-line-radios,
-body:not(.mobile) ul.inequalities-one-line-radios > li {
- display: inline;
-}
-
-body.mobile ul.inequalities-one-line-radios > li {
- padding: 7px 0;
-}
-
-.inequalities-padding#grid {
- margin-bottom: 2em;
-}
-
-#skip-question-button,
-#opt-out-button {
- margin-top: 15px;
-}
-
-.perseus-sr-only {
- border: 0;
- clip: rect(0, 0, 0, 0);
- font-size: 0;
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
-}
-.perseus-clearfix {
- *zoom: 1;
-}
-.perseus-clearfix:before,
-.perseus-clearfix:after {
- content: "";
- display: table;
-}
-.perseus-clearfix:after {
- clear: both;
-}
-/* Fall back colors? Eg educator, talks & interviews */
-.framework-perseus code {
- font-family: Courier, monospace;
-}
-.framework-perseus pre {
- background-color: #f0f1f2;
- border-radius: 4px;
- color: #21242c;
- font-size: 18px;
- padding: 16px;
- white-space: pre;
- overflow: auto;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container:not(.perseus-widget__definition) {
- font-size: 14px;
- line-height: 19.6px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-left,
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-right {
- max-width: 50%;
- padding-top: 32px;
- width: 100%;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-left .perseus-image-caption .paragraph .paragraph,
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-right .perseus-image-caption .paragraph .paragraph {
- margin-bottom: 0;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-left {
- float: left;
- padding-right: 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-widget-container.widget-float-right {
- float: right;
- padding-left: 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph {
- color: #21242c;
- font-size: 20px;
- line-height: 30px;
- margin: 0 auto;
- max-width: 688px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph .paragraph {
- color: #21242c;
- font-size: 20px;
- line-height: 30px;
- margin-bottom: 32px;
- margin-top: 0;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph .katex,
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph mjx-container {
- font-size: 100%;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) {
- color: #21242c;
- font-size: 20px;
- line-height: 30px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) table {
- color: #21242c;
- font-size: 20px;
- line-height: 30px;
- margin-bottom: 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) h2 {
- font-family: inherit;
- font-size: 30px;
- font-weight: 700;
- line-height: 1.1;
- margin-bottom: 16px;
- margin-top: 48px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) h3 {
- font-family: inherit;
- font-size: 26px;
- font-weight: 700;
- line-height: 1.1;
- margin-bottom: 16px;
- margin-top: 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) h4,
-.framework-perseus.perseus-article:not(.perseus-mobile) h5,
-.framework-perseus.perseus-article:not(.perseus-mobile) h6 {
- font-family: inherit;
- font-size: 22px;
- font-weight: 700;
- line-height: 25px;
- margin-bottom: 16px;
- margin-top: 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) blockquote {
- padding: 0 32px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .MathJax .math {
- color: inherit;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-image-caption .perseus-renderer .paragraph .paragraph,
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-image-caption .perseus-renderer .paragraph ol,
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-image-caption .perseus-renderer .paragraph ul {
- color: rgba(33, 36, 44, 0.64);
- font-size: 14px;
- line-height: 19px;
- margin: 16px auto 42px;
- text-align: left;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .paragraph.perseus-paragraph-full-width {
- margin-bottom: 32px;
- margin-left: 0;
- margin-right: 0;
- max-width: none;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .paragraph.perseus-paragraph-full-width > .paragraph {
- margin: 0;
- max-width: none;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .unresponsive-svg-image,
-.framework-perseus.perseus-article:not(.perseus-mobile) .svg-image {
- font-size: 14px;
- line-height: 19.6px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-block-math {
- margin-bottom: 32px;
- position: relative;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-block-math:before {
- bottom: 0;
- content: "";
- position: absolute;
- right: 0;
- top: 0;
- width: 30px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-block-math-inner {
- overflow-x: auto;
- padding-bottom: 8px;
- padding-right: 20px;
- padding-top: 8px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h1:first-child,
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h2:first-child,
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h3:first-child,
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h4:first-child,
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h5:first-child,
-.framework-perseus.perseus-article:not(.perseus-mobile) > .clearfix:first-child > .perseus-renderer:first-child > .paragraph:first-child h6:first-child {
- margin-top: 0;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph .perseus-formats-tooltip {
- padding: 8px 12px;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph .perseus-formats-tooltip .paragraph {
- margin-bottom: 0;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) .perseus-renderer > .paragraph .perseus-formats-tooltip .paragraph ul:not(.perseus-widget-radio) {
- font-size: 15px;
- line-height: 1.5;
- margin: 0;
-}
-.framework-perseus.perseus-article:not(.perseus-mobile) pre {
- margin: 0 -16px 32px -16px;
-}
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-option-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 14px;
- line-height: 1.25;
- color: #21242c;
-}
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-option-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 14px;
- line-height: 1.25;
- color: #21242c;
-}
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-rationale-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 14px;
- line-height: 1.25;
- color: #21242c;
-}
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 14px;
- line-height: 1.25;
- color: #21242c;
-}
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph .katex,
-.framework-perseus:not(.perseus-article):not(.perseus-mobile) .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph mjx-container {
- color: #21242c;
-}
-.framework-perseus.perseus-mobile .perseus-article .perseus-widget-container.widget-float-left {
- float: left;
- padding-right: 1em;
- max-width: 50%;
- width: 100%;
-}
-.framework-perseus.perseus-mobile .perseus-article .perseus-widget-container.widget-float-right {
- float: right;
- padding-left: 1em;
- max-width: 50%;
- width: 100%;
-}
-.framework-perseus.perseus-mobile .perseus-article .perseus-renderer > .paragraph {
- margin-left: auto;
- margin-right: auto;
- max-width: 700px;
-}
-.framework-perseus.perseus-mobile .perseus-article .paragraph.perseus-paragraph-full-width {
- margin-left: 0;
- margin-right: 0;
- max-width: none;
-}
-.framework-perseus.perseus-mobile .perseus-article .paragraph.perseus-paragraph-full-width > .paragraph {
- margin: 0;
- max-width: none;
-}
-.framework-perseus.perseus-mobile :not(blockquote) > div.paragraph {
- margin: 0;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- margin: 0 auto;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph:not(:first-child) {
- margin-top: 32px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph > .paragraph {
- margin: 0;
-}
-.framework-perseus.perseus-mobile .clearfix > .perseus-renderer {
- margin-bottom: 32px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio, .indicatorContainer) {
- margin: 0 0 0 1em;
- padding: 0;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio, .indicatorContainer) > li {
- padding-left: 10px;
- margin-bottom: 24px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol {
- margin: 0;
- padding-left: 32px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol > li {
- list-style-type: decimal;
- margin-bottom: 24px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol ol,
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) ol,
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol ul:not(.perseus-widget-radio),
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) ul:not(.perseus-widget-radio) {
- padding-top: 24px;
-}
-.framework-perseus.perseus-mobile .perseus-block-math {
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
- -webkit-touch-callout: none;
-}
-@media (max-width: 767px) {
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- max-width: none;
- }
- .framework-perseus.perseus-mobile h1 {
- font-weight: 700;
- padding-top: 0px;
- font-family: inherit;
- font-size: 24px;
- line-height: 1.2;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile h2 {
- font-weight: 700;
- padding-top: 16px;
- font-family: inherit;
- font-size: 24px;
- line-height: 1.2;
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile h3,
- .framework-perseus.perseus-mobile h4 {
- font-weight: 700;
- padding-top: 0px;
- font-family: inherit;
- font-size: 22px;
- line-height: 1.1;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .default-body-text {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile blockquote {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- color: #888d93;
- }
- .framework-perseus.perseus-mobile table {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 16px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 16px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 16px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 16px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph .katex,
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph mjx-container {
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption .paragraph .paragraph {
- color: #888d93;
- font-size: 14px;
- line-height: 1.3;
- text-align: left;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption.has-title .paragraph .paragraph strong:first-child {
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile :is(.katex, mjx-container):not(.mafs-graph *) {
- font-size: 21px;
- line-height: 1.2;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-block-math .katex,
- .framework-perseus.perseus-mobile .perseus-block-math mjx-container {
- font-size: 21px;
- line-height: 1.5;
- }
- .framework-perseus.perseus-mobile .graphie-label .katex,
- .framework-perseus.perseus-mobile .graphie-label mjx-container {
- font-size: 1.21em;
- line-height: 1.2;
- }
- .framework-perseus.perseus-mobile code {
- font-family: Courier, monospace;
- }
- .framework-perseus.perseus-mobile pre {
- background-color: #f0f1f2;
- border-radius: 4px;
- color: #21242c;
- font-size: 18px;
- line-height: 1.6;
- padding: 16px;
- white-space: pre;
- overflow: auto;
- }
- .framework-perseus.perseus-mobile blockquote {
- padding: 0 0 0 18px;
- border-left: 4px solid #d8d8d8;
- }
-}
-@media (min-width: 768px) and (max-width: 1199px) {
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- max-width: 512px;
- }
- .framework-perseus.perseus-mobile h1 {
- font-weight: 700;
- padding-top: 0px;
- font-family: inherit;
- font-size: 30px;
- line-height: 1.1;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile h2 {
- font-weight: 700;
- padding-top: 32px;
- font-family: inherit;
- font-size: 30px;
- line-height: 1.1;
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile h3,
- .framework-perseus.perseus-mobile h4 {
- font-weight: 700;
- padding-top: 16px;
- font-family: inherit;
- font-size: 28px;
- line-height: 1.1;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .default-body-text {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile blockquote {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- color: #888d93;
- }
- .framework-perseus.perseus-mobile table {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.5;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 18px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph .katex,
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph mjx-container {
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption .paragraph .paragraph {
- color: #888d93;
- font-size: 17px;
- line-height: 1.4;
- text-align: left;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption.has-title .paragraph .paragraph strong:first-child {
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile :is(.katex, mjx-container):not(.mafs-graph *) {
- font-size: 23px;
- line-height: 1.3;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-block-math .katex,
- .framework-perseus.perseus-mobile .perseus-block-math mjx-container {
- font-size: 30px;
- line-height: 1.3;
- }
- .framework-perseus.perseus-mobile .graphie-label .katex,
- .framework-perseus.perseus-mobile .graphie-label mjx-container {
- font-size: 1.21em;
- line-height: 1.2;
- }
- .framework-perseus.perseus-mobile code {
- font-family: Courier, monospace;
- }
- .framework-perseus.perseus-mobile pre {
- background-color: #f0f1f2;
- border-radius: 4px;
- color: #21242c;
- font-size: 18px;
- line-height: 1.6;
- padding: 16px;
- white-space: pre;
- overflow: auto;
- }
- .framework-perseus.perseus-mobile blockquote {
- padding: 0 0 0 20px;
- border-left: 4px solid #d8d8d8;
- }
-}
-@media (min-width: 1200px) {
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- max-width: 688px;
- }
- .framework-perseus.perseus-mobile h1 {
- font-weight: 700;
- padding-top: 0px;
- font-family: inherit;
- font-size: 35px;
- line-height: 1.1;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile h2 {
- font-weight: 700;
- padding-top: 32px;
- font-family: inherit;
- font-size: 35px;
- line-height: 1.1;
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile h3,
- .framework-perseus.perseus-mobile h4 {
- font-weight: 700;
- padding-top: 16px;
- font-family: inherit;
- font-size: 30px;
- line-height: 1.1;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .default-body-text {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-renderer > .paragraph ol {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile blockquote {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- color: #888d93;
- }
- .framework-perseus.perseus-mobile table {
- font-family: inherit;
- font-size: 22px;
- line-height: 1.4;
- color: #626569;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-option-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph {
- font-family: inherit;
- font-size: 20px;
- line-height: 1.25;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph .katex,
- .framework-perseus.perseus-mobile .perseus-radio-rationale-content .perseus-renderer > .paragraph .paragraph mjx-container {
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption .paragraph .paragraph {
- color: #888d93;
- font-size: 20px;
- line-height: 1.4;
- text-align: left;
- }
- .framework-perseus.perseus-mobile .perseus-image-caption.has-title .paragraph .paragraph strong:first-child {
- color: #3b3e40;
- }
- .framework-perseus.perseus-mobile :is(.katex, mjx-container):not(.mafs-graph *) {
- font-size: 25px;
- line-height: 1.2;
- color: #21242c;
- }
- .framework-perseus.perseus-mobile .perseus-block-math .katex,
- .framework-perseus.perseus-mobile .perseus-block-math mjx-container {
- font-size: 30px;
- line-height: 1.3;
- }
- .framework-perseus.perseus-mobile .graphie-label .katex,
- .framework-perseus.perseus-mobile .graphie-label mjx-container {
- font-size: 1.21em;
- line-height: 1.2;
- }
- .framework-perseus.perseus-mobile code {
- font-family: Courier, monospace;
- }
- .framework-perseus.perseus-mobile pre {
- background-color: #f0f1f2;
- border-radius: 4px;
- color: #21242c;
- font-size: 18px;
- line-height: 1.6;
- padding: 16px;
- white-space: pre;
- overflow: auto;
- }
- .framework-perseus.perseus-mobile blockquote {
- padding: 0 0 0 20px;
- border-left: 5px solid #d8d8d8;
- }
-}
-.framework-perseus.perseus-mobile .perseus-widget-container {
- font-size: 14px;
- line-height: 19.6px;
-}
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-left,
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-right {
- max-width: 50%;
- padding-top: 32px;
- width: 100%;
-}
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-left .perseus-image-caption .paragraph .paragraph,
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-right .perseus-image-caption .paragraph .paragraph {
- margin-bottom: 0;
-}
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-left {
- float: left;
- padding-right: 32px;
-}
-.framework-perseus.perseus-mobile .perseus-widget-container.widget-float-right {
- float: right;
- padding-left: 32px;
-}
-.framework-perseus.perseus-mobile .MathJax .math {
- color: inherit;
-}
-.framework-perseus.perseus-mobile .perseus-image-widget {
- text-align: center;
-}
-.framework-perseus.perseus-mobile .perseus-block-math {
- padding-top: 16px;
- padding-bottom: 16px;
-}
-.framework-perseus.perseus-mobile .paragraph.perseus-paragraph-full-width {
- margin-left: 0;
- margin-right: 0;
- max-width: none;
-}
-.framework-perseus.perseus-mobile .paragraph.perseus-paragraph-full-width > .paragraph {
- margin: 0;
- max-width: none;
-}
-.framework-perseus.perseus-mobile .unresponsive-svg-image,
-.framework-perseus.perseus-mobile .svg-image {
- font-size: 14px;
- line-height: 19.6px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph .perseus-formats-tooltip {
- padding: 8px 12px;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph .perseus-formats-tooltip .paragraph {
- margin-bottom: 0;
-}
-.framework-perseus.perseus-mobile .perseus-renderer > .paragraph .perseus-formats-tooltip .paragraph ul:not(.perseus-widget-radio) {
- font-size: 15px;
- line-height: 1.5;
- margin: 0;
-}
-/* Derived from the MIT-licensed zoom.js:
- https://github.com/fat/zoom.js/blob/fd4f3e43153da7596da0bade198e99f98b47791e/
-*/
-.zoomable {
- cursor: pointer;
- cursor: -webkit-zoom-in;
- cursor: -moz-zoom-in;
-}
-.zoom-img {
- background-color: white;
- position: absolute;
- z-index: 9001;
-}
-img.zoom-img {
- cursor: pointer;
- cursor: -webkit-zoom-out;
- cursor: -moz-zoom-out;
-}
-.zoom-transition {
- transition: transform 300ms ease;
-}
-.zoom-overlay {
- z-index: 9000;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- overflow: scroll;
-}
-.zoom-overlay-open,
-.zoom-overlay-transitioning {
- cursor: default;
-}
-.zoom-overlay-open {
- height: 100%;
- max-height: 100%;
- overflow: hidden;
-}
-.zoom-backdrop {
- z-index: 8999;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: white;
- opacity: 0;
- transition: opacity 300ms;
-}
-.zoom-overlay-open > .zoom-backdrop {
- opacity: 0.9;
-}
-#perseus {
- position: relative;
-}
-.framework-perseus.perseus-mobile {
- margin-top: 48px;
-}
-.no-select {
- -webkit-user-select: none;
- user-select: none;
-}
-.blank-background {
- background-color: #fdfdfd;
-}
-#answer_area .blank-background {
- background-color: transparent;
-}
-.above-scratchpad {
- position: relative;
- z-index: 2;
-}
-.graphie.above-scratchpad,
-.graphie-container.above-scratchpad {
- background-color: #fdfdfd;
-}
-.perseus-mobile .graphie-container.above-scratchpad {
- background: #ffffff;
-}
-.graphie {
- -webkit-user-select: none;
- user-select: none;
-}
-.perseus-interactive,
-.perseus-interactive.above-scratchpad {
- position: relative;
- z-index: 3;
-}
-#answercontent input[type=text].perseus-input-size-normal,
-#answercontent input[type=number].perseus-input-size-normal,
-.framework-perseus input[type=text].perseus-input-size-normal,
-.framework-perseus input[type=number].perseus-input-size-normal {
- border: 1px solid #ccc;
- width: 80px;
-}
-#answercontent input[type=text].perseus-input-size-small,
-#answercontent input[type=number].perseus-input-size-small,
-.framework-perseus input[type=text].perseus-input-size-small,
-.framework-perseus input[type=number].perseus-input-size-small {
- border: 1px solid #ccc;
- width: 40px;
-}
-#answercontent input[type=text].perseus-input-right-align,
-#answercontent input[type=number].perseus-input-right-align,
-.framework-perseus input[type=text].perseus-input-right-align,
-.framework-perseus input[type=number].perseus-input-right-align {
- text-align: right;
-}
-.framework-perseus.perseus-mobile .perseus-input-right-align .keypad-input {
- text-align: right;
-}
-.framework-perseus div.paragraph {
- font-family: "Lato", sans-serif;
- font-weight: 400;
- font-size: 18px;
- line-height: 22px;
- margin: 22px 0px;
-}
-.framework-perseus .test-prep-blurb div.paragraph {
- font-size: 16px;
- line-height: 20px;
-}
-.framework-perseus div.instructions {
- display: block;
- font-family: "Noto Serif", serif;
- font-weight: 800;
- font-size: 18px;
- line-height: 22px;
- font-style: italic;
-}
-.framework-perseus .perseus-renderer > .paragraph > ul:not(.perseus-widget-radio),
-.framework-perseus .perseus-renderer > .paragraph > ol {
- margin: 0px 0px 22px 0px;
-}
-.framework-perseus .paragraph ul:not(.perseus-widget-radio, .indicatorContainer) {
- padding-left: 35px;
- list-style-type: disc;
-}
-.framework-perseus .paragraph ol {
- list-style: decimal;
- padding-left: 2em;
-}
-.framework-perseus blockquote {
- padding: 0 2.5em;
-}
-.framework-perseus .zoomable {
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
- -webkit-touch-callout: none;
-}
-.framework-perseus sup:not(.mq-non-leaf) {
- font-size: smaller;
- vertical-align: super;
- line-height: 0;
-}
-.framework-perseus .range-input {
- border: 1px solid #ccc;
- border-radius: 5px;
- display: inline-block;
- padding: 0px 5px;
-}
-.framework-perseus .range-input > input {
- border: 0;
- display: inline;
- text-align: center;
- width: 30px;
-}
-.framework-perseus .range-input > span {
- color: #999;
- font-size: 14px;
-}
-.framework-perseus .number-input {
- border: 1px solid #ccc;
- border-radius: 5px;
- margin: 0;
- padding: 5px 0;
- text-align: center;
- width: 40px;
-}
-.framework-perseus .number-input.invalid-input {
- background-color: #cf5044;
- outline-color: red;
-}
-.framework-perseus .number-input.mini {
- width: 40px;
-}
-.framework-perseus .number-input.small {
- width: 60px;
-}
-.framework-perseus .number-input.normal {
- width: 80px;
-}
-.framework-perseus .math-output {
- display: inline-block;
- min-width: 80px;
- min-height: 36px;
- border-radius: 5px;
- padding: 0;
- margin-top: 4px;
- margin-bottom: 4px;
- background: white;
- border: 1px solid #a4a4a4;
-}
-.framework-perseus .graph-settings .graph-settings-axis-label {
- border: 1px solid #ccc;
- border-radius: 5px;
- display: inline-block;
- padding: 5px 5px;
- width: 70px;
- float: right;
- margin: 0 5px;
-}
-.framework-perseus .graph-settings .graph-settings-background-url {
- width: 250px;
-}
-.framework-perseus .graphie-container {
- position: relative;
-}
-.framework-perseus .graph-settings,
-.framework-perseus .image-settings,
-.framework-perseus .misc-settings {
- padding-bottom: 5px;
-}
-.framework-perseus .misc-settings,
-.framework-perseus .type-settings {
- border-top: 1px solid black;
- padding-top: 5px;
-}
-.framework-perseus .svg-image {
- display: block;
- margin: 0 auto;
- margin-inline-start: auto;
- margin-inline-end: auto;
-}
-.framework-perseus .unresponsive-svg-image,
-.framework-perseus .perseus-rendered-radio .unresponsive-svg-image {
- display: inline-block;
- position: relative;
-}
-.framework-perseus .unresponsive-svg-image > .graphie-container,
-.framework-perseus .perseus-rendered-radio .unresponsive-svg-image > .graphie-container {
- position: absolute;
- top: 0;
- left: 0;
-}
-.framework-perseus .fixed-to-responsive {
- position: relative;
- width: 100%;
-}
-.framework-perseus .fixed-to-responsive > :not(:first-child) {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-/* Legacy table styles. Remove when XOM is rolled out (see below for
- updated XOM styles). */
-.framework-perseus:not(.perseus-mobile) table {
- font-size: 14px;
- line-height: 19.6px;
-}
-.framework-perseus:not(.perseus-mobile) table th,
-.framework-perseus:not(.perseus-mobile) table td {
- padding: 5px 10px;
- text-align: left;
-}
-.framework-perseus:not(.perseus-mobile) table th[align="center"],
-.framework-perseus:not(.perseus-mobile) table td[align="center"] {
- text-align: center;
-}
-.framework-perseus:not(.perseus-mobile) table th[align="right"],
-.framework-perseus:not(.perseus-mobile) table td[align="right"] {
- text-align: right;
-}
-.framework-perseus:not(.perseus-mobile) table th {
- border-bottom: 2px solid #ccc;
- font-weight: bold;
- padding-bottom: 2px;
-}
-.framework-perseus:not(.perseus-mobile) table tr:nth-child(odd) td {
- background-color: #f7f8fa;
-}
-.framework-perseus:not(.perseus-mobile) .perseus-titled-table {
- display: inline-block;
-}
-.framework-perseus:not(.perseus-mobile) .perseus-titled-table table {
- margin-left: auto;
- margin-right: auto;
-}
-.framework-perseus:not(.perseus-mobile) .perseus-table-title {
- text-align: center;
- font-size: larger;
-}
-.framework-perseus:not(.perseus-mobile) table.non-markdown tr:nth-child(odd) td {
- background-color: transparent;
-}
-.framework-perseus:not(.perseus-mobile) table.non-markdown th,
-.framework-perseus:not(.perseus-mobile) table.non-markdown td {
- border-width: 0;
-}
-/* New XOM styles for tables. */
-.framework-perseus.perseus-mobile {
- /* There are three kinds of tables:
- 1) normal "tables" - emitted by markdown (src/perseus-markdown.jsx)
- 2) "titled tables" - emitted by markdown as well (these are just
- tables with a title, and encased in one more element)
- 3) "table widget" - where a user is expected to enter answers in a
- table form (src/widgets/table.jsx).
- Moreover, there the Categorizer widget uses
tags, so these
- stylings will apply there as well. */
-}
-.framework-perseus.perseus-mobile table {
- border-collapse: collapse;
- margin: 0 auto;
-}
-.framework-perseus.perseus-mobile table tbody > tr {
- border: 1px solid #e5e5e5;
-}
-.framework-perseus.perseus-mobile table th,
-.framework-perseus.perseus-mobile table td {
- padding: 16px;
- text-align: left;
-}
-.framework-perseus.perseus-mobile table th[align="center"],
-.framework-perseus.perseus-mobile table td[align="center"] {
- text-align: center;
-}
-.framework-perseus.perseus-mobile table th[align="right"],
-.framework-perseus.perseus-mobile table td[align="right"] {
- text-align: right;
-}
-.framework-perseus.perseus-mobile table td {
- background: #fff;
-}
-.framework-perseus.perseus-mobile table th {
- font-weight: bold;
-}
-@media (max-width: 767px) {
- .framework-perseus.perseus-mobile table {
- width: 100%;
- min-width: 480px;
- }
- .framework-perseus.perseus-mobile table tbody > tr {
- border-left: 0;
- border-right: 0;
- }
-}
-.framework-perseus.perseus-mobile .perseus-titled-table {
- display: inline-block;
-}
-.framework-perseus.perseus-mobile .perseus-table-title {
- text-align: center;
- font-size: larger;
-}
-/* Widget CSS */
-.perseus-graph-padding {
- box-sizing: content-box;
- padding: 25px 25px 0 0;
-}
-.categorizer-container {
- margin-top: 20px;
-}
-.categorizer-container div.paragraph {
- margin: 10px 0px;
-}
-.categorizer-container .category {
- text-align: center;
-}
-.categorizer-container table {
- min-width: 0;
-}
-.categorizer-container label {
- position: relative;
- z-index: 2;
-}
-body.mobile .categorizer-container td.category input[type="radio"]:checked + span:before {
- color: #005a88;
-}
-body.mobile .categorizer-container td.category input[type="radio"] + span:active:before {
- color: #666;
- content: "\f111";
-}
-.perseus-widget-dropdown {
- position: relative;
-}
-.perseus-widget-expression {
- position: relative;
-}
-.perseus-widget-expression > span,
-.perseus-widget-expression .error-tooltip {
- display: inline-block;
- vertical-align: middle;
-}
-.perseus-widget-expression .error-tooltip {
- position: absolute;
- right: 6px;
- top: -2px;
-}
-.perseus-widget-expression .error-icon {
- color: #fcc335;
- cursor: pointer;
- font-size: 20px;
- position: relative;
- z-index: 3;
-}
-.perseus-widget-expression .error-text {
- background-color: #fff;
- padding: 5px;
- width: 210px;
-}
-.perseus-widget-expression .perseus-formats-tooltip {
- width: 190px;
-}
-#answer_area .perseus-widget-expression .perseus-math-input.mq-editable-field.mq-math-mode {
- min-width: 130px;
-}
-#answer_area .perseus-widget-expression .error-tooltip .error-text-container {
- left: -125px !important;
- top: -17px !important;
-}
-#answer_area .perseus-widget-expression .error-tooltip .error-text {
- font-size: 12px;
- width: 90px;
-}
-#answer_area .perseus-widget-expression .error-tooltip .tooltipContainer > div:first-child {
- visibility: hidden !important;
-}
-.perseus-widget-grapher {
- box-sizing: content-box;
- padding: 25px 25px 0 0;
-}
-.perseus-widget-grapher > .graphie-container {
- position: relative;
-}
-.perseus-widget-grapher > .graphie-container > img,
-.perseus-widget-grapher > .graphie-container .svg-image {
- position: absolute;
-}
-.framework-perseus .perseus-graded-group {
- position: relative;
- width: 100%;
- margin-left: 3px;
- padding-left: 5px;
-}
-.framework-perseus .perseus-graded-group.answer-correct {
- border-left: 3px solid #76a005;
- margin-left: 0;
-}
-.framework-perseus .perseus-graded-group.answer-incorrect {
- border-left: 3px solid #b2392e;
- margin-left: 0;
-}
-.framework-perseus .perseus-graded-group .group-icon {
- font-size: 14px;
- position: absolute;
- top: 50%;
- top: calc(50% - 7px);
- left: -30px;
- text-align: center;
- width: 16px;
-}
-.framework-perseus .perseus-group {
- position: relative;
- width: 100%;
-}
-.framework-perseus .perseus-group .group-icon {
- font-size: 14px;
- position: absolute;
- top: 50%;
- top: calc(50% - 7px);
- margin-left: -20px;
-}
-.perseus-image-widget .perseus-image-title {
- text-align: center;
-}
-.perseus-image-widget .perseus-image-caption {
- color: rgba(33, 36, 44, 0.7);
- padding-left: 16px;
- position: relative;
-}
-.perseus-image-widget .perseus-image-caption::before {
- content: "";
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- margin-left: 4px;
- width: 1px;
- background-color: rgba(33, 36, 44, 0.5);
- border: 1px solid rgba(33, 36, 44, 0.5);
- border-radius: 2px;
-}
-.perseus-image-widget .perseus-image-caption .paragraph {
- font-size: 14px;
- padding-right: 12px;
-}
-.perseus-image-editor .label-settings td {
- padding: 5px 4px;
- text-align: center;
-}
-.perseus-image-editor .label-settings tr:nth-child(odd) td {
- background-color: transparent;
-}
-.perseus-image-editor .label-settings th,
-.perseus-image-editor .label-settings td {
- border-width: 0;
-}
-.perseus-image-editor .image-settings,
-.perseus-image-editor .graph-settings {
- margin-top: 5px;
-}
-.perseus-image-widget {
- margin-inline-start: auto;
- margin-inline-end: auto;
-}
-.perseus-hint-renderer .perseus-image-widget {
- margin-inline-start: 0;
-}
-.perseus-hint-renderer .svg-image {
- margin-inline-start: 0;
-}
-.perseus-widget-interactive-graph {
- box-sizing: content-box;
- padding: 25px 25px 0 0;
-}
-.perseus-widget-interactive-graph > .graphie-container {
- position: relative;
-}
-.perseus-widget-interactive-graph > .graphie-container > img,
-.perseus-widget-interactive-graph > .graphie-container > .unresponsive-svg-image {
- position: absolute;
- bottom: 0;
- left: 0;
-}
-.perseus-mobile .tooltip.visible {
- z-index: 2;
-}
-.perseus-mobile .tooltip.visible .tooltip-content:before {
- border: solid;
- border-color: white transparent;
- border-width: 10px 10px 0 10px;
- bottom: -10px;
- content: "";
- left: 50%;
- transform: translateX(-50%);
- position: absolute;
- z-index: 2;
-}
-.perseus-mobile .tooltip .tooltip-content {
- display: none;
-}
-.perseus-mobile .tooltip.visible .tooltip-content {
- display: inline-block;
- background-color: #ffffff;
- border-radius: 5px;
- bottom: 50px;
- left: 50%;
- transform: translateX(-50%);
- padding: 5px;
- position: absolute;
- white-space: nowrap;
- min-width: 30px;
- text-align: center;
-}
-.perseus-mobile .tooltip.visible .tooltip-content .katex,
-.perseus-mobile .tooltip.visible .tooltip-content mjx-container {
- color: #71b307 !important;
-}
-.perseus-mobile .graphie-label .katex,
-.perseus-mobile .graphie-label mjx-container {
- color: inherit !important;
-}
-.framework-perseus .perseus-label-image-widget-instructions div.paragraph {
- margin: 0;
-}
-@media (max-width: 767px) {
- [id*="perseus-label-image-widget-answer-pill"] > div.perseus-renderer-responsive {
- all: revert;
- }
- .framework-perseus.perseus-mobile [id*="perseus-label-image-widget-answer-pill"] mjx-container {
- all: revert;
- }
-}
-.framework-perseus:not(.perseus-article).perseus-mobile .perseus-label-image-widget-instructions {
- color: initial;
-}
-.framework-perseus:not(.perseus-article).perseus-mobile .perseus-label-image-widget-instructions .perseus-renderer .paragraph > .paragraph {
- color: initial !important;
- font-size: initial !important;
- line-height: initial !important;
-}
-.framework-perseus:not(.perseus-article).perseus-mobile .perseus-label-image-widget-instructions .perseus-block-math {
- padding: 0;
-}
-.framework-perseus:not(.perseus-article).perseus-mobile .perseus-label-image-widget-instructions .perseus-block-math .katex,
-.framework-perseus:not(.perseus-article).perseus-mobile .perseus-label-image-widget-instructions .perseus-block-math mjx-container {
- font-size: initial !important;
- line-height: initial !important;
-}
-.perseus-label-image-widget-answer-choices .perseus-block-math > .perseus-block-math-inner,
-.perseus-label-image-widget-instructions .perseus-block-math > .perseus-block-math-inner {
- overflow-x: hidden !important;
-}
-.perseus-widget-matcher {
- /* Ideally we'd get rid of this, as most of the styles have been moved
- to inline styles using Aphrodite, but this is a hacky "reach-in" into
- descendent widgets that we can't modify */
-}
-.perseus-widget-matcher div.paragraph {
- margin: 0;
-}
-.perseus-matrix .matrix-prefix,
-.perseus-matrix .matrix-suffix {
- display: inline-block;
- margin: 10px 5px 0 10px;
- vertical-align: top;
-}
-.perseus-matrix .matrix-suffix {
- margin: 10px 10px 0 5px;
-}
-.perseus-matrix div.paragraph {
- margin: 0;
-}
-.perseus-matrix .matrix-input {
- background: #e2e2e2;
- display: inline-block;
- margin: 5px;
- padding: 3px;
- position: relative;
- width: auto;
-}
-.perseus-matrix .matrix-row {
- white-space: nowrap;
-}
-.perseus-matrix .matrix-bracket {
- border-color: #666;
- border-style: solid;
- border-bottom-width: 2px;
- border-top-width: 2px;
- margin-top: -2px;
- position: absolute;
- width: 6px;
-}
-.perseus-matrix .matrix-bracket.bracket-left {
- border-color: #666;
- border-left-width: 2px;
- border-right-width: 0;
- left: 3px;
-}
-.perseus-matrix .matrix-bracket.bracket-right {
- border-color: #666;
- border-left-width: 0;
- border-right-width: 2px;
- margin-left: -3px;
-}
-.perseus-matrix input,
-.perseus-matrix .number-input {
- border: none;
- border-radius: 0;
- box-sizing: border-box;
- margin: 3px;
- padding: 0;
- text-align: center;
-}
-.perseus-matrix input.outside,
-.perseus-matrix .number-input.outside {
- background: #f3f3f3;
-}
-.perseus-matrix input:focus,
-.perseus-matrix .number-input:focus {
- border: none;
- outline: none;
-}
-.static-mode.perseus-matrix input,
-.static-mode.perseus-matrix .number-input {
- background: #f5f5f5;
-}
-.perseus-matrix.the-matrix .matrix-bracket,
-.perseus-matrix.the-matrix .matrix-left,
-.perseus-matrix.the-matrix .matrix-right {
- border-color: #29f139;
-}
-.perseus-matrix.the-matrix .matrix-input {
- background: #222;
-}
-.perseus-matrix.the-matrix input,
-.perseus-matrix.the-matrix .number-input {
- background: #666;
- color: #29f139;
- font-weight: bold;
-}
-.perseus-matrix.the-matrix input.outside,
-.perseus-matrix.the-matrix .number-input.outside {
- background: #444;
-}
-body.mobile .perseus-matrix .matrix-input {
- display: table;
-}
-body.mobile .perseus-matrix .matrix-row {
- display: table-row;
-}
-body.mobile .perseus-matrix .matrix-input-field {
- display: table-cell;
-}
-body.mobile .perseus-matrix .math-output {
- margin: 4px 4px 2px 4px;
- max-height: 36px;
- max-width: 80px;
- overflow: hidden;
-}
-.perseus-matrix-editor .perseus-single-editor {
- width: 338px;
-}
-.perseus-widget-measurer {
- position: relative;
-}
-.perseus-widget-measurer img {
- position: absolute;
-}
-.perseus-widget-measurer-url {
- width: 70%;
-}
-#translations-dashboard .perseus-widget-measurer > .graphie {
- z-index: -1;
-}
-.orderer {
- position: relative;
-}
-.orderer.layout-horizontal .draggable-box {
- margin-left: 0;
- margin-top: 30px;
- padding: 13px;
-}
-.orderer .card {
- padding: 0 10px;
- cursor: pointer;
- position: relative;
- -webkit-user-select: none;
- user-select: none;
- width: auto;
- display: flex;
- flex-direction: column;
- justify-content: center;
-}
-.orderer.height-normal.layout-horizontal .card {
- height: 65px;
-}
-.orderer.height-normal.layout-vertical .card {
- padding: 5px;
-}
-.orderer.height-auto .card {
- padding: 0;
-}
-.orderer.height-auto.layout-horizontal .drag-hint {
- min-height: 65px;
- min-width: 22px;
-}
-.orderer.layout-horizontal .bank {
- padding: 0;
- margin: 0px 13px;
-}
-.orderer div.paragraph {
- margin: 0;
-}
-.orderer .card-wrap {
- position: relative;
- z-index: 3;
- width: auto;
-}
-.orderer.layout-horizontal .card-wrap {
- float: left;
-}
-.orderer.layout-horizontal .card-wrap:not(:first-child) {
- margin-left: 8px;
-}
-.orderer.layout-vertical .card-wrap {
- float: none;
- text-align: center;
-}
-.orderer.layout-vertical .card-wrap:not(:first-child) {
- margin-top: 8px;
-}
-.orderer.layout-vertical .bank,
-.orderer.layout-vertical .draggable-box {
- box-sizing: border-box;
- float: left;
- max-width: 50%;
-}
-.orderer.layout-vertical .bank {
- padding: 11px 20px 11px 0;
- margin: 0;
-}
-.orderer.layout-vertical .draggable-box {
- margin-top: 0;
- padding: 10px;
- min-height: 170px;
-}
-.orderer.layout-vertical .draggable-box .drag-hint {
- box-sizing: border-box;
- min-width: 140px;
- min-height: 34px;
-}
-.orderer.layout-vertical .draggable-box .placeholder {
- box-sizing: border-box;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage,
-.perseus-widget-passage-container .perseus-widget-passage {
- line-height: 20px;
- margin: 22px;
- position: relative;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage div.paragraph,
-.perseus-widget-passage-container .perseus-widget-passage div.paragraph {
- font-family: KaTeX_Main, Times, "Times New Roman", serif;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage .passage-title div.paragraph,
-.perseus-widget-passage-container .perseus-widget-passage .passage-title div.paragraph {
- font-family: "Noto Serif", serif;
- font-weight: 700;
- font-size: 20px;
- line-height: 22px;
- margin: 0 0 10px;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph,
-.perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph {
- font-weight: 400;
- font-size: 16px;
- line-height: 20px;
- text-indent: 20px;
- margin: 0;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph span,
-.perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph span {
- text-indent: 0;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph em,
-.perseus-widget-passage-container .perseus-widget-passage > .passage-text div.paragraph em {
- line-height: 0;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .katex,
-.perseus-widget-passage-container .katex,
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container mjx-container,
-.perseus-widget-passage-container mjx-container {
- line-height: 18px;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .footnotes,
-.perseus-widget-passage-container .footnotes {
- margin-top: 22px;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .footnotes div.paragraph,
-.perseus-widget-passage-container .footnotes div.paragraph {
- font-size: 14px;
- margin: 0;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-highlight,
-.perseus-widget-passage-container .perseus-highlight {
- background-color: #fffabe;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-review-highlight,
-.perseus-widget-passage-container .perseus-review-highlight {
- background-color: #eee7b2;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-passage-square-label,
-.perseus-widget-passage-container .perseus-passage-square-label,
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-passage-circle-label,
-.perseus-widget-passage-container .perseus-passage-circle-label,
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-passage-bracket-label,
-.perseus-widget-passage-container .perseus-passage-bracket-label {
- font-family: Times, "Times New Roman", serif;
- font-size: 16px;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .line-numbers,
-.perseus-widget-passage-container .line-numbers {
- font-size: 12px;
- font-style: italic;
- font-weight: 600;
- position: absolute;
- text-align: right;
- max-height: 100%;
- overflow: hidden;
- padding-right: 6px;
- left: -52px;
- width: 37px;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .line-numbers span,
-.perseus-widget-passage-container .line-numbers span {
- display: block;
- line-height: 20px;
- position: relative;
- top: 2px;
- visibility: hidden;
-}
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .line-numbers span:nth-of-type(5n),
-.perseus-widget-passage-container .line-numbers span:nth-of-type(5n),
-.framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .line-numbers .line-marker,
-.perseus-widget-passage-container .line-numbers .line-marker {
- visibility: visible;
-}
-.perseus-widget-passage-editor .perseus-single-editor {
- font-family: Times, "Times New Roman", serif;
- margin-left: -11px;
-}
-.perseus-widget-passage-editor .perseus-textarea-pair textarea {
- font-size: 13px;
- line-height: 17px;
-}
-body.sat-section .perseus-widget-passage {
- margin-top: 0;
-}
-.perseus-widget-plotter svg,
-.perseus-widget-plotter vml {
- position: absolute;
-}
-.perseus-widget-plotter span.rotate {
- transform: rotate(-90deg);
-}
-.set-from-scale-box {
- border: 2px solid #eeeeee;
- border-radius: 3px;
- padding: 3px;
-}
-.categories-title {
- font-size: 14px;
-}
-.perseus-mobile .perseus-widget-plotter {
- border: solid 0.5px #babec2;
- border-radius: 4px;
-}
-.perseus-mobile .perseus-widget-plotter .graphie-label .katex,
-.perseus-mobile .perseus-widget-plotter .graphie-label mjx-container {
- color: #626569;
-}
-.perseus-widget-radio.perseus-rendered-radio div,
-.perseus-widget-radio.perseus-rendered-radio div > p {
- /* TODO(alpert): Find a better way of doing inline renderers */
- display: inline;
-}
-.perseus-widget-radio.perseus-rendered-radio .perseus-radio-rationale-content > .perseus-renderer > .paragraph {
- display: table;
- margin: 0;
-}
-.perseus-widget-radio.perseus-rendered-radio .perseus-radio-rationale-content > .perseus-renderer > .paragraph:not(:last-child) {
- margin-bottom: 8px;
-}
-.perseus-widget-radio .unresponsive-svg-image div,
-.perseus-widget-radio .svg-image div {
- display: block;
-}
-.perseus-widget-radio li div.instructions {
- margin-bottom: 5px;
-}
-.perseus-widget-radio li .value {
- display: block;
- margin-left: 18px;
- min-height: 22px;
-}
-.perseus-widget-radio li img,
-.perseus-widget-radio li table {
- display: inline-block;
- vertical-align: middle;
-}
-.perseus-widget-radio li table {
- border: 1px solid #ccc;
-}
-.perseus-widget-radio .perseus-radio-option {
- /* Radio options with images that are set apart should be on their own line */
-}
-.perseus-widget-radio .perseus-radio-option .paragraph + .paragraph:has(img:only-child),
-.perseus-widget-radio .perseus-radio-option .paragraph:has(img:only-child):has(+ .paragraph) {
- display: block;
-}
-.perseus-widget-radio-fieldset .instructions {
- font-family: "Lato", sans-serif;
-}
-.draggy-boxy-thing .draggable-box,
-.draggy-boxy-thing .cards-area {
- background: #eee;
- border: 1px solid #ccc;
- border-bottom: 1px solid #aaa;
- box-shadow: 0 1px 2px #ccc;
-}
-.draggy-boxy-thing .cards-area {
- position: relative;
- z-index: 2;
-}
-.draggy-boxy-thing .card {
- position: relative;
- z-index: 3;
- background-color: #fff;
- border: 1px solid #b9b9b9;
- border-bottom-color: #939393;
- border-radius: 4px;
- cursor: pointer;
- touch-action: none;
-}
-.draggy-boxy-thing .card.placeholder {
- background: #ddd;
- border: 1px solid #ccc;
-}
-.draggy-boxy-thing .card.drag-hint {
- background: none;
- border: 1px dashed #aaa;
- cursor: auto;
-}
-.draggy-boxy-thing .card.drag-hint:hover {
- border-color: #aaa;
- box-shadow: none;
-}
-.draggy-boxy-thing .card.dragging {
- background-color: #ffedcd;
- opacity: 0.8;
- filter: opacity(0.8);
-}
-.draggy-boxy-thing .card.stack {
- z-index: auto;
-}
-.draggy-boxy-thing .card.stack:after {
- content: " ";
- background-color: #fff;
- border: 1px solid #b9b9b9;
- border-bottom-color: #939393;
- border-radius: 4px;
- height: 100%;
- width: 100%;
- z-index: -1;
- top: 1px;
- left: 1px;
- position: absolute;
-}
-.draggy-boxy-thing .card:hover {
- border-color: #ffa500;
- box-shadow: 0 0 4px #c78100;
-}
-.perseus-sortable div.paragraph {
- margin: 0;
-}
-.perseus-sortable .perseus-sortable-draggable:before {
- content: "";
- display: inline-block;
- height: 100%;
- vertical-align: middle;
-}
-.perseus-sortable .perseus-sortable-draggable > div {
- display: inline-block;
- font-size: 14px;
- max-width: 100%;
- vertical-align: middle;
-}
-.perseus-sortable .perseus-sortable-draggable-unpadded img {
- vertical-align: bottom;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown {
- text-align: left;
- margin: 20px auto;
- border-collapse: collapse;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown tr {
- height: 23px;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown th,
-.framework-perseus table.perseus-widget-table-of-values.non-markdown td {
- border: 2px solid black;
- border-width: 0 2px;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown th:first-child,
-.framework-perseus table.perseus-widget-table-of-values.non-markdown td:first-child {
- border-left: 0;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown th:last-child,
-.framework-perseus table.perseus-widget-table-of-values.non-markdown td:last-child {
- border-right: 0;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown th {
- font-weight: normal;
- padding: 5px;
- width: 80px;
- text-align: left;
- border-bottom: 2px solid black;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown th .paragraph {
- margin: 0;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown td {
- padding: 0px 5px;
-}
-.framework-perseus table.perseus-widget-table-of-values.non-markdown tbody tr:first-child td {
- padding-top: 5px;
-}
-.framework-perseus table.perseus-widget-table-of-values input,
-#answer_area table.perseus-widget-table-of-values input {
- width: 80px;
-}
-body.mobile .framework-perseus table.perseus-widget-table-of-values.non-markdown td {
- padding: 5px;
-}
-.old-unit-input input,
-.unit-editor-canonical {
- border: 1px solid #a4a4a4;
- border-radius: 5px;
- font-size: 14px;
- padding: 6px;
-}
-.unit-editor > div {
- margin: 5px 0;
-}
-.perseus-widget-container.widget-nohighlight {
- transition: all 0.15s;
-}
-.perseus-widget-container.widget-highlight {
- box-shadow: 0px 0px 0px 2px #ffa500;
- transition: all 0.15s;
-}
-.perseus-widget-container.widget-inline {
- display: inline;
-}
-.perseus-widget-container.widget-inline-block {
- display: inline-block;
-}
-.bibliotron-exercise .perseus-hint-renderer {
- border-left: 4px solid #f6f7f7;
- padding-left: 16px;
- position: relative;
-}
-.bibliotron-exercise .perseus-hint-renderer:focus {
- border-left-color: #d6d8da;
- outline: none;
-}
-.bibliotron-exercise .perseus-hint-renderer:before,
-.bibliotron-exercise .perseus-hint-renderer:after {
- content: "";
- display: table;
- clear: both;
-}
-.bibliotron-exercise .perseus-hint-renderer div.paragraph {
- margin-top: 0px;
- margin-bottom: 16px;
-}
-.bibliotron-exercise .perseus-hint-renderer.last-hint {
- margin-bottom: 32px;
-}
-@media (max-width: 767px) {
- .bibliotron-exercise .perseus-hint-renderer.last-hint {
- margin-bottom: 0;
- }
-}
-.perseus-hint-label {
- color: #00457c;
- display: none;
- font-weight: 600;
- margin-right: 13px;
- position: absolute;
- right: 100%;
- white-space: nowrap;
-}
-.perseus-domain-science .perseus-hint-label {
- color: #9e034e;
-}
-.perseus-domain-math .perseus-hint-label {
- color: #007d96;
-}
-.perseus-domain-economics .perseus-hint-label {
- color: #a75a05;
-}
-.perseus-domain-partner .perseus-hint-label {
- color: #208170;
-}
-.perseus-domain-humanities .perseus-hint-label {
- color: #be2612;
-}
-.perseus-domain-test-prep .perseus-hint-label {
- color: #543b78;
-}
-.perseus-domain-cs .perseus-hint-label {
- color: #0d923f;
-}
-.bibliotron-exercise .perseus-hint-renderer.last-rendered .perseus-hint-label {
- display: block;
-}
-@media (max-width: 767px) {
- .bibliotron-exercise .perseus-hint-renderer.last-rendered .perseus-hint-label {
- display: none;
- }
-}
-.perseus-tooltip {
- background: #fff;
- padding: 5px 10px;
- width: 240px;
-}
-.perseus-formats-tooltip {
- background: #fff;
- padding: 5px 10px;
- width: 240px;
- color: #777;
-}
-.framework-perseus .perseus-formats-tooltip .paragraph > ul {
- padding: 0;
- margin: -20px 0 -16px 0;
-}
-.framework-perseus .perseus-formats-tooltip .paragraph > ul > li {
- list-style-type: none;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode {
- background-color: transparent;
- font-size: 18px;
- min-width: 100px;
- border: unset;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode.mq-focused {
- box-shadow: unset;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode > .mq-root-block {
- padding: 4px;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode .mq-cursor {
- padding-left: 0;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode .mq-paren.mq-ghost {
- color: inherit;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode .mq-paren + span {
- margin: 0;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode .mq-binary-operator {
- font-family: KaTeX_Main !important;
-}
-.perseus-math-input.mq-editable-field.mq-math-mode sup {
- line-height: normal;
-}
-.perseus-widget-editor .perseus-math-input.mq-editable-field.mq-math-mode > .mq-root-block {
- border-radius: 0;
-}
-.math-input-buttons {
- background-color: rgba(255, 255, 255, 0.7);
- border-radius: 5px;
- border: 1px solid #ddd;
- box-sizing: border-box;
- margin-top: 5px;
- padding: 2px;
- width: 201px;
-}
-.math-input-buttons.absolute {
- left: -2px;
- position: absolute;
- top: -3px;
- z-index: 5;
-}
-.tex-button {
- display: block;
- float: left;
- width: 35px;
- height: 35px;
- margin: 2px;
- border: 1px solid #1c758a;
- background-color: white;
- border-radius: 5px;
-}
-.tex-button:hover {
- cursor: pointer;
- background-color: #f0f0f0;
-}
-.tex-button:focus {
- border: 2px solid #1c758a;
- outline: none;
-}
-.tex-button-row {
- margin: 5px 0;
-}
-.tex-button-row:first-child {
- margin-top: 0;
-}
-.tex-button-row:last-child {
- margin-bottom: 0;
-}
-.renderer-widget-error {
- background-color: #fcc;
-}
-.perseus-error {
- background: #cf5044;
- border: 2px solid red;
- border-radius: 5px;
- padding: 20px;
- margin: 15px 0 10px;
-}
-@media (max-width: 767px) {
- .perseus-renderer-responsive {
- margin: 0 16px;
- }
- .perseus-renderer-responsive .perseus-renderer-responsive {
- margin: 0;
- }
-}
-@media (max-width: 767px) {
- .perseus-mobile .perseus-block-math {
- font-size: 18px;
- }
-}
-.perseus-widget-editor-content .locked-figure-accordion h2 {
- padding-top: 0;
-}
-.keypad-input {
- outline: none !important;
-}
-.keypad-input .mq-editable-field .mq-root-block {
- overflow-x: auto;
-}
-.keypad-input .mq-editable-field .mq-cursor:not(:only-child),
-.keypad-input .mq-editable-field .mq-root-block.mq-hasCursor > .mq-cursor:only-child {
- /* HACK(charlie): Magic numbers to properly size and position the vertical
- cursor, which is visible whenever the cursor is not alone in its parent,
- with the exception that it's also visible when the entire input is
- empty. */
- height: 20px !important;
- width: 2px;
- margin-top: -5px !important;
- vertical-align: middle !important;
- border-radius: 1px !important;
-}
-.keypad-input .mq-editable-field .mq-cursor {
- border-left: 2px solid #1865f2 !important;
- margin-left: -1px !important;
- margin-right: -1px !important;
- opacity: 1 !important;
- transition: opacity 300ms ease !important;
- visibility: visible !important;
-}
-.keypad-input .mq-editable-field .mq-cursor.mq-blink {
- opacity: 0 !important;
- visibility: visible !important;
-}
-.keypad-input .mq-editable-field .mq-non-leaf .mq-cursor:only-child {
- border: 2px solid !important;
- border-color: #1865f2 !important;
- border-radius: 1px;
- opacity: 1 !important;
- padding: 0 4px 0 4px;
- transition: border-color 300ms ease !important;
-}
-.keypad-input .mq-editable-field .mq-non-leaf .mq-cursor:only-child.mq-blink {
- border-color: #1865f2 !important;
- opacity: 1 !important;
-}
-.keypad-input .mq-empty {
- background: transparent !important;
-}
-.keypad-input .mq-empty:not(.mq-root-block):after,
-.keypad-input .mq-hasCursor:empty:not(.mq-root-block):after {
- border: 2px solid rgba(33, 36, 44, 0.16);
- border-radius: 1px;
- color: transparent;
- display: inline-block;
- margin-left: -1px;
- margin-right: -1px;
- padding: 0 4px 0 4px;
- visibility: visible !important;
-}
-.keypad-input .mq-selection .mq-empty:not(.mq-root-block):after {
- border-color: white;
-}
-.keypad-input .mq-hasCursor:empty:not(.mq-root-block):after {
- content: "c";
-}
-.keypad-input .mq-math-mode .mq-selection .mq-non-leaf,
-.keypad-input .mq-editable-field .mq-selection .mq-non-leaf {
- background: #1865f2 !important;
- border-color: white !important;
- color: white !important;
-}
-.keypad-input .mq-math-mode .mq-selection .mq-scaled,
-.keypad-input .mq-editable-field .mq-selection .mq-scaled {
- background: transparent !important;
- border-color: transparent !important;
- color: white !important;
-}
-.keypad-input .mq-selection {
- background: #1865f2 !important;
- border-color: white !important;
- color: white !important;
- display: inline-block !important;
-}
-/**
- * @license
- * MathQuill v0.10.1, by Han, Jeanine, and Mary
- * http://mathquill.com | maintainers@mathquill.com
- *
- * This Source Code Form is subject to the terms of the
- * Mozilla Public License, v. 2.0. If a copy of the MPL
- * was not distributed with this file, You can obtain
- * one at http://mozilla.org/MPL/2.0/.
- */
-
-.mq-aria-alert {
- position: absolute;
- left: -1000px;
- top: -1000px;
- width: 0px;
- height: 0px;
- text-align: left;
- overflow: hidden;
-}
-.mq-mathspeak {
- position: absolute;
- left: -1000px;
- top: -1000px;
- width: 0px;
- height: 0px;
- text-align: left;
- overflow: hidden;
-}
-@font-face {
- font-family: Symbola;
- src: local('Symbola Regular'), local('Symbola'), url(fonts/Symbola.woff) format('woff');
-}
-.mq-editable-field {
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-editable-field .mq-cursor {
- border-left: 1px solid currentColor;
- margin-left: -1px;
- position: relative;
- z-index: 1;
- padding: 0;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-editable-field .mq-cursor.mq-blink {
- visibility: hidden;
-}
-.mq-editable-field,
-.mq-math-mode .mq-editable-field {
- border: 1px solid gray;
-}
-.mq-editable-field.mq-focused,
-.mq-math-mode .mq-editable-field.mq-focused {
- -webkit-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- -moz-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- border-color: #709ac0;
-}
-.mq-math-mode .mq-editable-field {
- margin: 1px;
-}
-.mq-editable-field .mq-latex-command-input {
- color: inherit;
- font-family: 'Courier New', monospace;
- border: 1px solid gray;
- padding-right: 1px;
- margin-right: 1px;
- margin-left: 2px;
-}
-.mq-editable-field .mq-latex-command-input.mq-empty {
- background: transparent;
-}
-.mq-editable-field .mq-latex-command-input.mq-hasCursor {
- border-color: ActiveBorder;
-}
-.mq-editable-field.mq-empty:after,
-.mq-editable-field.mq-text-mode:after,
-.mq-math-mode .mq-empty:after {
- visibility: hidden;
- content: 'c';
-}
-.mq-editable-field .mq-cursor:only-child:after,
-.mq-editable-field .mq-textarea + .mq-cursor:last-child:after {
- visibility: hidden;
- content: 'c';
-}
-.mq-editable-field .mq-text-mode .mq-cursor:only-child:after {
- content: '';
-}
-.mq-editable-field.mq-text-mode {
- overflow-x: auto;
- overflow-y: hidden;
-}
-.mq-root-block,
-.mq-math-mode .mq-root-block {
- display: -moz-inline-box;
- display: inline-block;
- width: 100%;
- padding: 2px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- white-space: nowrap;
- overflow: hidden;
- vertical-align: middle;
-}
-.mq-root-block .mq-digit,
-.mq-math-mode .mq-root-block .mq-digit {
- margin-left: 0.009em;
- margin-right: 0.009em;
-}
-.mq-root-block .mq-group-start,
-.mq-math-mode .mq-root-block .mq-group-start {
- margin-left: 0.11em;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-other,
-.mq-math-mode .mq-root-block .mq-group-other {
- margin-left: -0.01em;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-leading-1,
-.mq-math-mode .mq-root-block .mq-group-leading-1,
-.mq-root-block .mq-group-leading-2,
-.mq-math-mode .mq-root-block .mq-group-leading-2 {
- margin-left: 0;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-leading-3,
-.mq-math-mode .mq-root-block .mq-group-leading-3 {
- margin-left: 0.036em;
- margin-right: -0.01em;
-}
-.mq-root-block.mq-suppress-grouping .mq-group-start,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-start,
-.mq-root-block.mq-suppress-grouping .mq-group-other,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-other,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-1,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-1,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-2,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-2,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-3,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-3 {
- margin-left: 0.009em;
- margin-right: 0.009em;
-}
-.mq-math-mode {
- font-variant: normal;
- font-weight: normal;
- font-style: normal;
- font-size: 115%;
- line-height: 1;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode .mq-non-leaf,
-.mq-math-mode .mq-scaled {
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode var,
-.mq-math-mode .mq-text-mode,
-.mq-math-mode .mq-nonSymbola {
- font-family: 'Times New Roman', Symbola, serif;
- line-height: 0.9;
-}
-.mq-math-mode svg {
- fill: currentColor;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-.mq-math-mode * {
- font-size: inherit;
- line-height: inherit;
- margin: 0;
- padding: 0;
- border-color: black;
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
- box-sizing: border-box;
-}
-.mq-math-mode .mq-empty {
- background: rgba(0, 0, 0, 0.2);
-}
-.mq-math-mode .mq-empty.mq-root-block {
- background: transparent;
-}
-.mq-math-mode .mq-empty.mq-quiet-delimiter {
- background: transparent;
-}
-.mq-math-mode.mq-empty {
- background: transparent;
-}
-.mq-math-mode .mq-text-mode {
- display: inline-block;
- white-space: pre;
-}
-.mq-math-mode .mq-text-mode.mq-hasCursor {
- box-shadow: inset darkgray 0 0.1em 0.2em;
- padding: 0 0.1em;
- margin: 0 -0.1em;
- min-width: 1ex;
-}
-.mq-math-mode .mq-font {
- font: 1em 'Times New Roman', Symbola, serif;
-}
-.mq-math-mode .mq-font * {
- font-family: inherit;
- font-style: inherit;
-}
-.mq-math-mode b,
-.mq-math-mode b.mq-font {
- font-weight: bolder;
-}
-.mq-math-mode var,
-.mq-math-mode i,
-.mq-math-mode i.mq-font {
- font-style: italic;
-}
-.mq-math-mode var.mq-f {
- margin-right: 0.2em;
- margin-left: 0.1em;
-}
-.mq-math-mode .mq-roman var.mq-f {
- margin: 0;
-}
-.mq-math-mode big {
- font-size: 200%;
-}
-.mq-math-mode .mq-int > big {
- display: inline-block;
- -webkit-transform: scaleX(0.7);
- -moz-transform: scaleX(0.7);
- -ms-transform: scaleX(0.7);
- -o-transform: scaleX(0.7);
- transform: scaleX(0.7);
- vertical-align: -0.16em;
-}
-.mq-math-mode .mq-int > .mq-supsub {
- font-size: 80%;
- vertical-align: -1.1em;
- padding-right: 0.2em;
-}
-.mq-math-mode .mq-int > .mq-supsub > .mq-sup > .mq-sup-inner {
- vertical-align: 1.3em;
-}
-.mq-math-mode .mq-int > .mq-supsub > .mq-sub {
- margin-left: -0.35em;
-}
-.mq-math-mode .mq-roman {
- font-style: normal;
-}
-.mq-math-mode .mq-sans-serif {
- font-family: sans-serif, Symbola, serif;
-}
-.mq-math-mode .mq-monospace {
- font-family: monospace, Symbola, serif;
-}
-.mq-math-mode .mq-overline {
- border-top: 1px solid;
- margin-top: 1px;
-}
-.mq-math-mode .mq-underline {
- border-bottom: 1px solid;
- margin-bottom: 1px;
-}
-.mq-math-mode .mq-binary-operator {
- padding: 0 0.2em;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode .mq-supsub {
- text-align: left;
- font-size: 90%;
- vertical-align: -0.5em;
-}
-.mq-math-mode .mq-supsub.mq-sup-only {
- vertical-align: 0.5em;
-}
-.mq-math-mode .mq-supsub.mq-sup-only > .mq-sup {
- display: inline-block;
- vertical-align: text-bottom;
-}
-.mq-math-mode .mq-supsub .mq-sup {
- display: block;
-}
-.mq-math-mode .mq-supsub .mq-sub {
- display: block;
- float: left;
-}
-.mq-math-mode .mq-supsub .mq-binary-operator {
- padding: 0 0.1em;
-}
-.mq-math-mode .mq-supsub .mq-fraction {
- font-size: 70%;
-}
-.mq-math-mode sup.mq-nthroot {
- font-size: 80%;
- vertical-align: 0.8em;
- margin-right: -0.6em;
- margin-left: 0.2em;
- min-width: 0.5em;
-}
-.mq-math-mode .mq-ghost svg {
- opacity: 0.2;
-}
-.mq-math-mode .mq-bracket-middle {
- margin-top: 0.1em;
- margin-bottom: 0.1em;
-}
-.mq-math-mode .mq-bracket-l,
-.mq-math-mode .mq-bracket-r {
- position: absolute;
- top: 0;
- bottom: 2px;
-}
-.mq-math-mode .mq-bracket-l {
- left: 0;
-}
-.mq-math-mode .mq-bracket-r {
- right: 0;
-}
-.mq-math-mode .mq-bracket-container {
- position: relative;
-}
-.mq-math-mode .mq-array {
- vertical-align: middle;
- text-align: center;
-}
-.mq-math-mode .mq-array > span {
- display: block;
-}
-.mq-math-mode .mq-operator-name {
- font-family: Symbola, 'Times New Roman', serif;
- line-height: 0.9;
- font-style: normal;
-}
-.mq-math-mode var.mq-operator-name.mq-first {
- padding-left: 0.2em;
-}
-.mq-math-mode var.mq-operator-name.mq-last,
-.mq-math-mode .mq-supsub.mq-after-operator-name {
- padding-right: 0.2em;
-}
-.mq-math-mode .mq-fraction {
- font-size: 90%;
- text-align: center;
- vertical-align: -0.4em;
- padding: 0 0.2em;
-}
-.mq-math-mode .mq-fraction,
-.mq-math-mode .mq-large-operator,
-.mq-math-mode x:-moz-any-link {
- display: -moz-groupbox;
-}
-.mq-math-mode .mq-fraction,
-.mq-math-mode .mq-large-operator,
-.mq-math-mode x:-moz-any-link,
-.mq-math-mode x:default {
- display: inline-block;
-}
-.mq-math-mode .mq-numerator,
-.mq-math-mode .mq-denominator,
-.mq-math-mode .mq-dot-recurring {
- display: block;
-}
-.mq-math-mode .mq-numerator {
- padding: 0 0.1em;
-}
-.mq-math-mode .mq-denominator {
- border-top: 1px solid;
- float: right;
- width: 100%;
- padding: 0.1em;
-}
-.mq-math-mode .mq-dot-recurring {
- text-align: center;
- height: 0.3em;
-}
-.mq-math-mode .mq-sqrt-prefix {
- position: absolute;
- top: 1px;
- bottom: 0.15em;
- width: 0.95em;
-}
-.mq-math-mode .mq-sqrt-container {
- position: relative;
-}
-.mq-math-mode .mq-sqrt-stem {
- border-top: 1px solid;
- margin-top: 1px;
- margin-left: 0.9em;
- padding-left: 0.15em;
- padding-right: 0.2em;
- margin-right: 0.1em;
- padding-top: 1px;
-}
-.mq-math-mode .mq-diacritic-above {
- display: block;
- text-align: center;
- line-height: 0.4em;
-}
-.mq-math-mode .mq-diacritic-stem {
- display: block;
- text-align: center;
-}
-.mq-math-mode .mq-hat-prefix {
- display: block;
- text-align: center;
- line-height: 0.95em;
- margin-bottom: -0.7em;
- transform: scaleX(1.5);
- -moz-transform: scaleX(1.5);
- -o-transform: scaleX(1.5);
- -webkit-transform: scaleX(1.5);
-}
-.mq-math-mode .mq-hat-stem {
- display: block;
-}
-.mq-math-mode .mq-large-operator {
- vertical-align: -0.2em;
- padding: 0.2em;
- text-align: center;
-}
-.mq-math-mode .mq-large-operator .mq-from,
-.mq-math-mode .mq-large-operator big,
-.mq-math-mode .mq-large-operator .mq-to {
- display: block;
-}
-.mq-math-mode .mq-large-operator .mq-from,
-.mq-math-mode .mq-large-operator .mq-to {
- font-size: 80%;
-}
-.mq-math-mode .mq-large-operator .mq-from {
- float: right;
- /* take out of normal flow to manipulate baseline */
- width: 100%;
-}
-.mq-math-mode,
-.mq-math-mode .mq-editable-field {
- cursor: text;
- font-family: Symbola, 'Times New Roman', serif;
-}
-.mq-math-mode .mq-overarc {
- border-top: 1px solid black;
- -webkit-border-top-right-radius: 50% 0.3em;
- -moz-border-radius-topright: 50% 0.3em;
- border-top-right-radius: 50% 0.3em;
- -webkit-border-top-left-radius: 50% 0.3em;
- -moz-border-radius-topleft: 50% 0.3em;
- border-top-left-radius: 50% 0.3em;
- margin-top: 1px;
- padding-top: 0.15em;
-}
-.mq-math-mode .mq-overarrow {
- min-width: 0.5em;
- border-top: 1px solid black;
- margin-top: 1px;
- padding-top: 0.2em;
- text-align: center;
- position: relative;
-}
-.mq-math-mode .mq-overarrow:after {
- position: absolute;
- right: -0.1em;
- top: -0.48em;
- font-size: 0.5em;
- content: '\27A4';
-}
-.mq-math-mode .mq-overarrow.mq-arrow-left:after {
- content: '';
- display: none;
-}
-.mq-math-mode .mq-overarrow.mq-arrow-left:before,
-.mq-math-mode .mq-overarrow.mq-arrow-leftright:before {
- position: absolute;
- top: -0.48em;
- left: -0.1em;
- font-size: 0.5em;
- content: '\27A4';
- -moz-transform: scaleX(-1);
- -o-transform: scaleX(-1);
- -webkit-transform: scaleX(-1);
- transform: scaleX(-1);
- filter: FlipH;
- -ms-filter: 'FlipH';
-}
-.mq-math-mode .mq-selection,
-.mq-editable-field .mq-selection,
-.mq-math-mode .mq-selection .mq-non-leaf,
-.mq-editable-field .mq-selection .mq-non-leaf,
-.mq-math-mode .mq-selection .mq-scaled,
-.mq-editable-field .mq-selection .mq-scaled {
- background: #b4d5fe !important;
-}
-.mq-math-mode .mq-selection.mq-blur,
-.mq-editable-field .mq-selection.mq-blur,
-.mq-math-mode .mq-selection.mq-blur .mq-non-leaf,
-.mq-editable-field .mq-selection.mq-blur .mq-non-leaf,
-.mq-math-mode .mq-selection.mq-blur .mq-scaled,
-.mq-editable-field .mq-selection.mq-blur .mq-scaled {
- background: #d4d4d4 !important;
- color: black;
- border-color: black;
-}
-html body .mq-math-mode .mq-selection .mq-nthroot-container *,
-html body .mq-editable-field .mq-selection .mq-nthroot-container * {
- background: transparent !important;
-}
-.mq-editable-field .mq-textarea,
-.mq-math-mode .mq-textarea {
- position: relative;
- -webkit-user-select: text;
- -moz-user-select: text;
- user-select: text;
-}
-.mq-editable-field .mq-textarea *,
-.mq-math-mode .mq-textarea * {
- -webkit-user-select: text;
- -moz-user-select: text;
- user-select: text;
- position: absolute;
- clip: rect(1em 1em 1em 1em);
- -webkit-transform: scale(0);
- -moz-transform: scale(0);
- -ms-transform: scale(0);
- -o-transform: scale(0);
- transform: scale(0);
- resize: none;
- width: 1px;
- height: 1px;
- box-sizing: content-box;
-}
-
-
-.MafsView {
- display: block;
- background: var(--mafs-bg);
- overflow: hidden;
- -webkit-user-select: none;
- user-select: none;
- font-family: inherit;
- font-variant-numeric: tabular-nums;
- touch-action: none;
- outline: 0;
-
- --mafs-bg: black;
- --mafs-fg: white;
-
- --mafs-origin-color: var(--mafs-fg);
- --mafs-line-color: #555;
- --mafs-line-stroke-dash-style: 4, 3;
- --grid-line-subdivision-color: #222;
-
- --mafs-red: #f11d0e;
- --mafs-orange: #f14e0e;
- --mafs-yellow: #ffe44a;
- --mafs-green: #15e272;
- --mafs-blue: #58a6ff;
- --mafs-indigo: #7c58ff;
- --mafs-violet: #ae58ff;
- --mafs-pink: #ee00ab;
-}
-
-.MafsView text {
- fill: var(--mafs-fg);
- cursor: default;
-}
-
-.MafsView path {
- stroke: var(--mafs-fg);
-}
-
-.MafsView:focus-visible {
- border-radius: 5px;
- outline: 3px solid #58a6ff;
-}
-
-@supports not selector(:focus-visible) {
- .MafsView:focus {
- border-radius: 5px;
- outline: 3px solid #58a6ff;
- }
-}
-
-.mafs-shadow {
- paint-order: stroke;
- stroke-width: 3px;
- stroke: var(--mafs-bg);
- stroke-opacity: 0.75;
- stroke-linejoin: round;
-}
-
-.mafs-movable-point {
- cursor: grab;
- touch-action: none;
-}
-
-.mafs-movable-point-dragging {
- cursor: grabbing;
-}
-
-.mafs-movable-point:focus {
- outline: 0;
-}
-
-.mafs-movable-point * {
- fill: none;
- stroke: none;
-}
-
-.mafs-movable-point-hitbox {
- fill: transparent;
-}
-
-.mafs-movable-point-focus {
- stroke: var(--movable-point-color);
- stroke-width: 2;
- stroke-opacity: 0;
- fill: none;
- transition: stroke-opacity 0.2s ease;
-}
-
-.mafs-movable-point-ring {
- fill: var(--movable-point-color);
- fill-opacity: 0.25;
- stroke: none;
- transition: r 0.2s ease;
-}
-
-.mafs-movable-point-point {
- fill: var(--movable-point-color);
- transition: r 0.2s ease;
-}
-
-/* Hover */
-
-.mafs-movable-point:hover .mafs-movable-point-point,
-.mafs-movable-point:focus-visible .mafs-movable-point-point {
- r: calc(var(--movable-point-ring-size) - 2px);
-}
-
-.mafs-movable-point:hover .mafs-movable-point-ring,
-.mafs-movable-point:focus-visible .mafs-movable-point-ring {
- r: calc(var(--movable-point-ring-size) + 3px);
-}
-
-.mafs-movable-point.mafs-movable-point-dragging .mafs-movable-point-ring {
- r: var(--movable-point-ring-size);
-}
-
-/* Focus */
-
-.mafs-movable-point:focus-visible .mafs-movable-point-focus {
- stroke-opacity: 1;
-}
-
-/**
- * Overrides of mafs theme-- we will want to move this into JS land
- * to take advantage of WB tokens
- */
-.MafsView {
- --mafs-bg: transparent;
- --mafs-fg: rgb(33, 36, 44); /* WB color.offBlack */
-
- /* Grid lines */
- --mafs-line-color: rgba(33, 36, 44, 0.16); /* WB color.offBlack16*/
-
- /* Axis lines */
- --mafs-axis-stroke-width: 2px;
-
- --mafs-blue: #1865f2; /* WB color.blue */
- --mafs-red: #d92916; /* WB color.red */
- --mafs-green: #00a60e; /* WB color.green */
- --mafs-violet: #9059ff; /* WB color.purple */
- --mafs-yellow: #ffb100; /* WB color.gold */
-
- /* overridden on a per-point basis */
- --movable-point-color: var(--mafs-blue);
- --movable-point-center-radius: 6px;
- --movable-point-ring-radius: calc(2px + var(--movable-point-center-radius));
- --movable-point-halo-radius: calc(3px + var(--movable-point-ring-radius));
- --movable-point-hover-expansion: 2px;
- --movable-point-focus-ring-offset: 2px;
-
- --movable-line-stroke-color: var(--mafs-blue);
- --movable-line-stroke-weight: 2px;
- --movable-line-stroke-weight-active: 4px;
-}
-
-.MafsView > svg {
- /* Chrome/Safari bugfix for LEMS-1906 */
- display: block;
-}
-
-.MafsView .movable-line:hover,
-.movable-dragging {
- --movable-line-stroke-weight: var(--movable-line-stroke-weight-active);
-}
-
-.MafsView .movable-line:focus,
-.movable-polygon:focus {
- outline: none;
-}
-
-.MafsView
- .movable-line
- :is(.movable-line-focus-outline, .movable-line-focus-outline-gap) {
- stroke: transparent;
-}
-
-.MafsView .movable-line:focus-visible .movable-line-focus-outline {
- stroke: var(--mafs-blue);
- stroke-width: 10px;
-}
-
-.MafsView .movable-line:focus-visible .movable-line-focus-outline-gap {
- stroke: white;
- stroke-width: 6px;
-}
-
-.MafsView .movable-point {
- cursor: grab;
- touch-action: none;
- outline: none;
-}
-
-.MafsView .movable-point.movable-point--dragging {
- cursor: grabbing;
-}
-
-.MafsView .movable-point.movable-point--dragging .movable-point-halo {
- opacity: 0;
-}
-
-.MafsView .movable-point-hitbox {
- fill: transparent;
-}
-
-.MafsView
- :is(
- .movable-point-center,
- .movable-point-ring,
- .movable-point-halo,
- .movable-point-focus-outline
- ) {
- transition:
- r 0.15s ease-out,
- opacity 0.15s ease-out;
-}
-
-.MafsView .movable-point-center {
- r: var(--movable-point-center-radius);
-}
-
-.MafsView .movable-point-halo {
- r: var(--movable-point-halo-radius);
- fill: var(--movable-point-color);
- opacity: 0.25;
- filter: drop-shadow(0 5px 5px #0008);
-}
-
-.MafsView .movable-point-ring {
- r: var(--movable-point-ring-radius);
- fill: #fff;
-}
-
-.MafsView .movable-point:hover .movable-point-center {
- r: calc(
- var(--movable-point-hover-expansion) +
- var(--movable-point-center-radius)
- );
-}
-
-.MafsView .movable-point:hover .movable-point-ring {
- r: calc(
- var(--movable-point-hover-expansion) + var(--movable-point-ring-radius)
- );
-}
-
-.MafsView .movable-point:hover .movable-point-halo {
- r: calc(
- var(--movable-point-hover-expansion) + var(--movable-point-halo-radius)
- );
-}
-
-.MafsView .movable-point .movable-point-focus-outline {
- visibility: hidden;
- r: calc(
- var(--movable-point-halo-radius) +
- var(--movable-point-focus-ring-offset)
- );
- stroke-width: 2px;
- fill: none;
- stroke: var(--mafs-blue);
-}
-
-.MafsView .movable-point:hover .movable-point-focus-outline {
- r: calc(
- var(--movable-point-hover-expansion) + var(--movable-point-halo-radius) +
- var(--movable-point-focus-ring-offset)
- );
-}
-
-.MafsView
- .movable-point:is(:focus-visible, .movable-point--focus)
- .movable-point-focus-outline {
- visibility: visible;
-}
-
-.MafsView .movable-circle {
- cursor: grab;
-}
-
-.MafsView .movable-circle .circle {
- stroke: var(--mafs-blue);
- stroke-width: 2px;
- fill: transparent;
-}
-
-.MafsView .movable-circle:hover .circle {
- fill: var(--mafs-blue);
- fill-opacity: 0.16;
-}
-
-.MafsView .movable-circle.movable-circle--dragging {
- cursor: grabbing;
- outline: none;
-}
-
-.MafsView .movable-circle .focus-ring {
- visibility: hidden; /* overridden when the circle is focused */
- stroke: var(--mafs-blue);
- stroke-width: 2px;
- fill: transparent;
-}
-
-.MafsView .movable-circle:focus {
- outline: none;
-}
-
-.MafsView .movable-circle:focus-visible .focus-ring {
- visibility: visible;
-}
-
-.MafsView .movable-circle .movable-circle-handle {
- fill: var(--mafs-blue); /* WB fadedOffBlack64 */
- stroke: #fff;
- stroke-width: 2px;
-}
-
-.MafsView .movable-circle .movable-circle-handle-dot {
- fill: #fff;
- r: 1.25px;
-}
-
-@font-face {
- font-family: Mafs-MJXTEX;
- src: url("fonts/MathJax_Main-Regular.woff")
- format("woff");
-}
-
-.MafsView pattern g {
- stroke: #909296;
-}
-.axis-tick-labels {
- font-size: 14px;
- font-family: "Mafs-MJXTEX";
- line-height: 1.5em;
- /* Prevent labels from being selected, and from interfering with click and
- * drag interactions on the graph. */
- user-select: none;
- pointer-events: none;
- /* Prevent labels from covering up interaction points. */
- z-index: -1;
-}
-
-.y-axis-tick-labels {
- width: 1.75em;
- display: flex;
- flex-flow: column;
- transform: translateX(calc(-100% - 0.5em));
- position: absolute;
- text-align: right;
-}
-.y-axis-right-of-grid {
- transform: translateX(calc(50% + 0.5em));
-}
-.x-axis-tick-labels {
- display: flex;
- flex-flow: row;
- flex-direction: row-reverse;
- position: absolute;
- transform: translateY(calc(50% - 0.25em));
-}
-.x-axis-top-of-grid {
- transform: translateY(calc(-100% + 0.25em));
-}
-
-.x-axis-tick-labels span,
-.y-axis-tick-labels span {
- /* The negative sign is rendered as a pseudo-element to
- ensure that we are positioning all labels correctly */
- &::before {
- content: attr(data-content);
- position: absolute;
- /* We're translating the symbol up by 1px to ensure it's legible around gridlines. */
- transform: translate(-100%, -1px);
- font-weight: 600; /* This helps get the symbol to render crisply */
- }
-}
-
-.y-axis-tick-labels span {
- display: inline-block;
- height: var(--y-axis-label-height, 20px);
-}
-
-.x-axis-tick-labels span {
- text-align: center;
- width: var(--x-axis-label-width, 20px);
-}
diff --git a/kolibri/plugins/perseus_viewer/frontend/dist/math-input.css b/kolibri/plugins/perseus_viewer/frontend/dist/math-input.css
deleted file mode 100644
index 831d07bf996..00000000000
--- a/kolibri/plugins/perseus_viewer/frontend/dist/math-input.css
+++ /dev/null
@@ -1,620 +0,0 @@
-.keypad-input {
- outline: none !important;
-}
-.keypad-input .mq-editable-field .mq-root-block {
- overflow-x: auto;
-}
-.keypad-input .mq-editable-field .mq-cursor:not(:only-child),
-.keypad-input .mq-editable-field .mq-root-block.mq-hasCursor > .mq-cursor:only-child {
- /* HACK(charlie): Magic numbers to properly size and position the vertical
- cursor, which is visible whenever the cursor is not alone in its parent,
- with the exception that it's also visible when the entire input is
- empty. */
- height: 20px !important;
- width: 2px;
- margin-top: -5px !important;
- vertical-align: middle !important;
- border-radius: 1px !important;
-}
-.keypad-input .mq-editable-field .mq-cursor {
- border-left: 2px solid #1865f2 !important;
- margin-left: -1px !important;
- margin-right: -1px !important;
- opacity: 1 !important;
- transition: opacity 300ms ease !important;
- visibility: visible !important;
-}
-.keypad-input .mq-editable-field .mq-cursor.mq-blink {
- opacity: 0 !important;
- visibility: visible !important;
-}
-.keypad-input .mq-editable-field .mq-non-leaf .mq-cursor:only-child {
- border: 2px solid !important;
- border-color: #1865f2 !important;
- border-radius: 1px;
- opacity: 1 !important;
- padding: 0 4px 0 4px;
- transition: border-color 300ms ease !important;
-}
-.keypad-input .mq-editable-field .mq-non-leaf .mq-cursor:only-child.mq-blink {
- border-color: #1865f2 !important;
- opacity: 1 !important;
-}
-.keypad-input .mq-empty {
- background: transparent !important;
-}
-.keypad-input .mq-empty:not(.mq-root-block):after,
-.keypad-input .mq-hasCursor:empty:not(.mq-root-block):after {
- border: 2px solid rgba(33, 36, 44, 0.16);
- border-radius: 1px;
- color: transparent;
- display: inline-block;
- margin-left: -1px;
- margin-right: -1px;
- padding: 0 4px 0 4px;
- visibility: visible !important;
-}
-.keypad-input .mq-selection .mq-empty:not(.mq-root-block):after {
- border-color: white;
-}
-.keypad-input .mq-hasCursor:empty:not(.mq-root-block):after {
- content: "c";
-}
-.keypad-input .mq-math-mode .mq-selection .mq-non-leaf,
-.keypad-input .mq-editable-field .mq-selection .mq-non-leaf {
- background: #1865f2 !important;
- border-color: white !important;
- color: white !important;
-}
-.keypad-input .mq-math-mode .mq-selection .mq-scaled,
-.keypad-input .mq-editable-field .mq-selection .mq-scaled {
- background: transparent !important;
- border-color: transparent !important;
- color: white !important;
-}
-.keypad-input .mq-selection {
- background: #1865f2 !important;
- border-color: white !important;
- color: white !important;
- display: inline-block !important;
-}
-/**
- * @license
- * MathQuill v0.10.1, by Han, Jeanine, and Mary
- * http://mathquill.com | maintainers@mathquill.com
- *
- * This Source Code Form is subject to the terms of the
- * Mozilla Public License, v. 2.0. If a copy of the MPL
- * was not distributed with this file, You can obtain
- * one at http://mozilla.org/MPL/2.0/.
- */
-
-.mq-aria-alert {
- position: absolute;
- left: -1000px;
- top: -1000px;
- width: 0px;
- height: 0px;
- text-align: left;
- overflow: hidden;
-}
-.mq-mathspeak {
- position: absolute;
- left: -1000px;
- top: -1000px;
- width: 0px;
- height: 0px;
- text-align: left;
- overflow: hidden;
-}
-@font-face {
- font-family: Symbola;
- src: local('Symbola Regular'), local('Symbola'), url(fonts/Symbola.woff) format('woff');
-}
-.mq-editable-field {
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-editable-field .mq-cursor {
- border-left: 1px solid currentColor;
- margin-left: -1px;
- position: relative;
- z-index: 1;
- padding: 0;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-editable-field .mq-cursor.mq-blink {
- visibility: hidden;
-}
-.mq-editable-field,
-.mq-math-mode .mq-editable-field {
- border: 1px solid gray;
-}
-.mq-editable-field.mq-focused,
-.mq-math-mode .mq-editable-field.mq-focused {
- -webkit-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- -moz-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
- border-color: #709ac0;
-}
-.mq-math-mode .mq-editable-field {
- margin: 1px;
-}
-.mq-editable-field .mq-latex-command-input {
- color: inherit;
- font-family: 'Courier New', monospace;
- border: 1px solid gray;
- padding-right: 1px;
- margin-right: 1px;
- margin-left: 2px;
-}
-.mq-editable-field .mq-latex-command-input.mq-empty {
- background: transparent;
-}
-.mq-editable-field .mq-latex-command-input.mq-hasCursor {
- border-color: ActiveBorder;
-}
-.mq-editable-field.mq-empty:after,
-.mq-editable-field.mq-text-mode:after,
-.mq-math-mode .mq-empty:after {
- visibility: hidden;
- content: 'c';
-}
-.mq-editable-field .mq-cursor:only-child:after,
-.mq-editable-field .mq-textarea + .mq-cursor:last-child:after {
- visibility: hidden;
- content: 'c';
-}
-.mq-editable-field .mq-text-mode .mq-cursor:only-child:after {
- content: '';
-}
-.mq-editable-field.mq-text-mode {
- overflow-x: auto;
- overflow-y: hidden;
-}
-.mq-root-block,
-.mq-math-mode .mq-root-block {
- display: -moz-inline-box;
- display: inline-block;
- width: 100%;
- padding: 2px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- white-space: nowrap;
- overflow: hidden;
- vertical-align: middle;
-}
-.mq-root-block .mq-digit,
-.mq-math-mode .mq-root-block .mq-digit {
- margin-left: 0.009em;
- margin-right: 0.009em;
-}
-.mq-root-block .mq-group-start,
-.mq-math-mode .mq-root-block .mq-group-start {
- margin-left: 0.11em;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-other,
-.mq-math-mode .mq-root-block .mq-group-other {
- margin-left: -0.01em;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-leading-1,
-.mq-math-mode .mq-root-block .mq-group-leading-1,
-.mq-root-block .mq-group-leading-2,
-.mq-math-mode .mq-root-block .mq-group-leading-2 {
- margin-left: 0;
- margin-right: -0.01em;
-}
-.mq-root-block .mq-group-leading-3,
-.mq-math-mode .mq-root-block .mq-group-leading-3 {
- margin-left: 0.036em;
- margin-right: -0.01em;
-}
-.mq-root-block.mq-suppress-grouping .mq-group-start,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-start,
-.mq-root-block.mq-suppress-grouping .mq-group-other,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-other,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-1,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-1,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-2,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-2,
-.mq-root-block.mq-suppress-grouping .mq-group-leading-3,
-.mq-math-mode .mq-root-block.mq-suppress-grouping .mq-group-leading-3 {
- margin-left: 0.009em;
- margin-right: 0.009em;
-}
-.mq-math-mode {
- font-variant: normal;
- font-weight: normal;
- font-style: normal;
- font-size: 115%;
- line-height: 1;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode .mq-non-leaf,
-.mq-math-mode .mq-scaled {
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode var,
-.mq-math-mode .mq-text-mode,
-.mq-math-mode .mq-nonSymbola {
- font-family: 'Times New Roman', Symbola, serif;
- line-height: 0.9;
-}
-.mq-math-mode svg {
- fill: currentColor;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-.mq-math-mode * {
- font-size: inherit;
- line-height: inherit;
- margin: 0;
- padding: 0;
- border-color: black;
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
- box-sizing: border-box;
-}
-.mq-math-mode .mq-empty {
- background: rgba(0, 0, 0, 0.2);
-}
-.mq-math-mode .mq-empty.mq-root-block {
- background: transparent;
-}
-.mq-math-mode .mq-empty.mq-quiet-delimiter {
- background: transparent;
-}
-.mq-math-mode.mq-empty {
- background: transparent;
-}
-.mq-math-mode .mq-text-mode {
- display: inline-block;
- white-space: pre;
-}
-.mq-math-mode .mq-text-mode.mq-hasCursor {
- box-shadow: inset darkgray 0 0.1em 0.2em;
- padding: 0 0.1em;
- margin: 0 -0.1em;
- min-width: 1ex;
-}
-.mq-math-mode .mq-font {
- font: 1em 'Times New Roman', Symbola, serif;
-}
-.mq-math-mode .mq-font * {
- font-family: inherit;
- font-style: inherit;
-}
-.mq-math-mode b,
-.mq-math-mode b.mq-font {
- font-weight: bolder;
-}
-.mq-math-mode var,
-.mq-math-mode i,
-.mq-math-mode i.mq-font {
- font-style: italic;
-}
-.mq-math-mode var.mq-f {
- margin-right: 0.2em;
- margin-left: 0.1em;
-}
-.mq-math-mode .mq-roman var.mq-f {
- margin: 0;
-}
-.mq-math-mode big {
- font-size: 200%;
-}
-.mq-math-mode .mq-int > big {
- display: inline-block;
- -webkit-transform: scaleX(0.7);
- -moz-transform: scaleX(0.7);
- -ms-transform: scaleX(0.7);
- -o-transform: scaleX(0.7);
- transform: scaleX(0.7);
- vertical-align: -0.16em;
-}
-.mq-math-mode .mq-int > .mq-supsub {
- font-size: 80%;
- vertical-align: -1.1em;
- padding-right: 0.2em;
-}
-.mq-math-mode .mq-int > .mq-supsub > .mq-sup > .mq-sup-inner {
- vertical-align: 1.3em;
-}
-.mq-math-mode .mq-int > .mq-supsub > .mq-sub {
- margin-left: -0.35em;
-}
-.mq-math-mode .mq-roman {
- font-style: normal;
-}
-.mq-math-mode .mq-sans-serif {
- font-family: sans-serif, Symbola, serif;
-}
-.mq-math-mode .mq-monospace {
- font-family: monospace, Symbola, serif;
-}
-.mq-math-mode .mq-overline {
- border-top: 1px solid;
- margin-top: 1px;
-}
-.mq-math-mode .mq-underline {
- border-bottom: 1px solid;
- margin-bottom: 1px;
-}
-.mq-math-mode .mq-binary-operator {
- padding: 0 0.2em;
- display: -moz-inline-box;
- display: inline-block;
-}
-.mq-math-mode .mq-supsub {
- text-align: left;
- font-size: 90%;
- vertical-align: -0.5em;
-}
-.mq-math-mode .mq-supsub.mq-sup-only {
- vertical-align: 0.5em;
-}
-.mq-math-mode .mq-supsub.mq-sup-only > .mq-sup {
- display: inline-block;
- vertical-align: text-bottom;
-}
-.mq-math-mode .mq-supsub .mq-sup {
- display: block;
-}
-.mq-math-mode .mq-supsub .mq-sub {
- display: block;
- float: left;
-}
-.mq-math-mode .mq-supsub .mq-binary-operator {
- padding: 0 0.1em;
-}
-.mq-math-mode .mq-supsub .mq-fraction {
- font-size: 70%;
-}
-.mq-math-mode sup.mq-nthroot {
- font-size: 80%;
- vertical-align: 0.8em;
- margin-right: -0.6em;
- margin-left: 0.2em;
- min-width: 0.5em;
-}
-.mq-math-mode .mq-ghost svg {
- opacity: 0.2;
-}
-.mq-math-mode .mq-bracket-middle {
- margin-top: 0.1em;
- margin-bottom: 0.1em;
-}
-.mq-math-mode .mq-bracket-l,
-.mq-math-mode .mq-bracket-r {
- position: absolute;
- top: 0;
- bottom: 2px;
-}
-.mq-math-mode .mq-bracket-l {
- left: 0;
-}
-.mq-math-mode .mq-bracket-r {
- right: 0;
-}
-.mq-math-mode .mq-bracket-container {
- position: relative;
-}
-.mq-math-mode .mq-array {
- vertical-align: middle;
- text-align: center;
-}
-.mq-math-mode .mq-array > span {
- display: block;
-}
-.mq-math-mode .mq-operator-name {
- font-family: Symbola, 'Times New Roman', serif;
- line-height: 0.9;
- font-style: normal;
-}
-.mq-math-mode var.mq-operator-name.mq-first {
- padding-left: 0.2em;
-}
-.mq-math-mode var.mq-operator-name.mq-last,
-.mq-math-mode .mq-supsub.mq-after-operator-name {
- padding-right: 0.2em;
-}
-.mq-math-mode .mq-fraction {
- font-size: 90%;
- text-align: center;
- vertical-align: -0.4em;
- padding: 0 0.2em;
-}
-.mq-math-mode .mq-fraction,
-.mq-math-mode .mq-large-operator,
-.mq-math-mode x:-moz-any-link {
- display: -moz-groupbox;
-}
-.mq-math-mode .mq-fraction,
-.mq-math-mode .mq-large-operator,
-.mq-math-mode x:-moz-any-link,
-.mq-math-mode x:default {
- display: inline-block;
-}
-.mq-math-mode .mq-numerator,
-.mq-math-mode .mq-denominator,
-.mq-math-mode .mq-dot-recurring {
- display: block;
-}
-.mq-math-mode .mq-numerator {
- padding: 0 0.1em;
-}
-.mq-math-mode .mq-denominator {
- border-top: 1px solid;
- float: right;
- width: 100%;
- padding: 0.1em;
-}
-.mq-math-mode .mq-dot-recurring {
- text-align: center;
- height: 0.3em;
-}
-.mq-math-mode .mq-sqrt-prefix {
- position: absolute;
- top: 1px;
- bottom: 0.15em;
- width: 0.95em;
-}
-.mq-math-mode .mq-sqrt-container {
- position: relative;
-}
-.mq-math-mode .mq-sqrt-stem {
- border-top: 1px solid;
- margin-top: 1px;
- margin-left: 0.9em;
- padding-left: 0.15em;
- padding-right: 0.2em;
- margin-right: 0.1em;
- padding-top: 1px;
-}
-.mq-math-mode .mq-diacritic-above {
- display: block;
- text-align: center;
- line-height: 0.4em;
-}
-.mq-math-mode .mq-diacritic-stem {
- display: block;
- text-align: center;
-}
-.mq-math-mode .mq-hat-prefix {
- display: block;
- text-align: center;
- line-height: 0.95em;
- margin-bottom: -0.7em;
- transform: scaleX(1.5);
- -moz-transform: scaleX(1.5);
- -o-transform: scaleX(1.5);
- -webkit-transform: scaleX(1.5);
-}
-.mq-math-mode .mq-hat-stem {
- display: block;
-}
-.mq-math-mode .mq-large-operator {
- vertical-align: -0.2em;
- padding: 0.2em;
- text-align: center;
-}
-.mq-math-mode .mq-large-operator .mq-from,
-.mq-math-mode .mq-large-operator big,
-.mq-math-mode .mq-large-operator .mq-to {
- display: block;
-}
-.mq-math-mode .mq-large-operator .mq-from,
-.mq-math-mode .mq-large-operator .mq-to {
- font-size: 80%;
-}
-.mq-math-mode .mq-large-operator .mq-from {
- float: right;
- /* take out of normal flow to manipulate baseline */
- width: 100%;
-}
-.mq-math-mode,
-.mq-math-mode .mq-editable-field {
- cursor: text;
- font-family: Symbola, 'Times New Roman', serif;
-}
-.mq-math-mode .mq-overarc {
- border-top: 1px solid black;
- -webkit-border-top-right-radius: 50% 0.3em;
- -moz-border-radius-topright: 50% 0.3em;
- border-top-right-radius: 50% 0.3em;
- -webkit-border-top-left-radius: 50% 0.3em;
- -moz-border-radius-topleft: 50% 0.3em;
- border-top-left-radius: 50% 0.3em;
- margin-top: 1px;
- padding-top: 0.15em;
-}
-.mq-math-mode .mq-overarrow {
- min-width: 0.5em;
- border-top: 1px solid black;
- margin-top: 1px;
- padding-top: 0.2em;
- text-align: center;
- position: relative;
-}
-.mq-math-mode .mq-overarrow:after {
- position: absolute;
- right: -0.1em;
- top: -0.48em;
- font-size: 0.5em;
- content: '\27A4';
-}
-.mq-math-mode .mq-overarrow.mq-arrow-left:after {
- content: '';
- display: none;
-}
-.mq-math-mode .mq-overarrow.mq-arrow-left:before,
-.mq-math-mode .mq-overarrow.mq-arrow-leftright:before {
- position: absolute;
- top: -0.48em;
- left: -0.1em;
- font-size: 0.5em;
- content: '\27A4';
- -moz-transform: scaleX(-1);
- -o-transform: scaleX(-1);
- -webkit-transform: scaleX(-1);
- transform: scaleX(-1);
- filter: FlipH;
- -ms-filter: 'FlipH';
-}
-.mq-math-mode .mq-selection,
-.mq-editable-field .mq-selection,
-.mq-math-mode .mq-selection .mq-non-leaf,
-.mq-editable-field .mq-selection .mq-non-leaf,
-.mq-math-mode .mq-selection .mq-scaled,
-.mq-editable-field .mq-selection .mq-scaled {
- background: #b4d5fe !important;
-}
-.mq-math-mode .mq-selection.mq-blur,
-.mq-editable-field .mq-selection.mq-blur,
-.mq-math-mode .mq-selection.mq-blur .mq-non-leaf,
-.mq-editable-field .mq-selection.mq-blur .mq-non-leaf,
-.mq-math-mode .mq-selection.mq-blur .mq-scaled,
-.mq-editable-field .mq-selection.mq-blur .mq-scaled {
- background: #d4d4d4 !important;
- color: black;
- border-color: black;
-}
-html body .mq-math-mode .mq-selection .mq-nthroot-container *,
-html body .mq-editable-field .mq-selection .mq-nthroot-container * {
- background: transparent !important;
-}
-.mq-editable-field .mq-textarea,
-.mq-math-mode .mq-textarea {
- position: relative;
- -webkit-user-select: text;
- -moz-user-select: text;
- user-select: text;
-}
-.mq-editable-field .mq-textarea *,
-.mq-math-mode .mq-textarea * {
- -webkit-user-select: text;
- -moz-user-select: text;
- user-select: text;
- position: absolute;
- clip: rect(1em 1em 1em 1em);
- -webkit-transform: scale(0);
- -moz-transform: scale(0);
- -ms-transform: scale(0);
- -o-transform: scale(0);
- transform: scale(0);
- resize: none;
- width: 1px;
- height: 1px;
- box-sizing: content-box;
-}
-
diff --git a/kolibri/plugins/perseus_viewer/frontend/numeralNormalization.js b/kolibri/plugins/perseus_viewer/frontend/numeralNormalization.js
new file mode 100644
index 00000000000..08ee211c1ad
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/numeralNormalization.js
@@ -0,0 +1,151 @@
+/**
+ * Normalizes non-Western Arabic numerals to Western Arabic (ASCII 0-9).
+ *
+ * Many writing systems have their own digit characters. Perseus' scoring
+ * engine uses parseFloat/parseInt which only understand ASCII digits.
+ * This module transliterates any recognized non-Western digits so that
+ * users can type answers using their native keyboard/numeral system.
+ *
+ * Uses Unicode property escapes (\p{Nd}) to match decimal digits from
+ * any script — no hardcoded character ranges needed. This automatically
+ * covers current and future Unicode numeral systems.
+ */
+
+// Matches any Unicode decimal digit that is NOT ASCII 0-9.
+// \p{Nd} = Unicode "Decimal_Digit_Number" category (all scripts).
+// [0-9] is excluded so we only process non-Western digits.
+const nonWesternDigitRegex = /(?![0-9])\p{Nd}/gu;
+
+// Single-character test for any decimal digit (used in the base-finding loop).
+const singleNdRegex = /\p{Nd}/u;
+
+/**
+ * Replace any non-Western digit character with its ASCII equivalent.
+ *
+ * Unicode guarantees that decimal digits 0-9 are contiguous in every
+ * script. We find the block's "zero" by walking backwards (at most 9
+ * steps), then subtract to get the digit value.
+ */
+function normalizeNumerals(str) {
+ if (typeof str !== 'string') {
+ return str;
+ }
+ return str.replace(nonWesternDigitRegex, char => {
+ const code = char.codePointAt(0);
+ // Walk backwards to find the first character in this digit block
+ // (i.e., the script's "zero"). At most 9 steps.
+ let base = code;
+ while (base > 0 && singleNdRegex.test(String.fromCodePoint(base - 1))) {
+ base--;
+ }
+ return String(code - base);
+ });
+}
+
+/**
+ * Recursively apply a string transformation to all string values in
+ * a nested object/array structure. Non-string leaves pass through unchanged.
+ */
+function deepMapStrings(input, fn) {
+ if (typeof input === 'string') {
+ return fn(input);
+ }
+ if (Array.isArray(input)) {
+ return input.map(item => deepMapStrings(item, fn));
+ }
+ if (input !== null && typeof input === 'object') {
+ const result = {};
+ for (const key in input) {
+ result[key] = deepMapStrings(input[key], fn);
+ }
+ return result;
+ }
+ return input;
+}
+
+/**
+ * Recursively normalize all string values in a user input object.
+ * Handles the nested structures returned by getUserInput(), e.g.:
+ * { "numeric-input 1": { currentValue: "٤٢" } }
+ * { "expression 1": "٢x+٣" }
+ * { "radio 1": { selectedChoiceIds: ["radio-choice-1"] } }
+ *
+ * Non-string values (numbers, booleans, arrays of non-strings) pass through
+ * unchanged. Choice IDs like "radio-choice-1" contain only ASCII so they
+ * are unaffected by normalization.
+ */
+function normalizeUserInput(input) {
+ return deepMapStrings(input, normalizeNumerals);
+}
+
+// Cache for getLocalizedDigits — keyed by locale string.
+const _digitCache = {};
+
+/**
+ * Get the localized digits 0-9 for a locale using Intl.NumberFormat.
+ * Returns null if the locale's digits are identical to ASCII 0-9
+ * (meaning no character remapping is needed for display or input).
+ * Otherwise returns an array of 10 strings representing digits 0-9.
+ * Results are cached per locale.
+ */
+function getLocalizedDigits(locale) {
+ if (!locale) {
+ return null;
+ }
+ if (locale in _digitCache) {
+ return _digitCache[locale];
+ }
+ try {
+ const formatter = new Intl.NumberFormat(locale, { useGrouping: false });
+ const digits = [];
+ for (let i = 0; i < 10; i++) {
+ digits.push(formatter.format(i));
+ }
+ // If all digits match ASCII, no remapping is needed
+ const result = digits.every((d, i) => d === String(i)) ? null : digits;
+ _digitCache[locale] = result;
+ return result;
+ } catch (e) {
+ _digitCache[locale] = null;
+ return null;
+ }
+}
+
+/**
+ * Convert ASCII digits in a string to localized digits for a given locale.
+ * The reverse of normalizeNumerals: "42" → "٤٢" for Arabic.
+ * Returns the string unchanged if the locale's digits match ASCII.
+ */
+function localizeNumerals(str, locale) {
+ if (typeof str !== 'string') {
+ return str;
+ }
+ const digits = getLocalizedDigits(locale);
+ if (!digits) {
+ return str;
+ }
+ return str.replace(/[0-9]/g, d => digits[Number(d)]);
+}
+
+/**
+ * Recursively localize all string values in a user input object.
+ * The reverse of normalizeUserInput: converts ASCII digits back to
+ * the locale's native numeral system for display in widgets.
+ *
+ * Used when restoring saved answer state so that users see their
+ * answers in their native numeral format, not as ASCII digits.
+ */
+function localizeUserInput(input, locale) {
+ if (!getLocalizedDigits(locale)) {
+ return input;
+ }
+ return deepMapStrings(input, s => localizeNumerals(s, locale));
+}
+
+export {
+ normalizeNumerals,
+ normalizeUserInput,
+ getLocalizedDigits,
+ localizeNumerals,
+ localizeUserInput,
+};
diff --git a/kolibri/plugins/perseus_viewer/frontend/reactRouterShim.js b/kolibri/plugins/perseus_viewer/frontend/reactRouterShim.js
new file mode 100644
index 00000000000..c2868269665
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/reactRouterShim.js
@@ -0,0 +1,24 @@
+// Shim for react-router-dom-v5-compat used by Wonder Blocks components.
+//
+// Wonder Blocks (clickable, button, link, etc.) imports useInRouterContext,
+// useNavigate, and Link from this package. It checks useInRouterContext()
+// first — when false, components fall back to plain tags and standard
+// navigation.
+//
+// The real react-router-dom-v5-compat@6.x re-exports from react-router@6,
+// but Kolibri's Perseus plugin only has react-router@5 which lacks these
+// APIs. Rather than pulling in all of react-router@6, we provide stubs
+// that tell Wonder Blocks "no router context exists."
+var React = require('react');
+
+exports.useInRouterContext = function useInRouterContext() {
+ return false;
+};
+
+exports.useNavigate = function useNavigate() {
+ return function () {};
+};
+
+exports.Link = React.forwardRef(function ShimLink(props, ref) {
+ return React.createElement('a', Object.assign({}, props, { ref: ref }));
+});
diff --git a/kolibri/plugins/perseus_viewer/frontend/translator.js b/kolibri/plugins/perseus_viewer/frontend/translator.js
index 9e1a876c312..3b2f943d8c3 100644
--- a/kolibri/plugins/perseus_viewer/frontend/translator.js
+++ b/kolibri/plugins/perseus_viewer/frontend/translator.js
@@ -1,25 +1,31 @@
import { createTranslator } from 'kolibri/utils/i18n';
export default createTranslator('PerseusInternalMessages', {
+ characterCount:
+ '{used, plural, one {{ used } / { num } Character} other {{ used } / { num } Characters}}',
closeKeypad: 'close math keypad',
openKeypad: 'open math keypad',
+ mathInputBox: 'Math input box',
removeHighlight: 'Remove highlight',
addHighlight: 'Add highlight',
hintPos: 'Hint #{ pos }',
errorRendering: 'Error rendering: { error }',
APPROXIMATED_PI_ERROR:
- 'Your answer is close, but you may have approximated pi. Enter your answer as a multiple of pi, like 12\\\\ \\\\text[pi] or 2/3\\\\ \\\\text[pi]',
+ 'Your answer is close, but you may have approximated pi. Enter your answer as a multiple of pi, like 12 pi or 2/3 pi',
+ EMPTY_RESPONSE_ERROR: 'There are still more parts of this question to answer.',
EXTRA_SYMBOLS_ERROR:
'We could not understand your answer. Please check your answer for extra text or symbols.',
NEEDS_TO_BE_SIMPLFIED_ERROR: 'Your answer is almost correct, but it needs to be simplified.',
MISSING_PERCENT_ERROR:
- 'Your answer is almost correct, but it is missing a \\\\% at the end.',
+ 'Your answer is almost correct, but it is missing a \\\\\\\\% at the end.',
MULTIPLICATION_SIGN_ERROR: {
message:
"I'm a computer. I only understand multiplication if you use an asterisk (*) as the multiplication sign.",
context:
'Feel free to skip translating the first sentence, just make clear the necessity to use the asterisk (*) as the multiplication sign.',
},
+ USER_INPUT_EMPTY: 'Your answer is empty.',
+ USER_INPUT_TOO_LONG: 'Please shorten your response.',
WRONG_CASE_ERROR: {
message: 'Your answer includes use of a variable with the wrong case.',
context: 'Refers to capitalization of the variables.',
@@ -30,6 +36,7 @@ export default createTranslator('PerseusInternalMessages', {
"Refers to variables in algebra, and assumes that the variable name is always just one letter (like 'a' , 'b', etc.) ",
},
invalidSelection: 'Make sure you select something for every row.',
+ INVALID_CHOICE_SELECTION: 'Invalid choice selection',
ERROR_TITLE: 'Oops!',
ERROR_MESSAGE: "Sorry, I don't understand that!",
hints: 'Hints',
@@ -45,7 +52,6 @@ export default createTranslator('PerseusInternalMessages', {
current: 'Current',
correct: 'Correct',
correctSelected: 'Correct (selected)',
- correctCrossedOut: 'Correct (but you crossed it out)',
incorrect: 'Incorrect',
incorrectSelected: 'Incorrect (selected)',
hideExplanation: 'Hide explanation',
@@ -58,10 +64,10 @@ export default createTranslator('PerseusInternalMessages', {
simplifiedProperExample: 'a *simplified proper* fraction, like $3/5$',
improperExample: 'an *improper* fraction, like $10/7$ or $14/8$',
simplifiedImproperExample: 'a *simplified improper* fraction, like $7/4$',
- mixedExample: 'a mixed number, like $1\\\\ 3/4$',
+ mixedExample: 'a mixed number, like $1\\\\\\\\ 3/4$',
decimalExample: 'an *exact* decimal, like $0.75$',
- percentExample: 'a percent, like $12.34\\\\%$',
- piExample: 'a multiple of pi, like $12\\\\ \\\\text[pi]$ or $2/3\\\\ \\\\text[pi]$',
+ percentExample: 'a percent, like $12.34\\\\\\\\%$',
+ piExample: 'a multiple of pi, like $12$ pi or $2/3$ pi',
yourAnswer: '**Your answer should be** ',
yourAnswerLabel: 'Your answer:',
addPoints: 'Click to add points',
@@ -87,22 +93,6 @@ export default createTranslator('PerseusInternalMessages', {
circleFilled: 'Make circle filled',
numDivisions: 'Number of divisions:',
divisions: 'Please make sure the number of divisions is in the range { divRangeString }.',
- lineRange: 'lines { lineRange }',
- lineNumber: 'line { lineNumber }',
- symbolPassage:
- 'The symbol { questionSymbol } indicates that question { questionNumber } references this portion of the passage.',
- symbolQuestion:
- ' The symbol { sentenceSymbol } indicates that the following sentence is referenced in a question.',
- lineLabel: {
- message: 'Line',
- context: 'a label next to a reading passage to denote the line number',
- },
- beginningPassage: 'Beginning of reading passage.',
- beginningFootnotes: 'Beginning of reading passage footnotes.',
- endPassage: 'End of reading passage.',
- questionMarker: '[Marker for question { number }]',
- circleMarker: '[Circle marker { number }]',
- sentenceMarker: '[Sentence { number }]',
dragHandles: 'Drag handles to make graph',
tapAddPoints: 'Tap to add points',
false: 'False',
@@ -116,21 +106,16 @@ export default createTranslator('PerseusInternalMessages', {
chooseAllAnswers: 'Choose all answers that apply:',
chooseOneAnswer: 'Choose 1 answer:',
choiceCheckedCorrect: '(Choice { letter }, Checked, Correct)',
- choiceCrossedOutCorrect: '(Choice { letter }, Crossed out, Correct)',
choiceCorrect: '(Choice { letter }, Correct)',
choiceCheckedIncorrect: '(Choice { letter }, Checked, Incorrect)',
- choiceCrossedOutIncorrect: '(Choice { letter }, Crossed out, Incorrect)',
choiceIncorrect: '(Choice { letter }, Incorrect)',
choiceChecked: '(Choice { letter }, Checked)',
- choiceCrossedOut: '(Choice { letter }, Crossed out)',
choice: '(Choice { letter })',
- crossOut: 'Cross out',
- crossOutOption: {
- message: 'Cross out option',
- context:
- 'Tooltip that informs the user that they can cross-out one of the options in the multiple choice type of question.',
+ notSelected: {
+ message: 'not selected',
+ context: 'Screen reader announcement for a choice that is not selected',
},
- crossOutChoice: 'Cross out Choice { letter }',
+ choicesSelected: '{num, plural, one {{ num } choice selected} other {{ num } choices selected}}',
bringBack: {
message: 'Bring back',
context:
@@ -146,6 +131,9 @@ export default createTranslator('PerseusInternalMessages', {
context:
'This is a list of single-character labels that will appear in front of multiple-choice options. For instance, a multiple-choice question with three options would display (A) first option (B) second option (C) third option. There must be spaces between each of the different characters. The characters will show up next to options in the order that they are listed here. Most multiple choice questions have 5 or fewer options.',
},
+ scrollAnswers: 'Scroll Answers',
+ scrollStart: 'Scroll to view start of the content',
+ scrollEnd: 'Scroll to view the end of the content',
rightArrow: 'Right arrow',
dontUnderstandUnits: "I couldn't understand those units.",
checkSigFigs: 'Check your significant figures.',
@@ -156,7 +144,448 @@ export default createTranslator('PerseusInternalMessages', {
videoTranscript: 'See video transcript',
somethingWrong: 'Something went wrong.',
videoWrapper: 'Khan Academy video wrapper',
- mathInputBox: 'Math input box',
+ mathInputTitle: 'mathematics keyboard',
+ mathInputDescription: 'Use keyboard/mouse to interact with math-based input fields',
+ sin: {
+ message: 'sin',
+ context:
+ 'A label for a button that will allow the user to input a sine function (shorthand version).',
+ },
+ cos: {
+ message: 'cos',
+ context:
+ 'A label for a button that will allow the user to input a cosine function (shorthand version).',
+ },
+ tan: {
+ message: 'tan',
+ context:
+ 'A label for a button that will allow the user to input a tangent function (shorthand version).',
+ },
+ simulationLoadFail: 'Sorry, this simulation cannot load.',
+ simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
+ selectAnAnswer: 'Select an answer',
+ addPoint: 'Add Point',
+ removePoint: 'Remove Point',
+ graphKeyboardPrompt: 'Press Shift + Enter to interact with the graph',
+ srInteractiveElements: 'Interactive elements: { elements }',
+ srNoInteractiveElements: 'No interactive elements',
+ closePolygon: {
+ message: 'Close shape',
+ context:
+ 'Button label for the button that closes an incomplete polygon created by the user in the interactive graph widget.',
+ },
+ openPolygon: {
+ message: 'Re-open shape',
+ context:
+ 'Button label for the button that opens a closed polygon created by the user in the interactive graph widget.',
+ },
+ srGraphInstructions: {
+ message:
+ 'Use the Tab key to move through the interactive elements in the graph. When an interactive element has focus, use Control + Shift + Arrows to move it.',
+ context:
+ 'Screen reader-only instructions for using the keyboard to move through the interactive elements in the interactive graph widget.',
+ },
+ srUnlimitedGraphInstructions: {
+ message:
+ 'Press Shift + Enter to interact with the graph. Use the Tab key to move through the interactive elements in the graph and access the graph Action Bar. When an interactive element has focus, use Control + Shift + Arrows to move it or use the Delete key to remove it from the graph. Use the buttons in the Action Bar to add or adjust elements within the graph.',
+ context:
+ "Screen reader-only instructions for using the keyboard to move through the 'unlimited' (addable/deletable by the user) interactive elements in the interactive graph widget.",
+ },
+ srPointAtCoordinates: {
+ message: 'Point { num } at { x } comma { y }.',
+ context:
+ "Aria label for an interactive Point element in the interactive graph widget, including the count for its order in the points (e.g. 'Point 1 at 0 comma 0'). Coordinate (x, y) is written out as 'x comma y'.",
+ },
+ srCircleGraph: {
+ message: 'A circle on a coordinate plane.',
+ context:
+ 'Aria label for the container containing the Circle and its interactive elements in the interactive graph widget.',
+ },
+ srCircleShape: {
+ message: 'Circle. The center point is at { centerX } comma { centerY }.',
+ context: 'Aria label for the interactive Circle element in the interactive graph widget.',
+ },
+ srCircleRadiusPointRight: {
+ message: 'Right radius endpoint at { radiusPointX } comma { radiusPointY }.',
+ context:
+ "Aria label for the interactive Point element that represents the radius endpoint when it's on the right side of the Circle in the interactive graph widget.",
+ },
+ srCircleRadiusPointLeft: {
+ message: 'Left radius endpoint at { radiusPointX } comma { radiusPointY }.',
+ context:
+ "Aria label for the interactive Point element that represents the radius endpoint when it's on the left side of the Circle in the interactive graph widget.",
+ },
+ srCircleRadius: {
+ message: 'Circle radius is { radius }.',
+ context:
+ 'Screen reader description for the radius of the Circle in the interactive graph widget.',
+ },
+ srCircleOuterPoints: {
+ message:
+ 'Points on the circle at { point1X } comma { point1Y }, { point2X } comma { point2Y }, { point3X } comma { point3Y }, { point4X } comma { point4Y }.',
+ context:
+ 'Screen reader description for four key points on the Circle in the interactive graph widget.',
+ },
+ srLinearGraph: {
+ message: 'A line on a coordinate plane.',
+ context:
+ 'Aria label for the container containing the Line and its interactive elements in the interactive graph widget.',
+ },
+ srLinearGraphPoints: {
+ message:
+ 'The line has two points, point 1 at { point1X } comma { point1Y } and point 2 at { point2X } comma { point2Y }.',
+ context:
+ 'Screen reader description for the two points defining the Line in the interactive graph widget.',
+ },
+ srLinearGraphSlopeIncreasing: {
+ message: 'Its slope increases from left to right.',
+ context:
+ 'Screen reader description for the upward slope of the Line in the interactive graph widget.',
+ },
+ srLinearGraphSlopeDecreasing: {
+ message: 'Its slope decreases from left to right.',
+ context:
+ 'Screen reader description for the downward slope of the Line in the interactive graph widget.',
+ },
+ srLinearGraphSlopeHorizontal: {
+ message: 'Its slope is zero.',
+ context:
+ 'Screen reader description for the slope of a horizontal Line in the interactive graph widget.',
+ },
+ srLinearGraphSlopeVertical: {
+ message: 'Its slope is undefined.',
+ context:
+ 'Screen reader description for the slope of a vertical Line in the interactive graph widget.',
+ },
+ srLinearGraphXOnlyIntercept: {
+ message: 'The line crosses the X-axis at { xIntercept } comma 0.',
+ context:
+ 'Screen reader description for the intercept of the Line in the interactive graph widget when it only intersects the X-axis.',
+ },
+ srLinearGraphYOnlyIntercept: {
+ message: 'The line crosses the Y-axis at 0 comma { yIntercept }.',
+ context:
+ 'Screen reader description for the intercept of the Line in the interactive graph widget when it only intersects the Y-axis.',
+ },
+ srLinearGraphBothIntercepts: {
+ message:
+ 'The line crosses the X-axis at { xIntercept } comma 0 and the Y-axis at 0 comma { yIntercept }.',
+ context:
+ 'Screen reader description for the intercepts of the Line in the interactive graph widget when it intersects both the X-axis and Y-axis.',
+ },
+ srLinearGraphOriginIntercept: {
+ message: "The line crosses the X and Y axes at the graph's origin.",
+ context:
+ 'Screen reader description for the intercept of the Line in the interactive graph widget when it intersects both the X-axis and Y-axis at the origin.',
+ },
+ srLinearGrabHandle: {
+ message:
+ 'Line going through point { point1X } comma { point1Y } and point { point2X } comma { point2Y }.',
+ context:
+ 'Aria label for the interactive segment that allows the user to move the whole Line in the interactive graph widget.',
+ },
+ srAngleStartingSide: {
+ message: 'Point 3, starting side at { x } comma { y }.',
+ context:
+ "Aria label for interactive Point 3 of the Angle in the interactive graph widget, explaining it's on the starting side of the Angle.",
+ },
+ srAngleEndingSide: {
+ message: 'Point 2, ending side at { x } comma { y }.',
+ context:
+ "Aria label for interactive Point 2 of the Angle in the interactive graph widget, explaining it's on the ending side of the Angle.",
+ },
+ srAngleVertexWithAngleMeasure: {
+ message: 'Point 1, vertex at { x } comma { y }. Angle { angleMeasure } degrees.',
+ context:
+ "Aria label for interactive Point 1 of the Angle in the interactive graph widget, explaining it's the vertex of the Angle.",
+ },
+ srAngleGraphAriaLabel: {
+ message: 'An angle on a coordinate plane.',
+ context:
+ 'Aria label for the container containing the Angle and its interactive elements in the interactive graph widget.',
+ },
+ srAngleGraphAriaDescription: {
+ message:
+ 'The angle measure is { angleMeasure } degrees with a vertex at { vertexX } comma { vertexY }, a point on the starting side at { startingSideX } comma { startingSideY } and a point on the ending side at { endingSideX } comma { endingSideY }',
+ context:
+ 'Screen reader description for the measure of the Angle in the interactive graph widget.',
+ },
+ srAngleInteractiveElements: {
+ message:
+ 'An angle formed by 3 points. The vertex is at { vertexX } comma { vertexY }. The starting side point is at { startingSideX } comma { startingSideY }. The ending side point is at { endingSideX } comma { endingSideY }.',
+ context:
+ 'Screen reader description of all the elements available to interact with within the Angle graph in the interactive graph widget.',
+ },
+ srSingleSegmentGraphAriaLabel: {
+ message: 'A line segment on a coordinate plane.',
+ context:
+ 'Aria label for the container containing one Line Segment in the interactive graph widget.',
+ },
+ srMultipleSegmentGraphAriaLabel: {
+ message: '{ countOfSegments } line segments on a coordinate plane.',
+ context:
+ 'Aria label for the container containing multiple Line Segments in the interactive graph widget.',
+ },
+ srMultipleSegmentIndividualLabel: {
+ message:
+ 'Segment { indexOfSegment }: Endpoint 1 at { point1X } comma { point1Y }. Endpoint 2 at { point2X } comma { point2Y }.',
+ context:
+ "Screen reader description for one individual Line Segment in the interactive graph widget, including the count for its order in the segments (e.g. 'Segment 1', 'Segment 2', etc.)",
+ },
+ srSingleSegmentLabel: {
+ message:
+ 'Endpoint 1 at { point1X } comma { point1Y }. Endpoint 2 at { point2X } comma { point2Y }.',
+ context:
+ 'Screen reader description for one individual Line Segment in the interactive graph widget.',
+ },
+ srSegmentLength: {
+ message: 'Segment length { length } units.',
+ context:
+ 'Screen reader description for the length of a Line Segment in the interactive graph widget.',
+ },
+ srSingleSegmentGraphEndpointAriaLabel: {
+ message: 'Endpoint { endpointNumber } at { x } comma { y }.',
+ context:
+ 'Screen reader description for the endpoint of a Line Segment in the interactive graph widget when there is only one segment.',
+ },
+ srMultipleSegmentGraphEndpointAriaLabel: {
+ message: 'Endpoint { endpointNumber } on segment { indexOfSegment } at { x } comma { y }.',
+ context:
+ "Screen reader description for the endpoint of a Line Segment in the interactive graph widget when there are multiple segments. Includes the count for the segment's order (e.g. 'Segment 1', 'Segment 2', etc.)",
+ },
+ srSegmentGrabHandle: {
+ message: 'Segment from { point1X } comma { point1Y } to { point2X } comma { point2Y }.',
+ context:
+ 'Aria label for the interactive segment that allows the user to move the whole Line Segment in the interactive graph widget.',
+ },
+ srLinearSystemGraph: {
+ message: 'Two lines on a coordinate plane.',
+ context:
+ 'Aria label for the container containing two lines as part of a Linear System in the interactive graph widget.',
+ },
+ srLinearSystemPoints: {
+ message:
+ 'Line { lineNumber } has two points, point 1 at { point1X } comma { point1Y } and point 2 at { point2X } comma { point2Y }.',
+ context:
+ 'Screen reader description for the points of a line in the Linear System in the interactive graph widget.',
+ },
+ srLinearSystemPoint: {
+ message: 'Point { pointSequence } on line { lineNumber } at { x } comma { y }.',
+ context:
+ 'Screen reader description for a point on a line in the Linear System in the interactive graph widget.',
+ },
+ srLinearSystemGrabHandle: {
+ message:
+ 'Line { lineNumber } going through point { point1X } comma { point1Y } and point { point2X } comma { point2Y }.',
+ context:
+ 'Aria label for the interactive segment that allows the user to move a whole line in the Linear System in the interactive graph widget.',
+ },
+ srLinearSystemIntersection: {
+ message: 'Line 1 and line 2 intersect at point { x } comma { y }.',
+ context:
+ 'Screen reader description for the intersection of two lines in the Linear System in the interactive graph widget.',
+ },
+ srLinearSystemParallel: {
+ message: 'Line 1 and line 2 are parallel.',
+ context:
+ 'Screen reader description when two lines are parallel in the Linear System in the interactive graph widget.',
+ },
+ srRayGraph: {
+ message: 'A ray on a coordinate plane.',
+ context:
+ 'Screen reader description for the container containing a Ray in the interactive graph widget.',
+ },
+ srRayPoints: {
+ message:
+ 'The endpoint is at { point1X } comma { point1Y } and the ray goes through point { point2X } comma { point2Y }.',
+ context: 'Screen reader description for the points of a ray in the interactive graph widget.',
+ },
+ srRayGrabHandle: {
+ message:
+ 'Ray with endpoint { point1X } comma { point1Y } going through point { point2X } comma { point2Y }.',
+ context:
+ 'Aria label for the interactive segment that allows the user to move the whole Ray in the interactive graph widget.',
+ },
+ srRayEndpoint: {
+ message: 'Endpoint at { x } comma { y }.',
+ context:
+ 'Aria label for the initial point of a Ray (the point at which the ray starts) in the interactive graph widget.',
+ },
+ srRayTerminalPoint: {
+ message: 'Through point at { x } comma { y }.',
+ context:
+ 'Aria label for the point that determines the direction of the Ray in the interactive graph widget. The ray passes through this point.',
+ },
+ srQuadraticGraph: {
+ message: 'A parabola on a 4-quadrant coordinate plane.',
+ context:
+ 'Aria label for the container containing a Quadratic function in the interactive graph widget.',
+ },
+ srQuadraticFaceUp: {
+ message: 'The parabola opens upward.',
+ context:
+ 'Screen reader description for the direction of the Quadratic function in the interactive graph widget when it opens upward.',
+ },
+ srQuadraticFaceDown: {
+ message: 'The parabola opens downward.',
+ context:
+ 'Screen reader description for the direction of the Quadratic function in the interactive graph widget when it opens downward.',
+ },
+ srQuadraticGraphVertexOrigin: {
+ message: 'Vertex is at the origin.',
+ context:
+ 'Screen reader description for the Quadratic function in the interactive graph widget when its vertex is at the origin.',
+ },
+ srQuadraticGraphVertexXAxis: {
+ message: 'Vertex is on the X-axis.',
+ context:
+ 'Screen reader description for the Quadratic function in the interactive graph widget when its vertex is on the X-axis.',
+ },
+ srQuadraticGraphVertexYAxis: {
+ message: 'Vertex is on the Y-axis.',
+ context:
+ 'Screen reader description for the Quadratic function in the interactive graph widget when its vertex is on the Y-axis.',
+ },
+ srQuadraticGraphVertexQuadrant: {
+ message: 'Vertex is in quadrant { quadrant }.',
+ context:
+ 'Screen reader description for the Quadratic function in the interactive graph widget when its vertex is in a specific quadrant (quadrant 1, 2, 3, or 4).',
+ },
+ srQuadraticTwoXIntercepts: {
+ message: 'The X-intercepts are at { intercept1 } comma 0 and { intercept2 } comma 0.',
+ context:
+ 'Screen reader description for the X-intercepts of the Quadratic function in the interactive graph widget when there are two X-intercepts.',
+ },
+ srQuadraticOneXIntercept: {
+ message: 'The X-intercept is at { intercept } comma 0.',
+ context:
+ 'Screen reader description for the X-intercept of the Quadratic function in the interactive graph widget when there is only one X-intercept.',
+ },
+ srQuadraticYIntercept: {
+ message: 'The Y-intercept is at 0 comma { intercept }.',
+ context:
+ 'Screen reader description for the Y-intercept of the Quadratic function in the interactive graph widget.',
+ },
+ srQuadraticPointOrigin: {
+ message: 'Point { pointNumber } on parabola at the origin.',
+ context:
+ 'Aria label for an interactive Point on the Quadratic function in the interactive graph widget when the Point is at the origin.',
+ },
+ srQuadraticPointAxis: {
+ message: 'Point { pointNumber } on parabola at { x } comma { y }.',
+ context:
+ 'Aria label for an interactive Point on the Quadratic function in the interactive graph widget when the Point is on the X-axis or Y-axis.',
+ },
+ srQuadraticPointQuadrant: {
+ message: 'Point { pointNumber } on parabola in quadrant { quadrant } at { x } comma { y }.',
+ context:
+ 'Aria label for an interactive Point on the Quadratic function in the interactive graph widget when the Point is in a specific quadrant.',
+ },
+ srQuadraticInteractiveElements: {
+ message:
+ 'Parabola with points at { point1X } comma { point1Y }, { point2X } comma { point2Y }, and { point3X } comma { point3Y }.',
+ context:
+ 'Screen reader description of all the elements available to interact with within the Quadratic function in the interactive graph widget.',
+ },
+ srPolygonGraph: {
+ message: 'A polygon.',
+ context:
+ "Aria label for the container containing a Polygon in the interactive graph widget when it's on a plane/grid without axes.",
+ },
+ srPolygonGraphCoordinatePlane: {
+ message: 'A polygon on a coordinate plane.',
+ context:
+ "Aria label for the container containing a Polygon in the interactive graph widget when it's on a coordinate plane.",
+ },
+ srPolygonGraphPointsNum: {
+ message: 'The polygon has { num } points.',
+ context:
+ 'Screen reader description for the number of points in the Polygon in the interactive graph widget.',
+ },
+ srPolygonGraphPointsOne: {
+ message: 'The polygon has 1 point.',
+ context:
+ 'Screen reader description for the number of points in the Polygon in the interactive graph widget when there is only one point.',
+ },
+ srPolygonElementsNum: {
+ message: 'A polygon with { num } points.',
+ context:
+ 'Screen reader description for the Polygon in the interactive graph widget explaining that it has a certain number of points.',
+ },
+ srPolygonElementsOne: {
+ message: 'A polygon with 1 point.',
+ context:
+ 'Screen reader description for the Polygon in the interactive graph widget explaining that it has one point.',
+ },
+ srPolygonPointAngleApprox: {
+ message: 'Angle approximately equal to { angle } degrees.',
+ context:
+ "Screen reader description for the angle of a point in the Polygon in the interactive graph widget when it's not an exact integer.",
+ },
+ srPolygonPointAngle: {
+ message: 'Angle equal to { angle } degrees.',
+ context:
+ "Screen reader description for the angle of a point in the Polygon in the interactive graph widget when it's an integer.",
+ },
+ srPolygonSideLength: {
+ message: 'A line segment, length equal to { length } units, connects to point { pointNum }.',
+ context:
+ 'Screen reader description for the side of the Polygon in the interactive graph widget when its length is an exact integer.',
+ },
+ srPolygonSideLengthApprox: {
+ message:
+ 'A line segment, length approximately equal to { length } units, connects to point { pointNum }.',
+ context:
+ 'Screen reader description for the side of the Polygon in the interactive graph widget when its length is not an exact integer.',
+ },
+ srUnlimitedPolygonEmpty: {
+ message: 'An empty coordinate plane.',
+ context:
+ 'Screen reader description for the empty container that will eventually contain a Polygon in the interactive graph widget after the user has added points.',
+ },
+ srSinusoidGraph: {
+ message: 'A sinusoid function on a coordinate plane.',
+ context:
+ 'Aria label for the container containing a Sinusoid function in the interactive graph widget.',
+ },
+ srSinusoidRootPoint: {
+ message: 'Midline intersection at { x } comma { y }.',
+ context:
+ 'Aria label for the Point defining the midline intersection of the Sinusoid function in the interactive graph widget.',
+ },
+ srSinusoidMaxPoint: {
+ message: 'Maximum point at { x } comma { y }.',
+ context:
+ 'Aria label for the Point defining the maximum of the Sinusoid function in the interactive graph widget.',
+ },
+ srSinusoidMinPoint: {
+ message: 'Minimum point at { x } comma { y }.',
+ context:
+ 'Aria label for the Point defining the minimum of the Sinusoid function in the interactive graph widget.',
+ },
+ srSinusoidFlatPoint: {
+ message: 'Line through point at { x } comma { y }.',
+ context:
+ 'Aria label for the Point defining the amplitude of the Sinusoid function in the interactive graph widget when the amplitude is 0.',
+ },
+ srSinusoidDescription: {
+ message:
+ 'The graph shows a wave with a minimum value of { minValue } and a maximum value of { maxValue }. The wave completes a full cycle from { cycleStart } to { cycleEnd }.',
+ context: 'Screen reader description of the Sinusoid function in the interactive graph widget.',
+ },
+ srSinusoidInteractiveElements: {
+ message:
+ 'Sinusoid graph with midline intersection point at { point1X } comma { point1Y } and extremum point at { point2X } comma { point2Y }.',
+ context:
+ 'Screen reader description of all the elements available to interact with within the Sinusoid function in the interactive graph widget.',
+ },
+ imageExploreButton: 'Explore image',
+ imageAlternativeTitle: 'Explore image and description',
+ imageDescriptionLabel: 'Description',
+ imageZoomAriaLabel: 'Zoom image.',
+ imageResetZoomAriaLabel: 'Reset zoom.',
+ gifPlayButtonLabel: 'Play Animation',
+ gifPauseButtonLabel: 'Pause Animation',
fingerTap: 'Tap with one or two fingers to open keyboard',
before: 'before { obj }',
after: 'after { obj }',
diff --git a/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/index.vue b/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/index.vue
new file mode 100644
index 00000000000..59018ada3cf
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/index.vue
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/keys.js b/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/keys.js
new file mode 100644
index 00000000000..33fb87404c0
--- /dev/null
+++ b/kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad/keys.js
@@ -0,0 +1,222 @@
+/**
+ * Static key definitions for the NumericKeypad.
+ *
+ * Only two things are dynamic at runtime:
+ * 1. Digit labels — localized to the content's numeral system
+ * 2. The multiplication key in expression mode — × vs · based on Perseus config
+ *
+ * Everything else (operator keys, grid positions, aria labels) is defined here
+ * as module-level constants.
+ */
+import translator from '../../translator';
+
+const {
+ percent$,
+ pi$,
+ fractionExcludingExpression$,
+ decimal$,
+ negative$,
+ delete$,
+ times$,
+ plus$,
+ leftParenthesis$,
+ divide$,
+ minus$,
+ rightParenthesis$,
+ equalsSign$,
+ customExponent$,
+ squareRoot$,
+} = translator;
+
+// --- Operator keys (static) ---
+
+const PERCENT = {
+ id: 'PERCENT',
+ label: '%',
+ ariaLabel: percent$,
+ secondary: true,
+};
+
+const PI = {
+ id: 'PI',
+ label: 'π',
+ ariaLabel: pi$,
+ secondary: true,
+};
+
+const FRAC = {
+ id: 'FRAC',
+ label: '⁄',
+ ariaLabel: fractionExcludingExpression$,
+ secondary: true,
+};
+
+const DECIMAL = {
+ id: 'DECIMAL',
+ label: '.',
+ ariaLabel: decimal$,
+};
+
+const NEGATIVE = {
+ id: 'NEGATIVE',
+ label: '(−)',
+ ariaLabel: negative$,
+};
+
+const BACKSPACE = {
+ id: 'BACKSPACE',
+ label: '⌫',
+ ariaLabel: delete$,
+ secondary: true,
+};
+
+const PLUS = {
+ id: 'PLUS',
+ label: '+',
+ ariaLabel: plus$,
+ secondary: true,
+};
+
+const LEFT_PAREN = {
+ id: 'LEFT_PAREN',
+ label: '(',
+ ariaLabel: leftParenthesis$,
+ secondary: true,
+};
+
+const DIVIDE = {
+ id: 'DIVIDE',
+ label: '÷',
+ ariaLabel: divide$,
+ secondary: true,
+};
+
+const MINUS = {
+ id: 'MINUS',
+ label: '−',
+ ariaLabel: minus$,
+ secondary: true,
+};
+
+const RIGHT_PAREN = {
+ id: 'RIGHT_PAREN',
+ label: ')',
+ ariaLabel: rightParenthesis$,
+ secondary: true,
+};
+
+const EQUAL = {
+ id: 'EQUAL',
+ label: '=',
+ ariaLabel: equalsSign$,
+ secondary: true,
+};
+
+const EXP = {
+ id: 'EXP',
+ label: 'xⁿ',
+ ariaLabel: customExponent$,
+ secondary: true,
+};
+
+const SQRT = {
+ id: 'SQRT',
+ label: '√',
+ ariaLabel: squareRoot$,
+ secondary: true,
+};
+
+// --- Layout helpers ---
+
+/**
+ * Attach a grid position to a key definition.
+ * Returns a new object to avoid mutating the constant.
+ */
+function at(key, col, row) {
+ return { ...key, gridStyle: { gridColumn: col + 1, gridRow: row + 1 } };
+}
+
+/**
+ * Create a digit key. Label is resolved at runtime from localizedDigits.
+ */
+function digit(n, col, row, localizedDigits) {
+ const label = localizedDigits ? localizedDigits[n] : String(n);
+ return {
+ id: `NUM_${n}`,
+ label,
+ ariaLabel: () => label,
+ gridStyle: { gridColumn: col + 1, gridRow: row + 1 },
+ };
+}
+
+// --- Shared digit positions (same in both layouts) ---
+
+function digitRow0(ld) {
+ return [digit(7, 0, 0, ld), digit(8, 1, 0, ld), digit(9, 2, 0, ld)];
+}
+function digitRow1(ld) {
+ return [digit(4, 0, 1, ld), digit(5, 1, 1, ld), digit(6, 2, 1, ld)];
+}
+function digitRow2(ld) {
+ return [digit(1, 0, 2, ld), digit(2, 1, 2, ld), digit(3, 2, 2, ld)];
+}
+
+// --- Layouts ---
+
+// FRACTION layout: 4 columns × 4 rows
+// Row 0: 7 8 9 %
+// Row 1: 4 5 6 π
+// Row 2: 1 2 3 ⁄
+// Row 3: 0 . (−) ⌫
+export function fractionLayout(localizedDigits) {
+ return [
+ ...digitRow0(localizedDigits),
+ at(PERCENT, 3, 0),
+ ...digitRow1(localizedDigits),
+ at(PI, 3, 1),
+ ...digitRow2(localizedDigits),
+ at(FRAC, 3, 2),
+ digit(0, 0, 3, localizedDigits),
+ at(DECIMAL, 1, 3),
+ at(NEGATIVE, 2, 3),
+ at(BACKSPACE, 3, 3),
+ ];
+}
+
+// EXPRESSION layout: 6 columns × 4 rows
+// Row 0: 7 8 9 ×/· + (
+// Row 1: 4 5 6 ÷ − )
+// Row 2: 1 2 3 = ^ √
+// Row 3: 0 . (−) ⁄ π %
+export function expressionLayout(localizedDigits, useTimes) {
+ const timesKey = {
+ id: useTimes ? 'TIMES' : 'CDOT',
+ label: useTimes ? '×' : '·',
+ ariaLabel: times$,
+ secondary: true,
+ };
+
+ return [
+ ...digitRow0(localizedDigits),
+ at(timesKey, 3, 0),
+ at(PLUS, 4, 0),
+ at(LEFT_PAREN, 5, 0),
+
+ ...digitRow1(localizedDigits),
+ at(DIVIDE, 3, 1),
+ at(MINUS, 4, 1),
+ at(RIGHT_PAREN, 5, 1),
+
+ ...digitRow2(localizedDigits),
+ at(EQUAL, 3, 2),
+ at(EXP, 4, 2),
+ at(SQRT, 5, 2),
+
+ digit(0, 0, 3, localizedDigits),
+ at(DECIMAL, 1, 3),
+ at(NEGATIVE, 2, 3),
+ at(FRAC, 3, 3),
+ at(PI, 4, 3),
+ at(PERCENT, 5, 3),
+ ];
+}
diff --git a/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue b/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue
index d85b4a0e2ac..86bab3287d0 100644
--- a/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue
+++ b/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue
@@ -22,6 +22,7 @@
+
@@ -29,44 +30,51 @@