
==> ./monkeytype/README.md <==
[![](https://github.com/Miodec/monkeytype/blob/master/static/images/githubbanner2.png?raw=true)](https://monkeytype.com/)
<br />

<img align="left" alt="JavaScript" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/javascript/javascript.png" />
<img align="left" alt="HTML5" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/html/html.png" />
<img align="left" alt="CSS3" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/css/css.png" />
<img align="left" alt="CSS3" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/sass/sass.png" />
<br />

# About

Monkeytype is a minimalistic and customizable typing test. It features many test modes, an account system to save your typing speed history, and user-configurable features like themes, sounds, a smooth caret, and more.

# Features

- minimalistic design with no ads
- look at what you are typing
- focus mode
- different test modes
- punctuation mode
- themes
- quotes
- live wpm
- smooth caret
- account system
- command line
- and much more

# Discord bot

On the [Monkeytype Discord server](https://www.discord.gg/monkeytype), we added a Discord bot to auto-assign roles on our server. You can find its code over at https://github.com/Miodec/monkey-bot

# Bug report or Feature request

If you encounter a bug or have a feature request, [send me a message on Reddit](https://reddit.com/user/miodec), [create an issue](https://github.com/Miodec/monkeytype/issues), [create a discussion thread](https://github.com/Miodec/monkeytype/discussions), or [join the Discord server](https://www.discord.gg/monkeytype).

# Want to Contribute?

Refer to [CONTRIBUTING.md.](https://github.com/Miodec/monkeytype/blob/master/CONTRIBUTING.md)

# Code of Conduct

Before contributing to this repository, please read the [code of conduct.](https://github.com/Miodec/monkeytype/blob/master/CODE_OF_CONDUCT.md)

# Credits

[Montydrei](https://www.reddit.com/user/montydrei) for the name suggestion.

Everyone who provided valuable feedback on the [original Reddit post](https://www.reddit.com/r/MechanicalKeyboards/comments/gc6wx3/experimenting_with_a_completely_new_type_of/) for the prototype of this website.

All of the [contributors](https://github.com/Miodec/monkeytype/graphs/contributors) that have helped with implementing various features, adding themes, fixing bugs, and more.

# Support

If you wish to support further development and feel extra awesome, you can [donate](https://ko-fi.com/monkeytype), [become a Patron](https://www.patreon.com/monkeytype) or [buy a t-shirt](https://www.monkeytype.store/).

==> ./monkeytype/.npmrc <==
engine-strict=true

==> ./monkeytype/backend/example.env <==
DB_NAME=monkeytype
DB_URI=mongodb://localhost:27017
MODE=dev
# You can also use the format mongodb://username:password@host:port or
# uncomment the following lines if you want to define them separately
# DB_USERNAME=
# DB_PASSWORD=
# DB_AUTH_MECHANISM="SCRAM-SHA-256"
# DB_AUTH_SOURCE=admin

==> ./monkeytype/backend/init/mongodb.js <==
const { MongoClient } = require("mongodb");

let mongoClient;

module.exports = {
  async connectDB() {
    let options = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      connectTimeoutMS: 2000,
      serverSelectionTimeoutMS: 2000,
    };

    if (process.env.DB_USERNAME && process.env.DB_PASSWORD) {
      options.auth = {
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
      };
    }

    if (process.env.DB_AUTH_MECHANISM) {
      options.authMechanism = process.env.DB_AUTH_MECHANISM;
    }

    if (process.env.DB_AUTH_SOURCE) {
      options.authSource = process.env.DB_AUTH_SOURCE;
    }

    return MongoClient.connect(process.env.DB_URI, options)
      .then((client) => {
        mongoClient = client;
      })
      .catch((e) => {
        console.error(e.message);
        console.error("FAILED TO CONNECT TO DATABASE. EXITING...");
        process.exit(1);
      });
  },
  mongoDB() {
    return mongoClient.db(process.env.DB_NAME);
  },
};

==> ./monkeytype/backend/server.js <==
const express = require("express");
const { config } = require("dotenv");
const path = require("path");
const MonkeyError = require("./handlers/error");
config({ path: path.join(__dirname, ".env") });
const cors = require("cors");
const admin = require("firebase-admin");
const Logger = require("./handlers/logger.js");
const serviceAccount = require("./credentials/serviceAccountKey.json");
const { connectDB, mongoDB } = require("./init/mongodb");
const jobs = require("./jobs");
const addApiRoutes = require("./api/routes");

const PORT = process.env.PORT || 5005;

// MIDDLEWARE &  SETUP
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

app.set("trust proxy", 1);

app.use((req, res, next) => {
  if (process.env.MAINTENANCE === "true") {
    res.status(503).json({ message: "Server is down for maintenance" });
  } else {
    next();
  }
});

addApiRoutes(app);

//DO NOT REMOVE NEXT, EVERYTHING WILL EXPLODE
app.use(function (e, req, res, next) {
  if (/ECONNREFUSED.*27017/i.test(e.message)) {
    e.message = "Could not connect to the database. It may have crashed.";
    delete e.stack;
  }

  let monkeyError;
  if (e.errorID) {
    //its a monkey error
    monkeyError = e;
  } else {
    //its a server error
    monkeyError = new MonkeyError(e.status, e.message, e.stack);
  }
  if (!monkeyError.uid && req.decodedToken) {
    monkeyError.uid = req.decodedToken.uid;
  }
  if (process.env.MODE !== "dev" && monkeyError.status > 400) {
    Logger.log(
      "system_error",
      `${monkeyError.status} ${monkeyError.message}`,
      monkeyError.uid
    );
    mongoDB().collection("errors").insertOne({
      _id: monkeyError.errorID,
      timestamp: Date.now(),
      status: monkeyError.status,
      uid: monkeyError.uid,
      message: monkeyError.message,
      stack: monkeyError.stack,
    });
    monkeyError.stack = undefined;
  } else {
    console.error(monkeyError.message);
  }
  return res.status(monkeyError.status || 500).json(monkeyError);
});

console.log("Starting server...");
app.listen(PORT, async () => {
  console.log(`Listening on port ${PORT}`);
  console.log("Connecting to database...");
  await connectDB();
  console.log("Database connected");
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });

  console.log("Starting cron jobs...");
  jobs.forEach((job) => job.start());
});

==> ./monkeytype/backend/constants/quoteLanguages.js <==
const SUPPORTED_QUOTE_LANGUAGES = [
  "albanian",
  "arabic",
  "code_c++",
  "code_c",
  "code_java",
  "code_javascript",
  "code_python",
  "code_rust",
  "czech",
  "danish",
  "dutch",
  "english",
  "filipino",
  "french",
  "german",
  "hindi",
  "icelandic",
  "indonesian",
  "irish",
  "italian",
  "lithuanian",
  "malagasy",
  "polish",
  "portuguese",
  "russian",
  "serbian",
  "slovak",
  "spanish",
  "swedish",
  "thai",
  "toki_pona",
  "turkish",
  "vietnamese",
];

module.exports = SUPPORTED_QUOTE_LANGUAGES;

==> ./monkeytype/backend/dao/leaderboards.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { ObjectID } = require("mongodb");
const Logger = require("../handlers/logger");
const { performance } = require("perf_hooks");

class LeaderboardsDAO {
  static async get(mode, mode2, language, skip, limit = 50) {
    if (limit > 50 || limit <= 0) limit = 50;
    if (skip < 0) skip = 0;
    const preset = await mongoDB()
      .collection(`leaderboards.${language}.${mode}.${mode2}`)
      .find()
      .sort({ rank: 1 })
      .skip(parseInt(skip))
      .limit(parseInt(limit))
      .toArray();
    return preset;
  }

  static async getRank(mode, mode2, language, uid) {
    const res = await mongoDB()
      .collection(`leaderboards.${language}.${mode}.${mode2}`)
      .findOne({ uid });
    if (res)
      res.count = await mongoDB()
        .collection(`leaderboards.${language}.${mode}.${mode2}`)
        .estimatedDocumentCount();
    return res;
  }

  static async update(mode, mode2, language, uid = undefined) {
    let str = `lbPersonalBests.${mode}.${mode2}.${language}`;
    let start1 = performance.now();
    let lb = await mongoDB()
      .collection("users")
      .aggregate(
        [
          {
            $match: {
              [str + ".wpm"]: {
                $exists: true,
              },
              [str + ".acc"]: {
                $exists: true,
              },
              [str + ".timestamp"]: {
                $exists: true,
              },
              banned: { $exists: false },
            },
          },
          {
            $set: {
              [str + ".uid"]: "$uid",
              [str + ".name"]: "$name",
              [str + ".discordId"]: "$discordId",
            },
          },
          {
            $replaceRoot: {
              newRoot: "$" + str,
            },
          },
          {
            $sort: {
              wpm: -1,
              acc: -1,
              timestamp: -1,
            },
          },
        ],
        { allowDiskUse: true }
      )
      .toArray();
    let end1 = performance.now();

    let start2 = performance.now();
    let retval = undefined;
    lb.forEach((lbEntry, index) => {
      lbEntry.rank = index + 1;
      if (uid && lbEntry.uid === uid) {
        retval = index + 1;
      }
    });
    let end2 = performance.now();
    let start3 = performance.now();
    try {
      await mongoDB()
        .collection(`leaderboards.${language}.${mode}.${mode2}`)
        .drop();
    } catch (e) {}
    if (lb && lb.length !== 0)
      await mongoDB()
        .collection(`leaderboards.${language}.${mode}.${mode2}`)
        .insertMany(lb);
    let end3 = performance.now();

    let start4 = performance.now();
    await mongoDB()
      .collection(`leaderboards.${language}.${mode}.${mode2}`)
      .createIndex({
        uid: -1,
      });
    await mongoDB()
      .collection(`leaderboards.${language}.${mode}.${mode2}`)
      .createIndex({
        rank: 1,
      });
    let end4 = performance.now();

    let timeToRunAggregate = (end1 - start1) / 1000;
    let timeToRunLoop = (end2 - start2) / 1000;
    let timeToRunInsert = (end3 - start3) / 1000;
    let timeToRunIndex = (end4 - start4) / 1000;

    Logger.log(
      `system_lb_update_${language}_${mode}_${mode2}`,
      `Aggregate ${timeToRunAggregate}s, loop ${timeToRunLoop}s, insert ${timeToRunInsert}s, index ${timeToRunIndex}s`,
      uid
    );

    if (retval) {
      return {
        message: "Successfully updated leaderboard",
        rank: retval,
      };
    } else {
      return {
        message: "Successfully updated leaderboard",
      };
    }
  }
}

module.exports = LeaderboardsDAO;

==> ./monkeytype/backend/dao/preset.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { ObjectID } = require("mongodb");

class PresetDAO {
  static async getPresets(uid) {
    const preset = await mongoDB()
      .collection("presets")
      .find({ uid })
      .sort({ timestamp: -1 })
      .toArray(); // this needs to be changed to later take patreon into consideration
    return preset;
  }

  static async addPreset(uid, name, config) {
    const count = await mongoDB().collection("presets").find({ uid }).count();
    if (count >= 10) throw new MonkeyError(409, "Too many presets");
    let preset = await mongoDB()
      .collection("presets")
      .insertOne({ uid, name, config });
    return {
      insertedId: preset.insertedId,
    };
  }

  static async editPreset(uid, _id, name, config) {
    console.log(_id);
    const preset = await mongoDB()
      .collection("presets")
      .findOne({ uid, _id: ObjectID(_id) });
    if (!preset) throw new MonkeyError(404, "Preset not found");
    if (config) {
      return await mongoDB()
        .collection("presets")
        .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name, config } });
    } else {
      return await mongoDB()
        .collection("presets")
        .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name } });
    }
  }

  static async removePreset(uid, _id) {
    const preset = await mongoDB()
      .collection("presets")
      .findOne({ uid, _id: ObjectID(_id) });
    if (!preset) throw new MonkeyError(404, "Preset not found");
    return await mongoDB()
      .collection("presets")
      .deleteOne({ uid, _id: ObjectID(_id) });
  }
}

module.exports = PresetDAO;

==> ./monkeytype/backend/dao/quote-ratings.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");

class QuoteRatingsDAO {
  static async submit(quoteId, language, rating, update) {
    if (update) {
      await mongoDB()
        .collection("quote-rating")
        .updateOne(
          { quoteId, language },
          { $inc: { totalRating: rating } },
          { upsert: true }
        );
    } else {
      await mongoDB()
        .collection("quote-rating")
        .updateOne(
          { quoteId, language },
          { $inc: { ratings: 1, totalRating: rating } },
          { upsert: true }
        );
    }
    let quoteRating = await this.get(quoteId, language);

    let average = parseFloat(
      (
        Math.round((quoteRating.totalRating / quoteRating.ratings) * 10) / 10
      ).toFixed(1)
    );

    return await mongoDB()
      .collection("quote-rating")
      .updateOne({ quoteId, language }, { $set: { average } });
  }

  static async get(quoteId, language) {
    return await mongoDB()
      .collection("quote-rating")
      .findOne({ quoteId, language });
  }
}

module.exports = QuoteRatingsDAO;

==> ./monkeytype/backend/dao/user.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { ObjectID } = require("mongodb");
const { checkAndUpdatePb } = require("../handlers/pb");
const { updateAuthEmail } = require("../handlers/auth");
const { isUsernameValid } = require("../handlers/validation");

class UsersDAO {
  static async addUser(name, email, uid) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (user)
      throw new MonkeyError(400, "User document already exists", "addUser");
    return await mongoDB()
      .collection("users")
      .insertOne({ name, email, uid, addedAt: Date.now() });
  }

  static async deleteUser(uid) {
    return await mongoDB().collection("users").deleteOne({ uid });
  }

  static async updateName(uid, name) {
    const nameDoc = await mongoDB()
      .collection("users")
      .findOne({ name: { $regex: new RegExp(`^${name}$`, "i") } });
    if (nameDoc) throw new MonkeyError(409, "Username already taken");
    let user = await mongoDB().collection("users").findOne({ uid });
    if (
      Date.now() - user.lastNameChange < 2592000000 &&
      isUsernameValid(user.name)
    ) {
      throw new MonkeyError(409, "You can change your name once every 30 days");
    }
    return await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } });
  }

  static async clearPb(uid) {
    return await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { personalBests: {}, lbPersonalBests: {} } });
  }

  static async isNameAvailable(name) {
    const nameDoc = await mongoDB().collection("users").findOne({ name });
    if (nameDoc) {
      return false;
    } else {
      return true;
    }
  }

  static async updateQuoteRatings(uid, quoteRatings) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user)
      throw new MonkeyError(404, "User not found", "updateQuoteRatings");
    await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { quoteRatings } });
    return true;
  }

  static async updateEmail(uid, email) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "update email");
    await updateAuthEmail(uid, email);
    await mongoDB().collection("users").updateOne({ uid }, { $set: { email } });
    return true;
  }

  static async getUser(uid) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "get user");
    return user;
  }

  static async getUserByDiscordId(discordId) {
    const user = await mongoDB().collection("users").findOne({ discordId });
    if (!user)
      throw new MonkeyError(404, "User not found", "get user by discord id");
    return user;
  }

  static async addTag(uid, name) {
    let _id = ObjectID();
    await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $push: { tags: { _id, name } } });
    return {
      _id,
      name,
    };
  }

  static async getTags(uid) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "get tags");
    return user.tags;
  }

  static async editTag(uid, _id, name) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "edit tag");
    if (
      user.tags === undefined ||
      user.tags.filter((t) => t._id == _id).length === 0
    )
      throw new MonkeyError(404, "Tag not found");
    return await mongoDB()
      .collection("users")
      .updateOne(
        {
          uid: uid,
          "tags._id": ObjectID(_id),
        },
        { $set: { "tags.$.name": name } }
      );
  }

  static async removeTag(uid, _id) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "remove tag");
    if (
      user.tags === undefined ||
      user.tags.filter((t) => t._id == _id).length === 0
    )
      throw new MonkeyError(404, "Tag not found");
    return await mongoDB()
      .collection("users")
      .updateOne(
        {
          uid: uid,
          "tags._id": ObjectID(_id),
        },
        { $pull: { tags: { _id: ObjectID(_id) } } }
      );
  }

  static async removeTagPb(uid, _id) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "remove tag pb");
    if (
      user.tags === undefined ||
      user.tags.filter((t) => t._id == _id).length === 0
    )
      throw new MonkeyError(404, "Tag not found");
    return await mongoDB()
      .collection("users")
      .updateOne(
        {
          uid: uid,
          "tags._id": ObjectID(_id),
        },
        { $set: { "tags.$.personalBests": {} } }
      );
  }

  static async updateLbMemory(uid, mode, mode2, language, rank) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "update lb memory");
    if (user.lbMemory === undefined) user.lbMemory = {};
    if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {};
    if (user.lbMemory[mode][mode2] === undefined)
      user.lbMemory[mode][mode2] = {};
    user.lbMemory[mode][mode2][language] = rank;
    return await mongoDB()
      .collection("users")
      .updateOne(
        { uid },
        {
          $set: { lbMemory: user.lbMemory },
        }
      );
  }

  static async checkIfPb(uid, result) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "check if pb");

    const {
      mode,
      mode2,
      acc,
      consistency,
      difficulty,
      lazyMode,
      language,
      punctuation,
      rawWpm,
      wpm,
      funbox,
    } = result;

    if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") {
      return false;
    }

    if (mode === "quote") {
      return false;
    }

    let lbpb = user.lbPersonalBests;
    if (!lbpb) lbpb = {};

    let pb = checkAndUpdatePb(
      user.personalBests,
      lbpb,
      mode,
      mode2,
      acc,
      consistency,
      difficulty,
      lazyMode,
      language,
      punctuation,
      rawWpm,
      wpm
    );

    if (pb.isPb) {
      await mongoDB()
        .collection("users")
        .updateOne({ uid }, { $set: { personalBests: pb.obj } });
      if (pb.lbObj) {
        await mongoDB()
          .collection("users")
          .updateOne({ uid }, { $set: { lbPersonalBests: pb.lbObj } });
      }
      return true;
    } else {
      return false;
    }
  }

  static async checkIfTagPb(uid, result) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "check if tag pb");

    if (user.tags === undefined || user.tags.length === 0) {
      return [];
    }

    const {
      mode,
      mode2,
      acc,
      consistency,
      difficulty,
      lazyMode,
      language,
      punctuation,
      rawWpm,
      wpm,
      tags,
      funbox,
    } = result;

    if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") {
      return [];
    }

    if (mode === "quote") {
      return [];
    }

    let tagsToCheck = [];
    user.tags.forEach((tag) => {
      tags.forEach((resultTag) => {
        if (resultTag == tag._id) {
          tagsToCheck.push(tag);
        }
      });
    });

    let ret = [];

    tagsToCheck.forEach(async (tag) => {
      let tagpb = checkAndUpdatePb(
        tag.personalBests,
        undefined,
        mode,
        mode2,
        acc,
        consistency,
        difficulty,
        lazyMode,
        language,
        punctuation,
        rawWpm,
        wpm
      );
      if (tagpb.isPb) {
        ret.push(tag._id);
        await mongoDB()
          .collection("users")
          .updateOne(
            { uid, "tags._id": ObjectID(tag._id) },
            { $set: { "tags.$.personalBests": tagpb.obj } }
          );
      }
    });

    return ret;
  }

  static async resetPb(uid) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "reset pb");
    return await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { personalBests: {} } });
  }

  static async updateTypingStats(uid, restartCount, timeTyping) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user)
      throw new MonkeyError(404, "User not found", "update typing stats");

    return await mongoDB()
      .collection("users")
      .updateOne(
        { uid },
        {
          $inc: {
            startedTests: restartCount + 1,
            completedTests: 1,
            timeTyping,
          },
        }
      );
  }

  static async linkDiscord(uid, discordId) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "link discord");
    return await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { discordId } });
  }

  static async unlinkDiscord(uid) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user) throw new MonkeyError(404, "User not found", "unlink discord");
    return await mongoDB()
      .collection("users")
      .updateOne({ uid }, { $set: { discordId: null } });
  }

  static async incrementBananas(uid, wpm) {
    const user = await mongoDB().collection("users").findOne({ uid });
    if (!user)
      throw new MonkeyError(404, "User not found", "increment bananas");

    let best60;
    try {
      best60 = Math.max(...user.personalBests.time[60].map((best) => best.wpm));
    } catch (e) {
      best60 = undefined;
    }

    if (best60 === undefined || wpm >= best60 - best60 * 0.25) {
      //increment when no record found or wpm is within 25% of the record
      return await mongoDB()
        .collection("users")
        .updateOne({ uid }, { $inc: { bananas: 1 } });
    } else {
      return null;
    }
  }
}

module.exports = UsersDAO;

==> ./monkeytype/backend/dao/config.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");

class ConfigDAO {
  static async saveConfig(uid, config) {
    return await mongoDB()
      .collection("configs")
      .updateOne({ uid }, { $set: { config } }, { upsert: true });
  }

  static async getConfig(uid) {
    let config = await mongoDB().collection("configs").findOne({ uid });
    // if (!config) throw new MonkeyError(404, "Config not found");
    return config;
  }
}

module.exports = ConfigDAO;

==> ./monkeytype/backend/dao/new-quotes.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const fs = require("fs");
const simpleGit = require("simple-git");
const path = require("path");
let git;
try {
  git = simpleGit(path.join(__dirname, "../../../monkeytype-new-quotes"));
} catch (e) {
  git = undefined;
}
const stringSimilarity = require("string-similarity");
const { ObjectID } = require("mongodb");

class NewQuotesDAO {
  static async add(text, source, language, uid) {
    if (!git) throw new MonkeyError(500, "Git not available.");
    let quote = {
      text: text,
      source: source,
      language: language.toLowerCase(),
      submittedBy: uid,
      timestamp: Date.now(),
      approved: false,
    };
    //check for duplicate first
    const fileDir = path.join(
      __dirname,
      `../../../monkeytype-new-quotes/static/quotes/${language}.json`
    );
    let duplicateId = -1;
    let similarityScore = -1;
    if (fs.existsSync(fileDir)) {
      // let quoteFile = fs.readFileSync(fileDir);
      // quoteFile = JSON.parse(quoteFile.toString());
      // quoteFile.quotes.every((old) => {
      //   if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.9) {
      //     duplicateId = old.id;
      //     similarityScore = stringSimilarity.compareTwoStrings(
      //       old.text,
      //       quote.text
      //     );
      //     return false;
      //   }
      //   return true;
      // });
    } else {
      return { languageError: 1 };
    }
    if (duplicateId != -1) {
      return { duplicateId, similarityScore };
    }
    return await mongoDB().collection("new-quotes").insertOne(quote);
  }

  static async get() {
    if (!git) throw new MonkeyError(500, "Git not available.");
    return await mongoDB()
      .collection("new-quotes")
      .find({ approved: false })
      .sort({ timestamp: 1 })
      .limit(10)
      .toArray();
  }

  static async approve(quoteId, editQuote, editSource) {
    if (!git) throw new MonkeyError(500, "Git not available.");
    //check mod status
    let quote = await mongoDB()
      .collection("new-quotes")
      .findOne({ _id: ObjectID(quoteId) });
    if (!quote) {
      throw new MonkeyError(404, "Quote not found");
    }
    let language = quote.language;
    quote = {
      text: editQuote ? editQuote : quote.text,
      source: editSource ? editSource : quote.source,
      length: quote.text.length,
    };
    let message = "";
    const fileDir = path.join(
      __dirname,
      `../../../monkeytype-new-quotes/static/quotes/${language}.json`
    );
    await git.pull("upstream", "master");
    if (fs.existsSync(fileDir)) {
      let quoteFile = fs.readFileSync(fileDir);
      quoteFile = JSON.parse(quoteFile.toString());
      quoteFile.quotes.every((old) => {
        if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) {
          throw new MonkeyError(409, "Duplicate quote");
        }
      });
      let maxid = 0;
      quoteFile.quotes.map(function (q) {
        if (q.id > maxid) {
          maxid = q.id;
        }
      });
      quote.id = maxid + 1;
      quoteFile.quotes.push(quote);
      fs.writeFileSync(fileDir, JSON.stringify(quoteFile, null, 2));
      message = `Added quote to ${language}.json.`;
    } else {
      //file doesnt exist, create it
      quote.id = 1;
      fs.writeFileSync(
        fileDir,
        JSON.stringify({
          language: language,
          groups: [
            [0, 100],
            [101, 300],
            [301, 600],
            [601, 9999],
          ],
          quotes: [quote],
        })
      );
      message = `Created file ${language}.json and added quote.`;
    }
    await git.add([`static/quotes/${language}.json`]);
    await git.commit(`Added quote to ${language}.json`);
    await git.push("origin", "master");
    await mongoDB()
      .collection("new-quotes")
      .deleteOne({ _id: ObjectID(quoteId) });
    return { quote, message };
  }

  static async refuse(quoteId) {
    if (!git) throw new MonkeyError(500, "Git not available.");
    return await mongoDB()
      .collection("new-quotes")
      .deleteOne({ _id: ObjectID(quoteId) });
  }
}

module.exports = NewQuotesDAO;

==> ./monkeytype/backend/dao/public-stats.js <==
// const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { roundTo2 } = require("../handlers/misc");

class PublicStatsDAO {
  //needs to be rewritten, this is public stats not user stats
  static async updateStats(restartCount, time) {
    time = roundTo2(time);
    await mongoDB()
      .collection("public")
      .updateOne(
        { type: "stats" },
        {
          $inc: {
            testsCompleted: 1,
            testsStarted: restartCount + 1,
            timeTyping: time,
          },
        },
        { upsert: true }
      );
    return true;
  }
}

module.exports = PublicStatsDAO;

==> ./monkeytype/backend/dao/bot.js <==
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");

async function addCommand(command, arguments) {
  return await mongoDB().collection("bot-commands").insertOne({
    command,
    arguments,
    executed: false,
    requestTimestamp: Date.now(),
  });
}

async function addCommands(commands, arguments) {
  if (commands.length === 0 || commands.length !== arguments.length) {
    return [];
  }

  const normalizedCommands = commands.map((command, index) => {
    return {
      command,
      arguments: arguments[index],
      executed: false,
      requestTimestamp: Date.now(),
    };
  });

  return await mongoDB()
    .collection("bot-commands")
    .insertMany(normalizedCommands);
}

class BotDAO {
  static async updateDiscordRole(discordId, wpm) {
    return await addCommand("updateRole", [discordId, wpm]);
  }

  static async linkDiscord(uid, discordId) {
    return await addCommand("linkDiscord", [discordId, uid]);
  }

  static async unlinkDiscord(uid, discordId) {
    return await addCommand("unlinkDiscord", [discordId, uid]);
  }

  static async awardChallenge(discordId, challengeName) {
    return await addCommand("awardChallenge", [discordId, challengeName]);
  }

  static async announceLbUpdate(newRecords, leaderboardId) {
    if (newRecords.length === 0) {
      return [];
    }

    const leaderboardCommands = Array(newRecords.length).fill("sayLbUpdate");
    const leaderboardCommandsArguments = newRecords.map((newRecord) => {
      return [
        newRecord.discordId ?? newRecord.name,
        newRecord.rank,
        leaderboardId,
        newRecord.wpm,
        newRecord.raw,
        newRecord.acc,
        newRecord.consistency,
      ];
    });

    return await addCommands(leaderboardCommands, leaderboardCommandsArguments);
  }
}

module.exports = BotDAO;

==> ./monkeytype/backend/dao/psa.js <==
const { mongoDB } = require("../init/mongodb");

class PsaDAO {
  static async get(uid, config) {
    return await mongoDB().collection("psa").find().toArray();
  }
}

module.exports = PsaDAO;

==> ./monkeytype/backend/dao/result.js <==
const { ObjectID } = require("mongodb");
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const UserDAO = require("./user");

class ResultDAO {
  static async addResult(uid, result) {
    let user;
    try {
      user = await UserDAO.getUser(uid);
    } catch (e) {
      user = null;
    }
    if (!user) throw new MonkeyError(404, "User not found", "add result");
    if (result.uid === undefined) result.uid = uid;
    // result.ir = true;
    let res = await mongoDB().collection("results").insertOne(result);
    return {
      insertedId: res.insertedId,
    };
  }

  static async deleteAll(uid) {
    return await mongoDB().collection("results").deleteMany({ uid });
  }

  static async updateTags(uid, resultid, tags) {
    const result = await mongoDB()
      .collection("results")
      .findOne({ _id: ObjectID(resultid), uid });
    if (!result) throw new MonkeyError(404, "Result not found");
    const userTags = await UserDAO.getTags(uid);
    const userTagIds = userTags.map((tag) => tag._id.toString());
    let validTags = true;
    tags.forEach((tagId) => {
      if (!userTagIds.includes(tagId)) validTags = false;
    });
    if (!validTags)
      throw new MonkeyError(400, "One of the tag id's is not vaild");
    return await mongoDB()
      .collection("results")
      .updateOne({ _id: ObjectID(resultid), uid }, { $set: { tags } });
  }

  static async getResult(uid, id) {
    const result = await mongoDB()
      .collection("results")
      .findOne({ _id: ObjectID(id), uid });
    if (!result) throw new MonkeyError(404, "Result not found");
    return result;
  }

  static async getLastResult(uid) {
    let result = await mongoDB()
      .collection("results")
      .find({ uid })
      .sort({ timestamp: -1 })
      .limit(1)
      .toArray();
    result = result[0];
    if (!result) throw new MonkeyError(404, "No results found");
    return result;
  }

  static async getResultByTimestamp(uid, timestamp) {
    return await mongoDB().collection("results").findOne({ uid, timestamp });
  }

  static async getResults(uid, start, end) {
    start = start ?? 0;
    end = end ?? 1000;
    const result = await mongoDB()
      .collection("results")
      .find({ uid })
      .sort({ timestamp: -1 })
      .skip(start)
      .limit(end)
      .toArray(); // this needs to be changed to later take patreon into consideration
    if (!result) throw new MonkeyError(404, "Result not found");
    return result;
  }
}

module.exports = ResultDAO;

==> ./monkeytype/backend/middlewares/auth.js <==
const MonkeyError = require("../handlers/error");
const { verifyIdToken } = require("../handlers/auth");

module.exports = {
  async authenticateRequest(req, res, next) {
    try {
      if (process.env.MODE === "dev" && !req.headers.authorization) {
        if (req.body.uid) {
          req.decodedToken = {
            uid: req.body.uid,
          };
          console.log("Running authorization in dev mode");
          return next();
        } else {
          throw new MonkeyError(
            400,
            "Running authorization in dev mode but still no uid was provided"
          );
        }
      }
      const { authorization } = req.headers;
      if (!authorization)
        throw new MonkeyError(
          401,
          "Unauthorized",
          `endpoint: ${req.baseUrl} no authorization header found`
        );
      const token = authorization.split(" ");
      if (token[0].trim() !== "Bearer")
        return next(
          new MonkeyError(400, "Invalid Token", "Incorrect token type")
        );
      req.decodedToken = await verifyIdToken(token[1]);
      return next();
    } catch (e) {
      return next(e);
    }
  },
};

==> ./monkeytype/backend/middlewares/rate-limit.js <==
const rateLimit = require("express-rate-limit");

const getAddress = (req) =>
  req.headers["cf-connecting-ip"] ||
  req.headers["x-forwarded-for"] ||
  req.ip ||
  "255.255.255.255";
const message = "Too many requests, please try again later";
const multiplier = process.env.MODE === "dev" ? 100 : 1;

// Config Routing
exports.configUpdate = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.configGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 120 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Leaderboards Routing
exports.leaderboardsGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

// New Quotes Routing
exports.newQuotesGet = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.newQuotesAdd = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.newQuotesAction = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Quote Ratings Routing
exports.quoteRatingsGet = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.quoteRatingsSubmit = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Quote reporting
exports.quoteReportSubmit = rateLimit({
  windowMs: 30 * 60 * 1000, // 30 min
  max: 50 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Presets Routing
exports.presetsGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.presetsAdd = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.presetsRemove = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.presetsEdit = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

// PSA (Public Service Announcement) Routing
exports.psaGet = rateLimit({
  windowMs: 60 * 1000,
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Results Routing
exports.resultsGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.resultsAdd = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 500 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.resultsTagsUpdate = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 30 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.resultsDeleteAll = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 10 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.resultsLeaderboardGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.resultsLeaderboardQualificationGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

// Users Routing
exports.userGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userSignup = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 1 day
  max: 3 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userDelete = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 1 day
  max: 3 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userCheckName = rateLimit({
  windowMs: 60 * 1000,
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userUpdateName = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 1 day
  max: 3 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userUpdateLBMemory = rateLimit({
  windowMs: 60 * 1000,
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userUpdateEmail = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userClearPB = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userTagsGet = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userTagsRemove = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 30 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userTagsClearPB = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 60 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userTagsEdit = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 30 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userTagsAdd = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 30 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userDiscordLink = exports.usersTagsEdit = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 15 * multiplier,
  message,
  keyGenerator: getAddress,
});

exports.userDiscordUnlink = exports.usersTagsEdit = rateLimit({
  windowMs: 60 * 60 * 1000, // 60 min
  max: 15 * multiplier,
  message,
  keyGenerator: getAddress,
});

==> ./monkeytype/backend/middlewares/apiUtils.js <==
const joi = require("joi");
const MonkeyError = require("../handlers/error");

function requestValidation(validationSchema) {
  return (req, res, next) => {
    // In dev environments, as an alternative to token authentication,
    // you can pass the authentication middleware by having a user id in the body.
    // Inject the user id into the schema so that validation will not fail.
    if (process.env.MODE === "dev") {
      validationSchema.body = {
        uid: joi.any(),
        ...(validationSchema.body ?? {}),
      };
    }

    Object.keys(validationSchema).forEach((key) => {
      const schema = validationSchema[key];
      const joiSchema = joi.object().keys(schema);
      const { error } = joiSchema.validate(req[key] ?? {});
      if (error) {
        const errorMessage = error.details[0].message;
        throw new MonkeyError(400, `Invalid request: ${errorMessage}`);
      }
    });

    next();
  };
}

module.exports = {
  requestValidation,
};

==> ./monkeytype/backend/.gitignore <==
lastId.txt
log_success.txt
log_failed.txt

==> ./monkeytype/backend/api/controllers/leaderboards.js <==
const LeaderboardsDAO = require("../../dao/leaderboards");
const ResultDAO = require("../../dao/result");
const UserDAO = require("../../dao/user");
const admin = require("firebase-admin");
const { verifyIdToken } = require("../../handlers/auth");

class LeaderboardsController {
  static async get(req, res, next) {
    try {
      const { language, mode, mode2, skip, limit } = req.query;

      let uid;

      const { authorization } = req.headers;
      if (authorization) {
        const token = authorization.split(" ");
        if (token[0].trim() == "Bearer")
          req.decodedToken = await verifyIdToken(token[1]);
        uid = req.decodedToken.uid;
      }

      if (!language || !mode || !mode2 || !skip) {
        return res.status(400).json({
          message: "Missing parameters",
        });
      }
      let retval = await LeaderboardsDAO.get(
        mode,
        mode2,
        language,
        skip,
        limit
      );
      retval.forEach((item) => {
        if (uid && item.uid == uid) {
          //
        } else {
          delete item.discordId;
          delete item.uid;
          delete item.difficulty;
          delete item.language;
        }
      });
      return res.status(200).json(retval);
    } catch (e) {
      return next(e);
    }
  }

  static async getRank(req, res, next) {
    try {
      const { language, mode, mode2 } = req.query;
      const { uid } = req.decodedToken;
      if (!language || !mode || !mode2 || !uid) {
        return res.status(400).json({
          message: "Missing parameters",
        });
      }
      let retval = await LeaderboardsDAO.getRank(mode, mode2, language, uid);
      return res.status(200).json(retval);
    } catch (e) {
      return next(e);
    }
  }

  static async update(req, res, next) {
    try {
      return res.status(200).json({
        message: "Leaderboards disabled",
        lbdisabled: true,
      });
      if (process.env.LBDISABLED === true) {
        return res.status(200).json({
          message: "Leaderboards disabled",
          lbdisabled: true,
        });
      }
      const { rid } = req.body;
      const { uid } = req.decodedToken;
      if (!rid) {
        return res.status(400).json({
          message: "Missing parameters",
        });
      }
      //verify user first
      let user = await UserDAO.getUser(uid);
      if (!user) {
        return res.status(400).json({
          message: "User not found",
        });
      }
      if (user.banned === true) {
        return res.status(200).json({
          message: "User banned",
          banned: true,
        });
      }
      let userauth = await admin.auth().getUser(uid);
      if (!userauth.emailVerified) {
        return res.status(200).json({
          message: "User needs to verify email address",
          needsToVerifyEmail: true,
        });
      }

      let result = await ResultDAO.getResult(uid, rid);
      if (!result.language) result.language = "english";
      if (
        result.mode == "time" &&
        result.isPb &&
        (result.mode2 == 15 || result.mode2 == 60) &&
        ["english"].includes(result.language)
      ) {
        //check if its better than their current lb pb
        let lbpb =
          user?.lbPersonalBests?.[result.mode]?.[result.mode2]?.[
            result.language
          ]?.wpm;
        if (!lbpb) lbpb = 0;
        if (result.wpm >= lbpb) {
          //run update
          let retval = await LeaderboardsDAO.update(
            result.mode,
            result.mode2,
            result.language,
            uid
          );
          if (retval.rank) {
            await UserDAO.updateLbMemory(
              uid,
              result.mode,
              result.mode2,
              result.language,
              retval.rank
            );
          }
          return res.status(200).json(retval);
        } else {
          let rank = await LeaderboardsDAO.getRank(
            result.mode,
            result.mode2,
            result.language,
            uid
          );
          rank = rank?.rank;
          if (!rank) {
            return res.status(400).json({
              message: "User has a lbPb but was not found on the leaderboard",
            });
          }
          await UserDAO.updateLbMemory(
            uid,
            result.mode,
            result.mode2,
            result.language,
            rank
          );
          return res.status(200).json({
            message: "Not a new leaderboard personal best",
            rank,
          });
        }
      } else {
        return res.status(400).json({
          message: "This result is not eligible for any leaderboard",
        });
      }
    } catch (e) {
      return next(e);
    }
  }

  static async debugUpdate(req, res, next) {
    try {
      const { language, mode, mode2 } = req.body;
      if (!language || !mode || !mode2) {
        return res.status(400).json({
          message: "Missing parameters",
        });
      }
      let retval = await LeaderboardsDAO.update(mode, mode2, language);
      return res.status(200).json(retval);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = LeaderboardsController;

==> ./monkeytype/backend/api/controllers/preset.js <==
const PresetDAO = require("../../dao/preset");
const {
  isTagPresetNameValid,
  validateConfig,
} = require("../../handlers/validation");
const MonkeyError = require("../../handlers/error");

class PresetController {
  static async getPresets(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      let presets = await PresetDAO.getPresets(uid);
      return res.status(200).json(presets);
    } catch (e) {
      return next(e);
    }
  }

  static async addPreset(req, res, next) {
    try {
      const { name, config } = req.body;
      const { uid } = req.decodedToken;
      if (!isTagPresetNameValid(name))
        throw new MonkeyError(400, "Invalid preset name.");
      validateConfig(config);
      let preset = await PresetDAO.addPreset(uid, name, config);
      return res.status(200).json(preset);
    } catch (e) {
      return next(e);
    }
  }

  static async editPreset(req, res, next) {
    try {
      const { _id, name, config } = req.body;
      const { uid } = req.decodedToken;
      if (!isTagPresetNameValid(name))
        throw new MonkeyError(400, "Invalid preset name.");
      if (config) validateConfig(config);
      await PresetDAO.editPreset(uid, _id, name, config);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async removePreset(req, res, next) {
    try {
      const { _id } = req.body;
      const { uid } = req.decodedToken;
      await PresetDAO.removePreset(uid, _id);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = PresetController;

==> ./monkeytype/backend/api/controllers/core.js <==
class CoreController {
  static async handleTestResult() {}
}

==> ./monkeytype/backend/api/controllers/quote-ratings.js <==
const QuoteRatingsDAO = require("../../dao/quote-ratings");
const UserDAO = require("../../dao/user");
const MonkeyError = require("../../handlers/error");

class QuoteRatingsController {
  static async getRating(req, res, next) {
    try {
      const { quoteId, language } = req.query;
      let data = await QuoteRatingsDAO.get(parseInt(quoteId), language);
      return res.status(200).json(data);
    } catch (e) {
      return next(e);
    }
  }
  static async submitRating(req, res, next) {
    try {
      let { uid } = req.decodedToken;
      let { quoteId, rating, language } = req.body;
      quoteId = parseInt(quoteId);
      rating = parseInt(rating);
      if (isNaN(quoteId) || isNaN(rating)) {
        throw new MonkeyError(
          400,
          "Bad request. Quote id or rating is not a number."
        );
      }
      if (typeof language !== "string") {
        throw new MonkeyError(400, "Bad request. Language is not a string.");
      }

      if (rating < 1 || rating > 5) {
        throw new MonkeyError(
          400,
          "Bad request. Rating must be between 1 and 5."
        );
      }

      rating = Math.round(rating);

      //check if user already submitted a rating
      let user = await UserDAO.getUser(uid);

      if (!user) {
        throw new MonkeyError(401, "User not found.");
      }
      let quoteRatings = user.quoteRatings;

      if (quoteRatings === undefined) quoteRatings = {};
      if (quoteRatings[language] === undefined) quoteRatings[language] = {};
      if (quoteRatings[language][quoteId] == undefined)
        quoteRatings[language][quoteId] = undefined;

      let quoteRating = quoteRatings[language][quoteId];

      let newRating;
      let update;
      if (quoteRating) {
        //user already voted for this
        newRating = rating - quoteRating;
        update = true;
      } else {
        //user has not voted for this
        newRating = rating;
        update = false;
      }

      await QuoteRatingsDAO.submit(quoteId, language, newRating, update);
      quoteRatings[language][quoteId] = rating;
      await UserDAO.updateQuoteRatings(uid, quoteRatings);

      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = QuoteRatingsController;

==> ./monkeytype/backend/api/controllers/user.js <==
const UsersDAO = require("../../dao/user");
const BotDAO = require("../../dao/bot");
const {
  isUsernameValid,
  isTagPresetNameValid,
} = require("../../handlers/validation");
const MonkeyError = require("../../handlers/error");
const fetch = require("node-fetch");
const Logger = require("./../../handlers/logger.js");
const uaparser = require("ua-parser-js");

// import UsersDAO from "../../dao/user";
// import BotDAO from "../../dao/bot";
// import { isUsernameValid } from "../../handlers/validation";

class UserController {
  static async createNewUser(req, res, next) {
    try {
      const { name } = req.body;
      const { email, uid } = req.decodedToken;
      await UsersDAO.addUser(name, email, uid);
      Logger.log("user_created", `${name} ${email}`, uid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async deleteUser(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const userInfo = await UsersDAO.getUser(uid);
      await UsersDAO.deleteUser(uid);
      Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async updateName(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { name } = req.body;
      if (!isUsernameValid(name))
        return res.status(400).json({
          message:
            "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -",
        });
      let olduser = await UsersDAO.getUser(uid);
      await UsersDAO.updateName(uid, name);
      Logger.log(
        "user_name_updated",
        `changed name from ${olduser.name} to ${name}`,
        uid
      );
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async clearPb(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      await UsersDAO.clearPb(uid);
      Logger.log("user_cleared_pbs", "", uid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async checkName(req, res, next) {
    try {
      const { name } = req.body;
      if (!isUsernameValid(name))
        return next({
          status: 400,
          message:
            "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -",
        });
      const available = await UsersDAO.isNameAvailable(name);
      if (!available)
        return res.status(400).json({ message: "Username unavailable" });
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async updateEmail(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { newEmail } = req.body;
      try {
        await UsersDAO.updateEmail(uid, newEmail);
      } catch (e) {
        throw new MonkeyError(400, e.message, "update email", uid);
      }
      Logger.log("user_email_updated", `changed email to ${newEmail}`, uid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async getUser(req, res, next) {
    try {
      const { email, uid } = req.decodedToken;
      let userInfo;
      try {
        userInfo = await UsersDAO.getUser(uid);
      } catch (e) {
        if (email && uid) {
          userInfo = await UsersDAO.addUser(undefined, email, uid);
        } else {
          throw new MonkeyError(
            400,
            "User not found. Could not recreate user document.",
            "Tried to recreate user document but either email or uid is nullish",
            uid
          );
        }
      }
      let agent = uaparser(req.headers["user-agent"]);
      let logobj = {
        ip:
          req.headers["cf-connecting-ip"] ||
          req.headers["x-forwarded-for"] ||
          req.ip ||
          "255.255.255.255",
        agent:
          agent.os.name +
          " " +
          agent.os.version +
          " " +
          agent.browser.name +
          " " +
          agent.browser.version,
      };
      if (agent.device.vendor) {
        logobj.device =
          agent.device.vendor +
          " " +
          agent.device.model +
          " " +
          agent.device.type;
      }
      Logger.log("user_data_requested", logobj, uid);
      return res.status(200).json(userInfo);
    } catch (e) {
      return next(e);
    }
  }

  static async linkDiscord(req, res, next) {
    try {
      const { uid } = req.decodedToken;

      let requser;
      try {
        requser = await UsersDAO.getUser(uid);
      } catch (e) {
        requser = null;
      }
      if (requser?.banned === true) {
        throw new MonkeyError(403, "Banned accounts cannot link with Discord");
      }

      let discordFetch = await fetch("https://discord.com/api/users/@me", {
        headers: {
          authorization: `${req.body.data.tokenType} ${req.body.data.accessToken}`,
        },
      });
      discordFetch = await discordFetch.json();
      const did = discordFetch.id;
      if (!did) {
        throw new MonkeyError(
          500,
          "Could not get Discord account info",
          "did is undefined"
        );
      }
      let user;
      try {
        user = await UsersDAO.getUserByDiscordId(did);
      } catch (e) {
        user = null;
      }
      if (user !== null) {
        throw new MonkeyError(
          400,
          "This Discord account is already linked to a different account"
        );
      }
      await UsersDAO.linkDiscord(uid, did);
      await BotDAO.linkDiscord(uid, did);
      Logger.log("user_discord_link", `linked to ${did}`, uid);
      return res.status(200).json({
        message: "Discord account linked",
        did,
      });
    } catch (e) {
      return next(e);
    }
  }

  static async unlinkDiscord(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      let userInfo;
      try {
        userInfo = await UsersDAO.getUser(uid);
      } catch (e) {
        throw new MonkeyError(400, "User not found.");
      }
      if (!userInfo.discordId) {
        throw new MonkeyError(
          400,
          "User does not have a linked Discord account"
        );
      }
      await BotDAO.unlinkDiscord(uid, userInfo.discordId);
      await UsersDAO.unlinkDiscord(uid);
      Logger.log("user_discord_unlinked", userInfo.discordId, uid);
      return res.status(200).send();
    } catch (e) {
      return next(e);
    }
  }

  static async addTag(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { tagName } = req.body;
      if (!isTagPresetNameValid(tagName))
        return res.status(400).json({
          message:
            "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -",
        });
      let tag = await UsersDAO.addTag(uid, tagName);
      return res.status(200).json(tag);
    } catch (e) {
      return next(e);
    }
  }

  static async clearTagPb(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { tagid } = req.body;
      await UsersDAO.removeTagPb(uid, tagid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async editTag(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { tagid, newname } = req.body;
      if (!isTagPresetNameValid(newname))
        return res.status(400).json({
          message:
            "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -",
        });
      await UsersDAO.editTag(uid, tagid, newname);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async removeTag(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { tagid } = req.body;
      await UsersDAO.removeTag(uid, tagid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }

  static async getTags(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      let tags = await UsersDAO.getTags(uid);
      if (tags == undefined) tags = [];
      return res.status(200).json(tags);
    } catch (e) {
      return next(e);
    }
  }

  static async updateLbMemory(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { mode, mode2, language, rank } = req.body;
      await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = UserController;

==> ./monkeytype/backend/api/controllers/config.js <==
const ConfigDAO = require("../../dao/config");
const { validateConfig } = require("../../handlers/validation");

class ConfigController {
  static async getConfig(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      let config = await ConfigDAO.getConfig(uid);
      return res.status(200).json(config);
    } catch (e) {
      return next(e);
    }
  }
  static async saveConfig(req, res, next) {
    try {
      const { config } = req.body;
      const { uid } = req.decodedToken;
      validateConfig(config);
      await ConfigDAO.saveConfig(uid, config);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = ConfigController;

==> ./monkeytype/backend/api/controllers/new-quotes.js <==
const NewQuotesDAO = require("../../dao/new-quotes");
const MonkeyError = require("../../handlers/error");
const UserDAO = require("../../dao/user");
const Logger = require("../../handlers/logger.js");
// const Captcha = require("../../handlers/captcha");

class NewQuotesController {
  static async getQuotes(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const userInfo = await UserDAO.getUser(uid);
      if (!userInfo.quoteMod) {
        throw new MonkeyError(403, "You don't have permission to do this");
      }
      let data = await NewQuotesDAO.get();
      return res.status(200).json(data);
    } catch (e) {
      return next(e);
    }
  }

  static async addQuote(req, res, next) {
    try {
      throw new MonkeyError(
        500,
        "Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up."
      );
      // let { uid } = req.decodedToken;
      // let { text, source, language, captcha } = req.body;
      // if (!text || !source || !language) {
      //   throw new MonkeyError(400, "Please fill all the fields");
      // }
      // if (!(await Captcha.verify(captcha))) {
      //   throw new MonkeyError(400, "Captcha check failed");
      // }
      // let data = await NewQuotesDAO.add(text, source, language, uid);
      // return res.status(200).json(data);
    } catch (e) {
      return next(e);
    }
  }

  static async approve(req, res, next) {
    try {
      let { uid } = req.decodedToken;
      let { quoteId, editText, editSource } = req.body;
      const userInfo = await UserDAO.getUser(uid);
      if (!userInfo.quoteMod) {
        throw new MonkeyError(403, "You don't have permission to do this");
      }
      if (editText === "" || editSource === "") {
        throw new MonkeyError(400, "Please fill all the fields");
      }
      let data = await NewQuotesDAO.approve(quoteId, editText, editSource);
      Logger.log("system_quote_approved", data, uid);
      return res.status(200).json(data);
    } catch (e) {
      return next(e);
    }
  }

  static async refuse(req, res, next) {
    try {
      let { uid } = req.decodedToken;
      let { quoteId } = req.body;
      await NewQuotesDAO.refuse(quoteId, uid);
      return res.sendStatus(200);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = NewQuotesController;

==> ./monkeytype/backend/api/controllers/psa.js <==
const PsaDAO = require("../../dao/psa");

class PsaController {
  static async get(req, res, next) {
    try {
      let data = await PsaDAO.get();
      return res.status(200).json(data);
    } catch (e) {
      return next(e);
    }
  }
}

module.exports = PsaController;

==> ./monkeytype/backend/api/controllers/result.js <==
const ResultDAO = require("../../dao/result");
const UserDAO = require("../../dao/user");
const PublicStatsDAO = require("../../dao/public-stats");
const BotDAO = require("../../dao/bot");
const { validateObjectValues } = require("../../handlers/validation");
const { stdDev, roundTo2 } = require("../../handlers/misc");
const objecthash = require("object-hash");
const Logger = require("../../handlers/logger");
const path = require("path");
const { config } = require("dotenv");
config({ path: path.join(__dirname, ".env") });

let validateResult;
let validateKeys;
try {
  let module = require("../../anticheat/anticheat");
  validateResult = module.validateResult;
  validateKeys = module.validateKeys;
  if (!validateResult || !validateKeys) throw new Error("undefined");
} catch (e) {
  if (process.env.MODE === "dev") {
    console.error(
      "No anticheat module found. Continuing in dev mode, results will not be validated."
    );
  } else {
    console.error("No anticheat module found.");
    console.error(
      "To continue in dev mode, add 'MODE=dev' to the .env file in the backend directory."
    );
    process.exit(1);
  }
}

class ResultController {
  static async getResults(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const results = await ResultDAO.getResults(uid);
      return res.status(200).json(results);
    } catch (e) {
      next(e);
    }
  }

  static async deleteAll(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      await ResultDAO.deleteAll(uid);
      Logger.log("user_results_deleted", "", uid);
      return res.sendStatus(200);
    } catch (e) {
      next(e);
    }
  }

  static async updateTags(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { tags, resultid } = req.body;
      await ResultDAO.updateTags(uid, resultid, tags);
      return res.sendStatus(200);
    } catch (e) {
      next(e);
    }
  }

  static async addResult(req, res, next) {
    try {
      const { uid } = req.decodedToken;
      const { result } = req.body;
      result.uid = uid;
      if (validateObjectValues(result) > 0)
        return res.status(400).json({ message: "Bad input" });
      if (
        result.wpm <= 0 ||
        result.wpm > 350 ||
        result.acc < 75 ||
        result.acc > 100 ||
        result.consistency > 100
      ) {
        return res.status(400).json({ message: "Bad input" });
      }
      if (result.wpm == result.raw && result.acc != 100) {
        return res.status(400).json({ message: "Bad input" });
      }
      if (
        (result.mode === "time" && result.mode2 < 15 && result.mode2 > 0) ||
        (result.mode === "time" &&
          result.mode2 == 0 &&
          result.testDuration < 15) ||
        (result.mode === "words" && result.mode2 < 10 && result.mode2 > 0) ||
        (result.mode === "words" &&
          result.mode2 == 0 &&
          result.testDuration < 15) ||
        (result.mode === "custom" &&
          result.customText !== undefined &&
          !result.customText.isWordRandom &&
          !result.customText.isTimeRandom &&
          result.customText.textLen < 10) ||
        (result.mode === "custom" &&
          result.customText !== undefined &&
          result.customText.isWordRandom &&
          !result.customText.isTimeRandom &&
          result.customText.word < 10) ||
        (result.mode === "custom" &&
          result.customText !== undefined &&
          !result.customText.isWordRandom &&
          result.customText.isTimeRandom &&
          result.customText.time < 15)
      ) {
        return res.status(400).json({ message: "Test too short" });
      }

      let resulthash = result.hash;
      delete result.hash;
      const serverhash = objecthash(result);
      if (serverhash !== resulthash) {
        Logger.log(
          "incorrect_result_hash",
          {
            serverhash,
            resulthash,
            result,
          },
          uid
        );
        return res.status(400).json({ message: "Incorrect result hash" });
      }

      if (validateResult) {
        if (!validateResult(result)) {
          return res
            .status(400)
            .json({ message: "Result data doesn't make sense" });
        }
      } else {
        if (process.env.MODE === "dev") {
          console.error(
            "No anticheat module found. Continuing in dev mode, results will not be validated."
          );
        } else {
          throw new Error("No anticheat module found");
        }
      }

      result.timestamp = Math.round(result.timestamp / 1000) * 1000;

      //dont use - result timestamp is unreliable, can be changed by system time and stuff
      // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) {
      //   Logger.log(
      //     "time_traveler",
      //     {
      //       resultTimestamp: result.timestamp,
      //       serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10,
      //     },
      //     uid
      //   );
      //   return res.status(400).json({ message: "Time traveler detected" });

      // this probably wont work if we replace the timestamp with the server time later
      // let timestampres = await ResultDAO.getResultByTimestamp(
      //   uid,
      //   result.timestamp
      // );
      // if (timestampres) {
      //   return res.status(400).json({ message: "Duplicate result" });
      // }

      //convert result test duration to miliseconds
      const testDurationMilis = result.testDuration * 1000;
      //get latest result ordered by timestamp
      let lastResultTimestamp;
      try {
        lastResultTimestamp =
          (await ResultDAO.getLastResult(uid)).timestamp - 1000;
      } catch (e) {
        lastResultTimestamp = null;
      }

      result.timestamp = Math.round(Date.now() / 1000) * 1000;

      //check if its greater than server time - milis or result time - milis
      if (
        lastResultTimestamp &&
        (lastResultTimestamp + testDurationMilis > result.timestamp ||
          lastResultTimestamp + testDurationMilis >
            Math.round(Date.now() / 1000) * 1000)
      ) {
        Logger.log(
          "invalid_result_spacing",
          {
            lastTimestamp: lastResultTimestamp,
            resultTime: result.timestamp,
            difference:
              lastResultTimestamp + testDurationMilis - result.timestamp,
          },
          uid
        );
        return res.status(400).json({ message: "Invalid result spacing" });
      }

      try {
        result.keySpacingStats = {
          average:
            result.keySpacing.reduce(
              (previous, current) => (current += previous)
            ) / result.keySpacing.length,
          sd: stdDev(result.keySpacing),
        };
      } catch (e) {
        //
      }
      try {
        result.keyDurationStats = {
          average:
            result.keyDuration.reduce(
              (previous, current) => (current += previous)
            ) / result.keyDuration.length,
          sd: stdDev(result.keyDuration),
        };
      } catch (e) {
        //
      }

      const user = await UserDAO.getUser(uid);
      // result.name = user.name;

      //check keyspacing and duration here for bots
      if (
        result.mode === "time" &&
        result.wpm > 130 &&
        result.testDuration < 122
      ) {
        if (user.verified === false || user.verified === undefined) {
          if (
            result.keySpacingStats !== null &&
            result.keyDurationStats !== null
          ) {
            if (validateKeys) {
              if (!validateKeys(result, uid)) {
                return res
                  .status(400)
                  .json({ message: "Possible bot detected" });
              }
            } else {
              if (process.env.MODE === "dev") {
                console.error(
                  "No anticheat module found. Continuing in dev mode, results will not be validated."
                );
              } else {
                throw new Error("No anticheat module found");
              }
            }
          } else {
            return res.status(400).json({ message: "Missing key data" });
          }
        }
      }

      delete result.keySpacing;
      delete result.keyDuration;
      delete result.smoothConsistency;
      delete result.wpmConsistency;

      try {
        result.keyDurationStats.average = roundTo2(
          result.keyDurationStats.average
        );
        result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd);
        result.keySpacingStats.average = roundTo2(
          result.keySpacingStats.average
        );
        result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd);
      } catch (e) {
        //
      }

      let isPb = false;
      let tagPbs = [];

      if (!result.bailedOut) {
        isPb = await UserDAO.checkIfPb(uid, result);
        tagPbs = await UserDAO.checkIfTagPb(uid, result);
      }

      if (isPb) {
        result.isPb = true;
      }

      if (result.mode === "time" && String(result.mode2) === "60") {
        UserDAO.incrementBananas(uid, result.wpm);
        if (isPb && user.discordId) {
          BotDAO.updateDiscordRole(user.discordId, result.wpm);
        }
      }

      if (result.challenge && user.discordId) {
        BotDAO.awardChallenge(user.discordId, result.challenge);
      } else {
        delete result.challenge;
      }

      let tt = 0;
      let afk = result.afkDuration;
      if (afk == undefined) {
        afk = 0;
      }
      tt = result.testDuration + result.incompleteTestSeconds - afk;

      await UserDAO.updateTypingStats(uid, result.restartCount, tt);

      await PublicStatsDAO.updateStats(result.restartCount, tt);

      if (result.bailedOut === false) delete result.bailedOut;
      if (result.blindMode === false) delete result.blindMode;
      if (result.lazyMode === false) delete result.lazyMode;
      if (result.difficulty === "normal") delete result.difficulty;
      if (result.funbox === "none") delete result.funbox;
      if (result.language === "english") delete result.language;
      if (result.numbers === false) delete result.numbers;
      if (result.punctuation === false) delete result.punctuation;

      if (result.mode !== "custom") delete result.customText;

      let addedResult = await ResultDAO.addResult(uid, result);

      if (isPb) {
        Logger.log(
          "user_new_pb",
          `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${
            result.rawWpm
          } ${result.consistency}% (${addedResult.insertedId})`,
          uid
        );
      }

      return res.status(200).json({
        message: "Result saved",
        isPb,
        name: result.name,
        tagPbs,
        insertedId: addedResult.insertedId,
      });
    } catch (e) {
      next(e);
    }
  }

  static async getLeaderboard(req, res, next) {
    try {
      // const { type, mode, mode2 } = req.params;
      // const results = await ResultDAO.getLeaderboard(type, mode, mode2);
      // return res.status(200).json(results);
      return res
        .status(503)
        .json({ message: "Leaderboard temporarily disabled" });
    } catch (e) {
      next(e);
    }
  }

  static async checkLeaderboardQualification(req, res, next) {
    try {
      // const { uid } = req.decodedToken;
      // const { result } = req.body;
      // const data = await ResultDAO.checkLeaderboardQualification(uid, result);
      // return res.status(200).json(data);
      return res
        .status(503)
        .json({ message: "Leaderboard temporarily disabled" });
    } catch (e) {
      next(e);
    }
  }
}

module.exports = ResultController;

==> ./monkeytype/backend/api/routes/leaderboards.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const LeaderboardsController = require("../controllers/leaderboards");
const RateLimit = require("../../middlewares/rate-limit");

const { Router } = require("express");

const router = Router();

router.get("/", RateLimit.leaderboardsGet, LeaderboardsController.get);

router.get(
  "/rank",
  RateLimit.leaderboardsGet,
  authenticateRequest,
  LeaderboardsController.getRank
);

module.exports = router;

==> ./monkeytype/backend/api/routes/preset.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const PresetController = require("../controllers/preset");
const RateLimit = require("../../middlewares/rate-limit");

const { Router } = require("express");

const router = Router();

router.get(
  "/",
  RateLimit.presetsGet,
  authenticateRequest,
  PresetController.getPresets
);

router.post(
  "/add",
  RateLimit.presetsAdd,
  authenticateRequest,
  PresetController.addPreset
);

router.post(
  "/edit",
  RateLimit.presetsEdit,
  authenticateRequest,
  PresetController.editPreset
);

router.post(
  "/remove",
  RateLimit.presetsRemove,
  authenticateRequest,
  PresetController.removePreset
);

module.exports = router;

==> ./monkeytype/backend/api/routes/core.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");

const router = Router();

router.post("/test", authenticateRequest);

==> ./monkeytype/backend/api/routes/user.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const UserController = require("../controllers/user");
const RateLimit = require("../../middlewares/rate-limit");

const router = Router();

router.get(
  "/",
  RateLimit.userGet,
  authenticateRequest,
  UserController.getUser
);

router.post(
  "/signup",
  RateLimit.userSignup,
  authenticateRequest,
  UserController.createNewUser
);

router.post("/checkName", RateLimit.userCheckName, UserController.checkName);

router.post(
  "/delete",
  RateLimit.userDelete,
  authenticateRequest,
  UserController.deleteUser
);

router.post(
  "/updateName",
  RateLimit.userUpdateName,
  authenticateRequest,
  UserController.updateName
);

router.post(
  "/updateLbMemory",
  RateLimit.userUpdateLBMemory,
  authenticateRequest,
  UserController.updateLbMemory
);

router.post(
  "/updateEmail",
  RateLimit.userUpdateEmail,
  authenticateRequest,
  UserController.updateEmail
);

router.post(
  "/clearPb",
  RateLimit.userClearPB,
  authenticateRequest,
  UserController.clearPb
);

router.post(
  "/tags/add",
  RateLimit.userTagsAdd,
  authenticateRequest,
  UserController.addTag
);

router.get(
  "/tags",
  RateLimit.userTagsGet,
  authenticateRequest,
  UserController.getTags
);

router.post(
  "/tags/clearPb",
  RateLimit.userTagsClearPB,
  authenticateRequest,
  UserController.clearTagPb
);

router.post(
  "/tags/remove",
  RateLimit.userTagsRemove,
  authenticateRequest,
  UserController.removeTag
);

router.post(
  "/tags/edit",
  RateLimit.userTagsEdit,
  authenticateRequest,
  UserController.editTag
);

router.post(
  "/discord/link",
  RateLimit.userDiscordLink,
  authenticateRequest,
  UserController.linkDiscord
);

router.post(
  "/discord/unlink",
  RateLimit.userDiscordUnlink,
  authenticateRequest,
  UserController.unlinkDiscord
);

module.exports = router;

==> ./monkeytype/backend/api/routes/index.js <==
const pathOverride = process.env.API_PATH_OVERRIDE;
const BASE_ROUTE = pathOverride ? `/${pathOverride}` : "";

const API_ROUTE_MAP = {
  "/user": require("./user"),
  "/config": require("./config"),
  "/results": require("./result"),
  "/presets": require("./preset"),
  "/psa": require("./psa"),
  "/leaderboard": require("./leaderboards"),
  "/quotes": require("./quotes"),
};

function addApiRoutes(app) {
  app.get("/", (req, res) => {
    res.status(200).json({ message: "OK" });
  });

  Object.keys(API_ROUTE_MAP).forEach((route) => {
    const apiRoute = `${BASE_ROUTE}${route}`;
    const router = API_ROUTE_MAP[route];
    app.use(apiRoute, router);
  });
}

module.exports = addApiRoutes;

==> ./monkeytype/backend/api/routes/config.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const ConfigController = require("../controllers/config");
const RateLimit = require("../../middlewares/rate-limit");

const router = Router();

router.get(
  "/",
  RateLimit.configGet,
  authenticateRequest,
  ConfigController.getConfig
);

router.post(
  "/save",
  RateLimit.configUpdate,
  authenticateRequest,
  ConfigController.saveConfig
);

module.exports = router;

==> ./monkeytype/backend/api/routes/quotes.js <==
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const NewQuotesController = require("../controllers/new-quotes");
const QuoteRatingsController = require("../controllers/quote-ratings");
const RateLimit = require("../../middlewares/rate-limit");
const { requestValidation } = require("../../middlewares/apiUtils");
const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quoteLanguages");

const quotesRouter = Router();

quotesRouter.get(
  "/",
  RateLimit.newQuotesGet,
  authenticateRequest,
  NewQuotesController.getQuotes
);

quotesRouter.post(
  "/",
  RateLimit.newQuotesAdd,
  authenticateRequest,
  NewQuotesController.addQuote
);

quotesRouter.post(
  "/approve",
  RateLimit.newQuotesAction,
  authenticateRequest,
  NewQuotesController.approve
);

quotesRouter.post(
  "/reject",
  RateLimit.newQuotesAction,
  authenticateRequest,
  NewQuotesController.refuse
);

quotesRouter.get(
  "/rating",
  RateLimit.quoteRatingsGet,
  authenticateRequest,
  QuoteRatingsController.getRating
);

quotesRouter.post(
  "/rating",
  RateLimit.quoteRatingsSubmit,
  authenticateRequest,
  QuoteRatingsController.submitRating
);

quotesRouter.post(
  "/report",
  RateLimit.quoteReportSubmit,
  authenticateRequest,
  requestValidation({
    body: {
      quoteId: joi.string().required(),
      quoteLanguage: joi
        .string()
        .valid(...SUPPORTED_QUOTE_LANGUAGES)
        .required(),
      reason: joi
        .string()
        .valid(
          "Grammatical error",
          "Inappropriate content",
          "Low quality content"
        )
        .required(),
      comment: joi.string().allow("").max(250).required(),
    },
  }),
  (req, res) => {
    res.sendStatus(200);
  }
);

module.exports = quotesRouter;

==> ./monkeytype/backend/api/routes/psa.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const PsaController = require("../controllers/psa");
const RateLimit = require("../../middlewares/rate-limit");

const { Router } = require("express");

const router = Router();

router.get("/", RateLimit.psaGet, PsaController.get);

module.exports = router;

==> ./monkeytype/backend/api/routes/result.js <==
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const ResultController = require("../controllers/result");
const RateLimit = require("../../middlewares/rate-limit");

const router = Router();

router.get(
  "/",
  RateLimit.resultsGet,
  authenticateRequest,
  ResultController.getResults
);

router.post(
  "/add",
  RateLimit.resultsAdd,
  authenticateRequest,
  ResultController.addResult
);

router.post(
  "/updateTags",
  RateLimit.resultsTagsUpdate,
  authenticateRequest,
  ResultController.updateTags
);

router.post(
  "/deleteAll",
  RateLimit.resultsDeleteAll,
  authenticateRequest,
  ResultController.deleteAll
);

router.get(
  "/getLeaderboard/:type/:mode/:mode2",
  RateLimit.resultsLeaderboardGet,
  ResultController.getLeaderboard
);

router.post(
  "/checkLeaderboardQualification",
  RateLimit.resultsLeaderboardQualificationGet,
  authenticateRequest,
  ResultController.checkLeaderboardQualification
);

module.exports = router;

==> ./monkeytype/backend/jobs/deleteOldLogs.js <==
const { CronJob } = require("cron");
const { mongoDB } = require("../init/mongodb");

const CRON_SCHEDULE = "0 0 0 * * *";
const LOG_MAX_AGE_DAYS = 7;
const LOG_MAX_AGE_MILLISECONDS = LOG_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;

async function deleteOldLogs() {
  const data = await mongoDB()
    .collection("logs")
    .deleteMany({ timestamp: { $lt: Date.now() - LOG_MAX_AGE_MILLISECONDS } });

  Logger.log(
    "system_logs_deleted",
    `${data.deletedCount} logs deleted older than ${LOG_MAX_AGE_DAYS} day(s)`,
    undefined
  );
}

module.exports = new CronJob(CRON_SCHEDULE, deleteOldLogs);

==> ./monkeytype/backend/jobs/index.js <==
const updateLeaderboards = require("./updateLeaderboards");
const deleteOldLogs = require("./deleteOldLogs");

module.exports = [updateLeaderboards, deleteOldLogs];

==> ./monkeytype/backend/jobs/updateLeaderboards.js <==
const { CronJob } = require("cron");
const { mongoDB } = require("../init/mongodb");
const BotDAO = require("../dao/bot");
const LeaderboardsDAO = require("../dao/leaderboards");

const CRON_SCHEDULE = "30 4/5 * * * *";
const RECENT_AGE_MINUTES = 10;
const RECENT_AGE_MILLISECONDS = RECENT_AGE_MINUTES * 60 * 1000;

async function getTop10(leaderboardTime) {
  return await LeaderboardsDAO.get("time", leaderboardTime, "english", 0, 10);
}

async function updateLeaderboardAndNotifyChanges(leaderboardTime) {
  const top10BeforeUpdate = await getTop10(leaderboardTime);

  const previousRecordsMap = Object.fromEntries(
    top10BeforeUpdate.map((record) => {
      return [record.uid, record];
    })
  );

  await LeaderboardsDAO.update("time", leaderboardTime, "english");

  const top10AfterUpdate = await getTop10(leaderboardTime);

  const newRecords = top10AfterUpdate.filter((record) => {
    const userId = record.uid;

    const userImprovedRank =
      userId in previousRecordsMap &&
      previousRecordsMap[userId].rank > record.rank;

    const newUserInTop10 = !(userId in previousRecordsMap);

    const isRecentRecord =
      record.timestamp > Date.now() - RECENT_AGE_MILLISECONDS;

    return (userImprovedRank || newUserInTop10) && isRecentRecord;
  });

  if (newRecords.length > 0) {
    await BotDAO.announceLbUpdate(
      newRecords,
      `time ${leaderboardTime} english`
    );
  }
}

async function updateLeaderboards() {
  await updateLeaderboardAndNotifyChanges("15");
  await updateLeaderboardAndNotifyChanges("60");
}

module.exports = new CronJob(CRON_SCHEDULE, updateLeaderboards);

==> ./monkeytype/backend/handlers/logger.js <==
const { mongoDB } = require("../init/mongodb");

async function log(event, message, uid) {
  console.log(new Date(), "t", event, "t", uid, "t", message);
  await mongoDB().collection("logs").insertOne({
    timestamp: Date.now(),
    uid,
    event,
    message,
  });
}

module.exports = {
  log,
};

==> ./monkeytype/backend/handlers/misc.js <==
module.exports = {
  roundTo2(num) {
    return Math.round((num + Number.EPSILON) * 100) / 100;
  },
  stdDev(array) {
    const n = array.length;
    const mean = array.reduce((a, b) => a + b) / n;
    return Math.sqrt(
      array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
    );
  },
  mean(array) {
    try {
      return (
        array.reduce((previous, current) => (current += previous)) /
        array.length
      );
    } catch (e) {
      return 0;
    }
  },
};

==> ./monkeytype/backend/handlers/auth.js <==
const admin = require("firebase-admin");

module.exports = {
  async verifyIdToken(idToken) {
    return await admin.auth().verifyIdToken(idToken);
  },
  async updateAuthEmail(uid, email) {
    return await admin.auth().updateUser(uid, {
      email,
      emailVerified: false,
    });
  },
};

==> ./monkeytype/backend/handlers/pb.js <==
/*


obj structure

time: {
  10: [ - this is a list because there can be
    different personal bests for different difficulties, languages and punctuation
    {
      acc,
      consistency,
      difficulty,
      language,
      punctuation,
      raw,
      timestamp,
      wpm
    }
  ]
},
words: {
  10: [
    {}
  ]
},
zen: {
  zen: [
    {}
  ]
},
custom: {
  custom: {
    []
  }
}





*/

module.exports = {
  checkAndUpdatePb(
    obj,
    lbObj,
    mode,
    mode2,
    acc,
    consistency,
    difficulty,
    lazyMode = false,
    language,
    punctuation,
    raw,
    wpm
  ) {
    //verify structure first
    if (obj === undefined) obj = {};
    if (obj[mode] === undefined) obj[mode] = {};
    if (obj[mode][mode2] === undefined) obj[mode][mode2] = [];

    let isPb = false;
    let found = false;
    //find a pb
    obj[mode][mode2].forEach((pb) => {
      //check if we should compare first
      if (
        (pb.lazyMode === lazyMode ||
          (pb.lazyMode === undefined && lazyMode === false)) &&
        pb.difficulty === difficulty &&
        pb.language === language &&
        pb.punctuation === punctuation
      ) {
        found = true;
        //compare
        if (pb.wpm < wpm) {
          //update
          isPb = true;
          pb.acc = acc;
          pb.consistency = consistency;
          pb.difficulty = difficulty;
          pb.language = language;
          pb.punctuation = punctuation;
          pb.lazyMode = lazyMode;
          pb.raw = raw;
          pb.wpm = wpm;
          pb.timestamp = Date.now();
        }
      }
    });
    //if not found push a new one
    if (!found) {
      isPb = true;
      obj[mode][mode2].push({
        acc,
        consistency,
        difficulty,
        lazyMode,
        language,
        punctuation,
        raw,
        wpm,
        timestamp: Date.now(),
      });
    }

    if (
      lbObj &&
      mode === "time" &&
      (mode2 == "15" || mode2 == "60") &&
      !lazyMode
    ) {
      //updating lbpersonalbests object
      //verify structure first
      if (lbObj[mode] === undefined) lbObj[mode] = {};
      if (lbObj[mode][mode2] === undefined || Array.isArray(lbObj[mode][mode2]))
        lbObj[mode][mode2] = {};

      let bestForEveryLanguage = {};
      if (obj?.[mode]?.[mode2]) {
        obj[mode][mode2].forEach((pb) => {
          if (!bestForEveryLanguage[pb.language]) {
            bestForEveryLanguage[pb.language] = pb;
          } else {
            if (bestForEveryLanguage[pb.language].wpm < pb.wpm) {
              bestForEveryLanguage[pb.language] = pb;
            }
          }
        });
        Object.keys(bestForEveryLanguage).forEach((key) => {
          if (lbObj[mode][mode2][key] === undefined) {
            lbObj[mode][mode2][key] = bestForEveryLanguage[key];
          } else {
            if (lbObj[mode][mode2][key].wpm < bestForEveryLanguage[key].wpm) {
              lbObj[mode][mode2][key] = bestForEveryLanguage[key];
            }
          }
        });
        bestForEveryLanguage = {};
      }
    }

    return {
      isPb,
      obj,
      lbObj,
    };
  },
};

==> ./monkeytype/backend/handlers/error.js <==
const uuid = require("uuid");

class MonkeyError {
  constructor(status, message, stack = null, uid) {
    this.status = status ?? 500;
    this.errorID = uuid.v4();
    this.stack = stack;
    // this.message =
    // process.env.MODE === "dev"
    //   ? stack
    //     ? String(stack)
    //     : this.status === 500
    //     ? String(message)
    //     : message
    //   : "Internal Server Error " + this.errorID;

    if (process.env.MODE === "dev") {
      this.message = stack
        ? String(message) + "\nStack: " + String(stack)
        : String(message);
    } else {
      if (this.stack && this.status >= 500) {
        this.message = "Internal Server Error " + this.errorID;
      } else {
        this.message = String(message);
      }
    }
  }
}

module.exports = MonkeyError;

==> ./monkeytype/backend/handlers/validation.js <==
const MonkeyError = require("./error");

function isUsernameValid(name) {
  if (name === null || name === undefined || name === "") return false;
  if (/.*miodec.*/.test(name.toLowerCase())) return false;
  //sorry for the bad words
  if (
    /.*(bitly|fuck|bitch|shit|pussy|nigga|niqqa|niqqer|nigger|ni99a|ni99er|niggas|niga|niger|cunt|faggot|retard).*/.test(
      name.toLowerCase()
    )
  )
    return false;
  if (name.length > 14) return false;
  if (/^\..*/.test(name.toLowerCase())) return false;
  return /^[0-9a-zA-Z_.-]+$/.test(name);
}

function isTagPresetNameValid(name) {
  if (name === null || name === undefined || name === "") return false;
  if (name.length > 16) return false;
  return /^[0-9a-zA-Z_.-]+$/.test(name);
}

function isConfigKeyValid(name) {
  if (name === null || name === undefined || name === "") return false;
  if (name.length > 40) return false;
  return /^[0-9a-zA-Z_.\-#+]+$/.test(name);
}

function validateConfig(config) {
  Object.keys(config).forEach((key) => {
    if (!isConfigKeyValid(key)) {
      throw new MonkeyError(500, `Invalid config: ${key} failed regex check`);
    }
    // if (key === "resultFilters") return;
    // if (key === "customBackground") return;
    if (key === "customBackground" || key === "customLayoutfluid") {
      let val = config[key];
      if (/[<>]/.test(val)) {
        throw new MonkeyError(
          500,
          `Invalid config: ${key}:${val} failed regex check`
        );
      }
    } else {
      let val = config[key];
      if (Array.isArray(val)) {
        val.forEach((valarr) => {
          if (!isConfigKeyValid(valarr)) {
            throw new MonkeyError(
              500,
              `Invalid config: ${key}:${valarr} failed regex check`
            );
          }
        });
      } else {
        if (!isConfigKeyValid(val)) {
          throw new MonkeyError(
            500,
            `Invalid config: ${key}:${val} failed regex check`
          );
        }
      }
    }
  });
  return true;
}

function validateObjectValues(val) {
  let errCount = 0;
  if (val === null || val === undefined) {
    //
  } else if (Array.isArray(val)) {
    //array
    val.forEach((val2) => {
      errCount += validateObjectValues(val2);
    });
  } else if (typeof val === "object" && !Array.isArray(val)) {
    //object
    Object.keys(val).forEach((valkey) => {
      errCount += validateObjectValues(val[valkey]);
    });
  } else {
    if (!/^[0-9a-zA-Z._\-+]+$/.test(val)) {
      errCount++;
    }
  }
  return errCount;
}

module.exports = {
  isUsernameValid,
  isTagPresetNameValid,
  validateConfig,
  validateObjectValues,
};

==> ./monkeytype/backend/handlers/captcha.js <==
const fetch = require("node-fetch");
const path = require("path");
const { config } = require("dotenv");
config({ path: path.join(__dirname, ".env") });

module.exports = {
  async verify(captcha) {
    if (process.env.MODE === "dev") return true;
    let response = await fetch(
      `https://www.google.com/recaptcha/api/siteverify`,
      {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captcha}`,
      }
    );
    response = await response.json();
    return response?.success;
  },
};

==> ./monkeytype/backend/handlers/pb_old.js <==
// module.exports = {
//   check(result, userdata) {
//     let pbs = null;
//     if (result.mode == "quote") {
//       return false;
//     }
//     if (result.funbox !== "none") {
//       return false;
//     }

//     pbs = userdata?.personalBests;
//     if(pbs === undefined){
//       //userdao set personal best
//       return true;
//     }

//     // try {
//     //   pbs = userdata.personalBests;
//     //   if (pbs === undefined) {
//     //     throw new Error("pb is undefined");
//     //   }
//     // } catch (e) {
//       // User.findOne({ uid: userdata.uid }, (err, user) => {
//       //   user.personalBests = {
//       //     [result.mode]: {
//       //       [result.mode2]: [
//       //         {
//       //           language: result.language,
//       //           difficulty: result.difficulty,
//       //           punctuation: result.punctuation,
//       //           wpm: result.wpm,
//       //           acc: result.acc,
//       //           raw: result.rawWpm,
//       //           timestamp: Date.now(),
//       //           consistency: result.consistency,
//       //         },
//       //       ],
//       //     },
//       //   };
//       // }).then(() => {
//       //   return true;
//       // });
//     // }

//     let toUpdate = false;
//     let found = false;
//     try {
//       if (pbs[result.mode][result.mode2] === undefined) {
//         pbs[result.mode][result.mode2] = [];
//       }
//       pbs[result.mode][result.mode2].forEach((pb) => {
//         if (
//           pb.punctuation === result.punctuation &&
//           pb.difficulty === result.difficulty &&
//           pb.language === result.language
//         ) {
//           //entry like this already exists, compare wpm
//           found = true;
//           if (pb.wpm < result.wpm) {
//             //new pb
//             pb.wpm = result.wpm;
//             pb.acc = result.acc;
//             pb.raw = result.rawWpm;
//             pb.timestamp = Date.now();
//             pb.consistency = result.consistency;
//             toUpdate = true;
//           } else {
//             //no pb
//             return false;
//           }
//         }
//       });
//       //checked all pbs, nothing found - meaning this is a new pb
//       if (!found) {
//         pbs[result.mode][result.mode2] = [
//           {
//             language: result.language,
//             difficulty: result.difficulty,
//             punctuation: result.punctuation,
//             wpm: result.wpm,
//             acc: result.acc,
//             raw: result.rawWpm,
//             timestamp: Date.now(),
//             consistency: result.consistency,
//           },
//         ];
//         toUpdate = true;
//       }
//     } catch (e) {
//       // console.log(e);
//       pbs[result.mode] = {};
//       pbs[result.mode][result.mode2] = [
//         {
//           language: result.language,
//           difficulty: result.difficulty,
//           punctuation: result.punctuation,
//           wpm: result.wpm,
//           acc: result.acc,
//           raw: result.rawWpm,
//           timestamp: Date.now(),
//           consistency: result.consistency,
//         },
//       ];
//       toUpdate = true;
//     }

//     if (toUpdate) {
//       // User.findOne({ uid: userdata.uid }, (err, user) => {
//       //   user.personalBests = pbs;
//       //   user.save();
//       // });

//       //userdao update the whole personalBests parameter with pbs object
//       return true;
//     } else {
//       return false;
//     }
//   }
// }

==> ./monkeytype/backend/credentials/.gitkeep <==

==> ./monkeytype/.prettierignore <==
*.min.js
*.min.css
layouts.js
quotes/*
chartjs-plugin-*.js
sound/*
node_modules
css/balloon.css
_list.json

==> ./monkeytype/.editorconfig <==
root = true

[*.{html,js,css,scss,json,yml,yaml}]
indent_size = 2
indent_style = space

==> ./monkeytype/.gitignore <==
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*

# Firebase cache
.firebase/

# Firebase config

# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc

# Runtime data
pids
*.pid
*.seed
*.pid.lock

#Mac files
.DS_Store

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

#vs code
.vscode
*.code-workspace

.idea

#firebase
.firebaserc
.firebaserc_copy
serviceAccountKey*.json

#generated files
dist/

#cloudflare y
.cloudflareKey.txt
.cloudflareKey_copy.txt
purgeCfCache.sh

static/adtest.html
backend/lastId.txt
backend/log_success.txt
backend/credentials/*.json
backend/.env

static/adtest.html
backend/migrationStats.txt

backend/anticheat
==> ./monkeytype/static/index.html <==
<!DOCTYPE html>
<html lang="en">
  <head>
    <script>
      document.addEventListener("keydown", function (e) {
        if (e.key == "Escape") e.preventDefault();
      });
    </script>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Monkeytype</title>
    <!-- <link rel="stylesheet" href="css/fa.css" /> -->
    <link
      href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css"
      rel="stylesheet"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="css/balloon.css" />
    <link rel="stylesheet" href="css/style.css?v=1" />
    <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" />
    <link rel="stylesheet" href="" id="funBoxTheme" />
    <link id="favicon" rel="shortcut icon" href="/images/favicon/favicon.ico" />
    <link
      rel="apple-touch-icon"
      sizes="180x180"
      href="/images/favicon/apple-touch-icon.png"
    />
    <link
      rel="mask-icon"
      href="/images/favicon/safari-pinned-tab.svg"
      color="#eee"
    />
    <meta name="msapplication-TileColor" content="#e2b714" />
    <meta
      name="msapplication-config"
      content="/images/favicon/browserconfig.xml"
    />
    <meta id="metaThemeColor" name="theme-color" content="#e2b714" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
      onerror="this.onerror=null;this.href='css/fa.min.css';"
    />
    <link rel="manifest" href="manifest.json" />
    <meta name="name" content="Monkeytype" />
    <meta name="image" content="https://monkeytype.com/images/mtsocial.png" />
    <meta
      name="description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta
      name="keywords"
      content="typing speed test, typing speedtest, typing test, speetest, speed test, typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test"
    />
    <meta name="author" content="Miodec" />
    <meta property="og:title" content="Monkeytype" />
    <meta property="og:url" content="https://monkeytype.com/" />
    <meta property="og:type" content="website" />
    <meta
      property="og:image"
      content="https://monkeytype.com/images/mtsocial.png"
    />
    <meta
      property="og:description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta name="twitter:title" content="Monkeytype" />
    <meta
      name="twitter:image"
      content="https://monkeytype.com/images/mtsocial.png"
    />
    <meta name="twitter:card" content="summary_large_image" />
    <!-- <script type="text/javascript">
      window["nitroAds"] = window["nitroAds"] || {
        createAd: function () {
          window.nitroAds.queue.push(["createAd", arguments]);
        },
        queue: [],
      };
    </script> -->
    <!-- <script async src="https://s.nitropay.com/ads-693.js"></script> -->
    <script
      src="https://hb.vntsm.com/v3/live/ad-manager.min.js"
      type="text/javascript"
      data-site-id="60b78af12119122b8958910f"
      data-mode="scan"
      id="adScript"
      async
    ></script>
    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("sw.js");
      }
    </script>
  </head>
  <div class="customBackground"></div>

  <body>
    <noscript>
      <div
        style="
          background: #333;
          width: 100vw;
          height: 100vh;
          left: 0;
          top: 0;
          position: fixed;
          display: grid;
          justify-content: center;
          align-content: center;
          color: #646669;
          font-size: 1rem;
          font-family: Arial Rounded MT Bold;
          z-index: 99999;
          padding: 6rem;
          box-sizing: border-box;
        "
      >
        <div
          style="
            display: grid;
            justify-content: center;
            align-content: center;
            max-width: 800px;
            gap: 1rem;
          "
        >
          <span style="font-size: 6rem; color: #e2b714">:(</span>
          <span style="font-size: 2rem; color: #d1d0c5">
            Looks like JavaScript is disabled. Please enable JavaScript in order
            to use this website.
          </span>
        </div>
      </div>
    </noscript>
    <div
      id="nocss"
      style="
        background: #333;
        width: 100vw;
        height: 100vh;
        left: 0;
        top: 0;
        position: fixed;
        display: grid;
        justify-content: center;
        align-content: center;
        color: #646669;
        font-size: 1rem;
        font-family: Arial Rounded MT Bold;
        z-index: 99999;
        padding: 6rem;
        box-sizing: border-box;
      "
    >
      <div
        style="
          display: grid;
          justify-content: center;
          align-content: center;
          max-width: 800px;
          gap: 1rem;
        "
      >
        <span style="font-size: 6rem; color: #e2b714">:(</span>
        <span style="font-size: 2rem; color: #d1d0c5">
          It seems like the CSS failed to load. Please clear your cache to
          redownload the styles. If that doesn't help contact support.
          <br />
          (jack@monkeytype.com or discord.gg/monkeytype)
        </span>
        <span>(ctrl/cmd + shift + r on Chromium browsers)</span>
        <span>
          If the website works for a bit but then this screen comes back, clear
          your cache again and then on Monkeytype open the command line (esc)
          and search for "Clear SW cache".
        </span>
      </div>
    </div>
    <div id="ad_rich_media" class="hidden"></div>
    <div id="backgroundLoader" style="display: none"></div>
    <div id="bannerCenter"></div>
    <div id="notificationCenter">
      <div class="history"></div>
    </div>
    <div
      class="nameChangeMessage"
      style="
        background-color: var(--main-color);
        padding: 0.5rem;
        text-align: center;
        color: var(--bg-color);
        display: none;
      "
    >
      Important information about your account. Please click this message.
    </div>

    <div id="simplePopupWrapper" class="popupWrapper hidden">
      <div id="simplePopup" popupId=""></div>
    </div>

    <div id="mobileTestConfigPopupWrapper" class="popupWrapper hidden">
      <div id="mobileTestConfigPopup">
        <div class="group">
          <div class="button punctuation">punctuation</div>
          <div class="button numbers">numbers</div>
        </div>
        <div class="spacer"></div>
        <div class="group modeGroup">
          <div class="button" mode="time">time</div>
          <div class="button active" mode="words">words</div>
          <div class="button" mode="quote">quote</div>
          <div class="button" mode="zen">zen</div>
          <div class="button" mode="custom">custom</div>
        </div>
        <div class="spacer"></div>
        <div class="group timeGroup">
          <div class="button" time="15">15</div>
          <div class="button active" time="30">30</div>
          <div class="button" time="60">60</div>
          <div class="button" time="120">120</div>
          <div class="button" time="custom">custom</div>
        </div>
        <div class="group wordsGroup hidden">
          <div class="button" words="10">10</div>
          <div class="button" words="25">25</div>
          <div class="button" words="50">50</div>
          <div class="button" words="100">100</div>
          <div class="button" words="custom">custom</div>
        </div>
        <div class="group quoteGroup hidden">
          <div class="button" quote="-1">all</div>
          <div class="button" quote="0">short</div>
          <div class="button" quote="1">medium</div>
          <div class="button" quote="2">long</div>
          <div class="button" quote="3">thicc</div>
          <div class="button" quote="-2">search</div>
        </div>
        <div class="group customGroup hidden">
          <div class="button customChange">change</div>
        </div>
      </div>
    </div>

    <div id="pbTablesPopupWrapper" class="popupWrapper hidden">
      <div id="pbTablesPopup">
        <!-- <div class="title">All words personal bests</div> -->
        <table>
          <thead>
            <tr>
              <td width="1%">words</td>
              <td>
                wpm
                <br />
                <span class="sub">accuracy</span>
              </td>
              <td>
                raw
                <br />
                <span class="sub">consistency</span>
              </td>
              <td>difficulty</td>
              <td>language</td>
              <td>punctuation</td>
              <td>lazy mode</td>
              <td>date</td>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>

    <div id="practiseWordsPopupWrapper" class="popupWrapper hidden">
      <div id="practiseWordsPopup" action="">
        <div class="title">Practice words</div>
        <div class="text">
          This will start a new test in custom mode. Words that you mistyped
          more often or words that you typed much slower will be weighted higher
          and appear more often.
        </div>
        <div class="button missed" tabindex="1">Practice missed</div>
        <div class="button slow" tabindex="1">Practice slow</div>
        <div class="button both" tabindex="1">Practice both</div>
      </div>
    </div>

    <div id="rateQuotePopupWrapper" class="popupWrapper hidden">
      <div id="rateQuotePopup" quoteId="">
        <div class="quote">
          <div class="text">-</div>
          <div class="id">
            <div class="top">id</div>
            <div class="val">-</div>
          </div>
          <div class="length">
            <div class="top">length</div>
            <div class="val">-</div>
          </div>
          <div class="source">
            <div class="top">source</div>
            <div class="val">-</div>
          </div>
        </div>
        <div class="spacer"></div>
        <div class="ratingStats">
          <div class="ratingCount">
            <div class="top">ratings</div>
            <div class="val">-</div>
          </div>
          <div class="ratingAverage">
            <div class="top">average</div>
            <div class="val">-</div>
          </div>
          <div class="starsWrapper">
            <div class="top">your rating</div>
            <div class="stars">
              <div class="star active" rating="1">
                <i class="fas fa-fw fa-star"></i>
              </div>
              <div class="star active" rating="2">
                <i class="fas fa-fw fa-star"></i>
              </div>
              <div class="star active" rating="3">
                <i class="fas fa-fw fa-star"></i>
              </div>
              <div class="star" rating="4">
                <i class="fas fa-fw fa-star"></i>
              </div>
              <div class="star" rating="5">
                <i class="fas fa-fw fa-star"></i>
              </div>
            </div>
          </div>
        </div>
        <!-- <div class="button rate1">
            <i class="fas fa-star"></i>
            <i class="far fa-star"></i>
            <i class="far fa-star"></i>
          </div>
          <div class="button rate2">
            <i class="fas fa-star"></i>
            <i class="fas fa-star"></i>
            <i class="far fa-star"></i>
          </div>
          <div class="button rate3">
            <i class="fas fa-star"></i>
            <i class="fas fa-star"></i>
            <i class="fas fa-star"></i>
          </div> -->
        <span aria-label="Submit review" data-balloon-pos="up">
          <div class="icon-button submitButton">
            <i class="fas fa-chevron-right"></i>
          </div>
        </span>
      </div>
    </div>

    <div id="settingsImportWrapper" class="popupWrapper hidden">
      <div id="settingsImport" action="">
        <input type="text" />
        <div class="button">import settings</div>
      </div>
    </div>

    <div id="customThemeShareWrapper" class="popupWrapper hidden">
      <div id="customThemeShare" action="">
        <input type="text" />
        <div class="button">ok</div>
      </div>
    </div>
    <div id="customTextPopupWrapper" class="popupWrapper hidden">
      <div id="customTextPopup" action="">
        <div class="wordfilter button">
          <i class="fas fa-filter"></i>
          Words filter
        </div>
        <textarea class="textarea" placeholder="Custom text"></textarea>
        <div class="inputs">
          <label class="checkbox">
            <input type="checkbox" />
            <div class="customTextCheckbox"></div>
            Random
            <span>
              randomize the above words, and control how many words are
              generated.
            </span>
          </label>
          <div class="randomInputFields hidden">
            <label class="wordcount">
              Word count
              <input type="number" value="" min="1" max="10000" />
            </label>
            <div style="color: var(--sub-color)">or</div>
            <label class="time">
              Time
              <input type="number" value="" min="1" max="10000" />
            </label>
          </div>
          <label class="checkbox typographyCheck">
            <input type="checkbox" checked />
            <div class="customTextCheckbox customTextTypographyCheckbox"></div>
            Remove Fancy Typography
            <span>
              Standardises typography symbols (for example “ and ” become ")
            </span>
          </label>
          <label class="checkbox delimiterCheck">
            <input type="checkbox" />
            <div class="customTextCheckbox customTextDelimiterCheckbox"></div>
            Pipe Delimiter
            <span>
              Change how words are separated. Using the pipe delimiter allows
              you to randomize groups of words.
            </span>
          </label>
        </div>
        <div class="button apply">ok</div>
      </div>
    </div>
    <div id="wordFilterPopupWrapper" class="popupWrapper hidden">
      <div id="wordFilterPopup">
        <div class="group">
          <div class="title">language</div>
          <select class="languageInput" class=""></select>
        </div>
        <div class="group lengthgrid">
          <div class="title">min length</div>
          <div class="title">max length</div>
          <input
            class="wordLength wordMinInput"
            autocomplete="off"
            type="number"
          />
          <input
            class="wordLength wordMaxInput"
            autocomplete="off"
            type="number"
          />
        </div>
        <div class="group">
          <div class="title">include</div>
          <input class="wordIncludeInput" autocomplete="off" />
        </div>
        <div class="group">
          <div class="title">exclude</div>
          <input class="wordExcludeInput" autocomplete="off" />
        </div>
        <div class="tip">
          Use the above filters to include and exclude words or characters
          (separated by spaces)
        </div>
        <div class="tip">
          "Set" replaces the current custom word list with the filter result,
          "Add" appends the filter result to the current custom word list
        </div>
        <i
          class="fas fa-fw fa-spin fa-circle-notch hidden loadingIndicator"
        ></i>
        <div class="button" id="set">set</div>
        <div class="button">add</div>
      </div>
    </div>
    <div id="customWordAmountPopupWrapper" class="popupWrapper hidden">
      <div id="customWordAmountPopup">
        <div class="title">Word amount</div>
        <input type="number" value="1" min="1" max="10000" />
        <div class="tip">
          You can start an infinite test by inputting 0. Then, to stop the test,
          use the Bail Out feature (esc or ctrl/cmd + shift + p > Bail Out)
        </div>
        <div class="button">ok</div>
      </div>
    </div>
    <div id="customTestDurationPopupWrapper" class="popupWrapper hidden">
      <div id="customTestDurationPopup">
        <div class="title">Test duration</div>
        <div class="preview"></div>
        <input value="1" />
        <div class="tip">
          You can start an infinite test by inputting 0. Then, to stop the test,
          use the Bail Out feature (esc or ctrl/cmd + shift + p > Bail Out)
        </div>
        <div class="button">ok</div>
      </div>
    </div>
    <div id="quoteSearchPopupWrapper" class="popupWrapper hidden">
      <div id="quoteSearchPopup" mode="">
        <div id="quoteSearchTop">
          <div class="title">Quote Search</div>
          <div class="buttons">
            <div id="gotoSubmitQuoteButton" class="button">
              <i class="fas fa-plus"></i>
              Submit a quote
            </div>
            <div id="goToApproveQuotes" class="button">
              <i class="fas fa-check"></i>
              Approve quotes
            </div>
          </div>
        </div>
        <input
          id="searchBox"
          class="searchBox"
          type="text"
          maxlength="200"
          autocomplete="off"
        />
        <div id="extraResults">No search results</div>
        <div id="quoteSearchResults" class="quoteSearchResults"></div>
      </div>
    </div>
    <div id="quoteSubmitPopupWrapper" class="popupWrapper hidden">
      <div id="quoteSubmitPopup" mode="">
        <div class="title">Submit a Quote</div>
        <ul>
          <li>
            Do not include content that contains any libelous or otherwise
            unlawful, abusive or obscene text.
          </li>
          <li>Verify quotes added aren't duplicates of any already present</li>
          <li>
            Please do not add extremely short quotes (less than 60 characters)
          </li>
          <li>
            <b>
              Submitting low quality quotes or misusing this form will cause you
              to lose access to this feature
            </b>
          </li>
        </ul>
        <label>Quote</label>
        <div style="position: relative">
          <textarea
            id="submitQuoteText"
            class=""
            type="text"
            autocomplete="off"
          ></textarea>
          <div class="characterCount">-</div>
        </div>
        <label>Source</label>
        <input id="submitQuoteSource" class="" type="text" autocomplete="off" />
        <label>Language</label>
        <select name="language" id="submitQuoteLanguage"></select>
        <div
          class="g-recaptcha"
          data-sitekey="6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj"
        ></div>
        <div id="submitQuoteButton" class="button">Submit</div>
      </div>
    </div>
    <div id="quoteApprovePopupWrapper" class="popupWrapper hidden">
      <div id="quoteApprovePopup" mode="">
        <div class="top">
          <div class="title">Approve Quotes</div>
          <div class="button refreshList">
            <i class="fas fa-sync-alt"></i>
            Refresh list
          </div>
        </div>
        <div class="quotes"></div>
      </div>
    </div>
    <div id="tagsWrapper" class="popupWrapper hidden">
      <div id="tagsEdit" action="" tagid="">
        <div class="title"></div>
        <input type="text" />
        <div class="button"><i class="fas fa-plus"></i></div>
      </div>
    </div>
    <div id="resultEditTagsPanelWrapper" class="popupWrapper hidden">
      <div id="resultEditTagsPanel" resultid="">
        <div class="buttons"></div>
        <div class="button confirmButton"><i class="fas fa-check"></i></div>
      </div>
    </div>
    <div id="presetWrapper" class="popupWrapper hidden">
      <div id="presetEdit" action="" presetid="">
        <div class="title"></div>
        <input type="text" class="text" />
        <label class="checkbox">
          <input type="checkbox" />
          <div class="customTextCheckbox"></div>
          Change preset to current settings
        </label>
        <div class="button"><i class="fas fa-plus"></i></div>
      </div>
    </div>
    <div id="leaderboardsWrapper" class="hidden">
      <div id="leaderboards">
        <div class="leaderboardsTop">
          <div class="mainTitle">Leaderboards</div>
          <div class="subTitle">Next update in: --:--</div>
          <!-- <div class="buttons">
            <div class="buttonGroup">
              <div class="button active" board="time_15">time 15</div>
              <div class="button" board="time_60">time 60</div>
            </div>
          </div> -->
        </div>
        <div class="tables">
          <div class="titleAndTable">
            <div class="titleAndButtons">
              <div class="title">
                English Time 15
                <i
                  class="hidden leftTableLoader fas fa-fw fa-spin fa-circle-notch"
                ></i>
              </div>
              <div class="buttons">
                Jump to:
                <div class="button leftTableJumpToTop">
                  <i class="fas fa-fw fas fa-crown"></i>
                  <!-- Top -->
                </div>
                <div class="button leftTableJumpToMe">
                  <i class="fas fa-fw fas fa-user"></i>
                  <!-- Me -->
                </div>
              </div>
            </div>
            <div class="leftTableWrapper invisible">
              <table class="left">
                <thead>
                  <tr>
                    <td width="1%">#</td>
                    <td>name</td>
                    <td class="alignRight" width="15%">
                      wpm
                      <br />
                      <div class="sub">accuracy</div>
                    </td>
                    <td class="alignRight" width="15%">
                      raw
                      <br />
                      <div class="sub">consistency</div>
                    </td>
                    <td class="alignRight" width="13%">test</td>
                    <td class="alignRight" width="22%">date</td>
                  </tr>
                </thead>
                <tbody></tbody>
                <tfoot></tfoot>
              </table>
            </div>
          </div>
          <div class="titleAndTable">
            <div class="titleAndButtons">
              <div class="title">
                English Time 60
                <i
                  class="hidden rightTableLoader fas fa-fw fa-spin fa-circle-notch"
                ></i>
              </div>
              <div class="buttons">
                Jump to:
                <div class="button rightTableJumpToTop">
                  <i class="fas fa-fw fas fa-crown"></i>
                  <!-- Top -->
                </div>
                <div class="button rightTableJumpToMe">
                  <i class="fas fa-fw fas fa-user"></i>
                  <!-- Me -->
                </div>
              </div>
            </div>
            <div class="rightTableWrapper invisible">
              <table class="right">
                <thead>
                  <tr>
                    <td width="1%">#</td>
                    <td>name</td>
                    <td class="alignRight" width="15%">
                      wpm
                      <br />
                      <div class="sub">accuracy</div>
                    </td>
                    <td class="alignRight" width="15%">
                      raw
                      <br />
                      <div class="sub">consistency</div>
                    </td>
                    <td class="alignRight" width="13%">test</td>
                    <td class="alignRight" width="22%">date</td>
                  </tr>
                </thead>
                <tbody></tbody>
                <tfoot></tfoot>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div id="versionHistoryWrapper" class="hidden">
      <div id="versionHistory">
        <!-- <div class="tip">Click anywhere to dismiss</div> -->
        <div class="releases">
          <div class="release">
            <div class="title">v1</div>
            <div class="date">010101</div>
            <div class="body">test</div>
          </div>
          <div class="release">
            <div class="title">v2</div>
            <div class="date">010101</div>
            <div class="body">test</div>
          </div>
        </div>
      </div>
    </div>
    <div id="supportMeWrapper" class="popupWrapper hidden">
      <div id="supportMe">
        <div class="title">Support Monkeytype</div>
        <div class="text">
          Thank you so much for thinking about supporting this project. It would
          not be possible without you and your continued support.
          <i class="fas fa-heart"></i>
        </div>
        <div class="buttons">
          <div class="button ads">
            <div class="icon"><i class="fas fa-ad"></i></div>
            <div class="text">Enable Ads</div>
          </div>
          <a
            class="button"
            href="https://ko-fi.com/monkeytype"
            target="_blank"
            rel="noreferrer"
          >
            <div class="icon"><i class="fas fa-donate"></i></div>
            <div class="text">Donate</div>
          </a>
          <a
            class="button"
            href="https://www.patreon.com/monkeytype"
            target="_blank"
            rel="noreferrer"
          >
            <div class="icon"><i class="fab fa-patreon"></i></div>
            <div class="text">
              Become
              <br />
              a Patron
            </div>
          </a>
          <a
            class="button"
            href="https://monkeytype.store"
            target="_blank"
            rel="noreferrer"
          >
            <div class="icon"><i class="fas fa-tshirt"></i></div>
            <div class="text">Buy Merch</div>
          </a>
        </div>
      </div>
    </div>
    <div id="contactPopupWrapper" class="popupWrapper hidden">
      <div id="contactPopup">
        <div class="title">Contact</div>
        <div class="text">
          Feel free to send an email to jack@monkeytype.com (the buttons below
          will open the default mail client).
          <br />
          <br />
          Please
          <span>do not send</span>
          requests to delete account, update email, update name or clear
          personal bests - you can do that in the settings page.
        </div>
        <div class="buttons">
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Question] "
          >
            <div class="icon"><i class="fas fa-question-circle"></i></div>
            <div class="textGroup">
              <div class="text">Question</div>
              <!-- <div class="subtext">Confused about something?</div> -->
            </div>
          </a>
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Feedback] "
          >
            <div class="icon"><i class="fas fa-comment-dots"></i></div>
            <div class="textGroup">
              <div class="text">Feedback</div>
              <!-- <div class="subtext">Feel like the website could be improved?</div> -->
            </div>
          </a>
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Bug] "
          >
            <div class="icon"><i class="fas fa-bug"></i></div>
            <div class="textGroup">
              <div class="text">Bug Report</div>
              <!-- <div class="subtext">Report any bugs you found. You can also open a new issue on GitHub.</div> -->
            </div>
          </a>
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Account] "
          >
            <div class="icon"><i class="fas fa-user-circle"></i></div>
            <div class="textGroup">
              <div class="text">Account Help</div>
              <!-- <div class="subtext">Report any problems with your account like login issues.</div> -->
            </div>
          </a>
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Business] "
          >
            <div class="icon"><i class="fas fa-briefcase"></i></div>
            <div class="textGroup">
              <div class="text">Business Inquiry</div>
              <!-- <div class="subtext">Let's work together.</div> -->
            </div>
          </a>
          <a
            class="button"
            target="_blank"
            href="mailto:jack@monkeytype.com?subject=[Other] "
          >
            <div class="icon"><i class="fas fa-ellipsis-h"></i></div>
            <div class="textGroup">
              <div class="text">Other</div>
              <!-- <div class="subtext">We will try to help.</div> -->
            </div>
          </a>
        </div>
      </div>
    </div>
    <div id="commandLineWrapper" class="hidden">
      <div id="commandLine">
        <div
          style="
            display: grid;
            grid-template-columns: auto 1fr;
            align-items: center;
          "
        >
          <div class="searchicon">
            <i class="fas fa-search"></i>
          </div>
          <input type="text" class="input" placeholder="Type to search" />
        </div>
        <div class="separator hidden"></div>
        <div class="listTitle">Title</div>
        <div class="suggestions"></div>
      </div>
      <div id="commandInput" class="hidden">
        <input type="text" class="input" placeholder="input" />
      </div>
    </div>
    <div id="timerWrapper">
      <div id="timer" class="timerMain"></div>
    </div>
    <div style="display: flex; justify-content: space-around">
      <div id="centerContent" class="hidden">
        <div id="top">
          <div class="logo">
            <div class="icon">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                xmlns:xlink="http://www.w3.org/1999/xlink"
                style="isolation: isolate"
                viewBox="-680 -1030 300 180"
              >
                <g>
                  <path
                    d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
                  />
                  <path
                    d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
                  />
                  <path
                    d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
                  />
                  <path
                    d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
                  />
                  <path
                    d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
                  />
                  <path
                    d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
                  />
                  <path
                    d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
                  />
                  <path
                    d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
                  />
                  <path
                    d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
                  />
                </g>
              </svg>
            </div>
            <div class="text">
              <div class="top">monkey see</div>
              monkeytype
            </div>
          </div>
          <div id="menu">
            <a
              id="startTestButton"
              class="icon-button view-start"
              tabindex="2"
              href="/"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="fas fa-fw fa-keyboard"></i>
              </div>
            </a>
            <div
              class="icon-button leaderboards view-leaderboards"
              tabindex="2"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="fas fa-fw fa-crown"></i>
              </div>
            </div>
            <a
              class="icon-button view-about"
              tabindex="2"
              href="/about"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="fas fa-fw fa-info"></i>
              </div>
            </a>
            <!-- <a
              class="icon-button discord"
              tabindex="2"
              href="https://discord.gg/yENzqcB"
              onclick="this.blur();"
              target="_blank"
              style="text-decoration: none;"
            >
              <div class="icon">
                <i class="fab fa-discord"></i>
              </div>
            </a> -->
            <a
              class="icon-button view-settings"
              tabindex="2"
              href="/settings"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="fas fa-fw fa-cog"></i>
              </div>
            </a>
            <a
              class="icon-button hidden account view-account"
              tabindex="2"
              href="/account"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="fas fa-fw fa-user"></i>
              </div>
              <div class="text"></div>
            </a>
            <a
              class="icon-button login view-login"
              tabindex="2"
              href="/login"
              onclick="this.blur();"
            >
              <div class="icon">
                <i class="far fa-fw fa-user"></i>
              </div>
            </a>
          </div>

          <div class="config hidden">
            <div class="mobileConfig">
              <div class="icon-button">
                <i class="fas fa-bars"></i>
              </div>
            </div>
            <div class="desktopConfig">
              <div
                class="puncAndNum"
                style="
                  display: grid;
                  grid-auto-flow: column;
                  padding-top: 0.1rem;
                "
              >
                <div class="group punctuationMode">
                  <!--           <div class="title">time</div> -->
                  <div class="buttons">
                    <div class="text-button toggleButton" tabindex="2">
                      punctuation
                    </div>
                  </div>
                </div>
                <div class="group numbersMode">
                  <!--           <div class="title">time</div> -->
                  <div class="buttons">
                    <div class="text-button toggleButton" tabindex="2">
                      numbers
                    </div>
                  </div>
                </div>
              </div>
              <div class="group mode">
                <!--           <div class="title">mode</div> -->
                <div class="buttons">
                  <div class="text-button active" mode="time" tabindex="2">
                    time
                  </div>
                  <div class="text-button" mode="words" tabindex="2">words</div>
                  <div class="text-button" mode="quote" tabindex="2">quote</div>
                  <div class="text-button" mode="zen" tabindex="2">zen</div>
                  <div class="text-button" mode="custom" tabindex="2">
                    custom
                  </div>
                </div>
              </div>
              <div class="group wordCount hidden">
                <!--           <div class="title">words</div> -->
                <div class="buttons">
                  <div class="text-button" wordCount="10" tabindex="2">10</div>
                  <div class="text-button" wordCount="25" tabindex="2">25</div>
                  <div class="text-button active" wordCount="50" tabindex="2">
                    50
                  </div>
                  <div class="text-button" wordCount="100" tabindex="2">
                    100
                  </div>
                  <div class="text-button" wordCount="custom" tabindex="2">
                    <i class="fas fa-tools"></i>
                  </div>
                </div>
              </div>
              <div class="group time">
                <!--           <div class="title">time</div> -->
                <div class="buttons">
                  <div class="text-button" timeConfig="15" tabindex="2">15</div>
                  <div class="text-button active" timeConfig="30" tabindex="2">
                    30
                  </div>
                  <div class="text-button" timeConfig="60" tabindex="2">60</div>
                  <div class="text-button" timeConfig="120" tabindex="2">
                    120
                  </div>
                  <div class="text-button" timeConfig="custom" tabindex="2">
                    <i class="fas fa-tools"></i>
                  </div>
                </div>
              </div>
              <div class="group quoteLength hidden">
                <!--           <div class="title">time</div> -->
                <div class="buttons">
                  <div class="text-button" quoteLength="-1" tabindex="2">
                    all
                  </div>
                  <div class="text-button" quoteLength="0" tabindex="2">
                    short
                  </div>
                  <div class="text-button active" quoteLength="1" tabindex="2">
                    medium
                  </div>
                  <div class="text-button" quoteLength="2" tabindex="2">
                    long
                  </div>
                  <div class="text-button" quoteLength="3" tabindex="2">
                    thicc
                  </div>
                  <div class="text-button" quoteLength="-2" tabindex="2">
                    <i class="fas fa-search"></i>
                  </div>
                </div>
              </div>
              <div class="group customText hidden">
                <!--           <div class="title">time</div> -->
                <div class="buttons">
                  <div class="text-button">change</div>
                </div>
              </div>
            </div>
          </div>
          <div
            class="signOut hidden"
            style="grid-column: 3/4; grid-row: 1/2"
            tabindex="0"
          >
            <i class="fas fa-sign-out-alt"></i>
            sign out
          </div>
        </div>
        <div id="middle">
          <div class="page pageLoading hidden">
            <div class="preloader">
              <div class="icon">
                <i class="fas fa-fw fa-spin fa-circle-notch"></i>
              </div>
              <div class="barWrapper hidden">
                <div class="bar">
                  <div class="fill"></div>
                </div>
                <div class="text"></div>
              </div>
            </div>
          </div>
          <div class="page pageTest hidden">
            <div id="typingTest">
              <div id="capsWarning" class="hidden">
                <i class="fas fa-lock"></i>
                Caps Lock
              </div>
              <div id="memoryTimer">Time left to memorise all words: 0s</div>
              <div id="testModesNotice"></div>
              <input
                id="wordsInput"
                class=""
                tabindex="0"
                type="text"
                autocomplete="off"
                autocapitalize="off"
                autocorrect="off"
              />
              <div id="timerNumber" class="timerMain">
                <div>60</div>
              </div>
              <div id="miniTimerAndLiveWpm" class="timerMain size15">
                <div class="time hidden">1:00</div>
                <div class="wpm">60</div>
                <div class="acc">100%</div>
                <div class="burst">1</div>
              </div>
              <div class="outOfFocusWarning hidden">
                <i class="fas fa-mouse-pointer"></i>
                Click or press any key to focus
              </div>
              <div id="wordsWrapper" translate="no">
                <div id="paceCaret" class="default size15 hidden"></div>
                <div id="caret" class="default size15"></div>
                <div id="words" class="size15"></div>
              </div>
              <div class="keymap hidden">
                <div class="row r1">
                  <div></div>
                  <div class="keymap-key" id="Key1">
                    <span class="letter">1</span>
                  </div>
                  <div class="keymap-key" id="Key2">
                    <span class="letter">2</span>
                  </div>
                  <div class="keymap-key" id="Key3">
                    <span class="letter">3</span>
                  </div>
                  <div class="keymap-key" id="Key4">
                    <span class="letter">4</span>
                  </div>
                  <div class="keymap-key" id="Key5">
                    <span class="letter">5</span>
                  </div>
                  <div class="keymap-key" id="Key6">
                    <span class="letter">6</span>
                  </div>
                  <div class="keymap-split-spacer"></div>
                  <div class="keymap-key" id="Key7">
                    <span class="letter">7</span>
                  </div>
                  <div class="keymap-key" id="Key8">
                    <span class="letter">8</span>
                  </div>
                  <div class="keymap-key" id="Key9">
                    <span class="letter">9</span>
                  </div>
                  <div class="keymap-key" id="Key0">
                    <span class="letter">0</span>
                  </div>
                  <div class="keymap-key" id="Key-">
                    <span class="letter">-</span>
                  </div>
                  <div class="keymap-key" id="Key=">
                    <span class="letter">=</span>
                  </div>
                </div>
                <div class="row r2">
                  <div></div>
                  <div class="keymap-key" id="KeyQ">
                    <span class="letter">q</span>
                  </div>
                  <div class="keymap-key" id="KeyW">
                    <span class="letter">w</span>
                  </div>
                  <div class="keymap-key" id="KeyE">
                    <span class="letter">e</span>
                  </div>
                  <div class="keymap-key" id="KeyR">
                    <span class="letter">r</span>
                  </div>
                  <div class="keymap-key" id="KeyT">
                    <span class="letter">t</span>
                  </div>
                  <div class="keymap-split-spacer"></div>
                  <div class="keymap-key" id="KeyY">
                    <span class="letter">y</span>
                  </div>
                  <div class="keymap-key" id="KeyU">
                    <span class="letter">u</span>
                  </div>
                  <div class="keymap-key" id="KeyI">
                    <span class="letter">i</span>
                  </div>
                  <div class="keymap-key" id="KeyO">
                    <span class="letter">o</span>
                  </div>
                  <div class="keymap-key" id="KeyP">
                    <span class="letter">p</span>
                  </div>
                  <div class="keymap-key" id="KeyLeftBracket">
                    <span class="letter">[</span>
                  </div>
                  <div class="keymap-key" id="KeyRightBracket">
                    <span class="letter">]</span>
                  </div>
                  <div class="keymap-key hidden-key" id="Backslash">
                    <span class="letter">\</span>
                  </div>
                </div>
                <div class="row r3">
                  <div></div>
                  <div class="keymap-key" id="KeyA">
                    <span class="letter">a</span>
                  </div>
                  <div class="keymap-key" id="KeyS">
                    <span class="letter">s</span>
                  </div>
                  <div class="keymap-key" id="KeyD">
                    <span class="letter">d</span>
                  </div>
                  <div class="keymap-key" id="KeyF">
                    <span class="letter">f</span>
                    <div class="bump"></div>
                  </div>
                  <div class="keymap-key" id="KeyG">
                    <span class="letter">g</span>
                  </div>
                  <div class="keymap-split-spacer"></div>
                  <div class="keymap-key" id="KeyH">
                    <span class="letter">h</span>
                  </div>
                  <div class="keymap-key" id="KeyJ">
                    <span class="letter">j</span>
                    <div class="bump"></div>
                  </div>
                  <div class="keymap-key" id="KeyK">
                    <span class="letter">k</span>
                  </div>
                  <div class="keymap-key" id="KeyL">
                    <span class="letter">l</span>
                  </div>
                  <div class="keymap-key" id="KeySemicolon">
                    <span class="letter">;</span>
                  </div>
                  <div class="keymap-key" id="KeyQuote">
                    <span class="letter">'</span>
                  </div>
                </div>
                <div class="row r4">
                  <div></div>
                  <div class="keymap-key first" id="KeyZ">
                    <span class="letter">z</span>
                  </div>
                  <div class="keymap-key" id="KeyX">
                    <span class="letter">x</span>
                  </div>
                  <div class="keymap-key" id="KeyC">
                    <span class="letter">c</span>
                  </div>
                  <div class="keymap-key" id="KeyV">
                    <span class="letter">v</span>
                  </div>
                  <div class="keymap-key" id="KeyB">
                    <span class="letter">b</span>
                  </div>
                  <div class="keymap-key" id="KeyN">
                    <span class="letter">n</span>
                  </div>
                  <div class="keymap-split-spacer"></div>
                  <div class="extraKey">
                    <span class="letter"></span>
                  </div>
                  <div class="keymap-key" id="KeyM">
                    <span class="letter">m</span>
                  </div>
                  <div class="keymap-key" id="KeyComma">
                    <span class="letter">,</span>
                  </div>
                  <div class="keymap-key" id="KeyPeriod">
                    <span class="letter">.</span>
                  </div>
                  <div class="keymap-key" id="KeySlash">
                    <span class="letter">/</span>
                  </div>
                  <div class="keymap-key last">
                    <span class="letter"></span>
                  </div>
                </div>
                <div class="row r5">
                  <div></div>
                  <div class="keymap-key key-split-space" id="KeySpace">
                    <span class="letter"></span>
                  </div>
                  <div class="keymap-split-spacer"></div>
                  <div class="keymap-key key-split-space" id="KeySpace2">
                    <span class="letter"></span>
                  </div>
                  <div class="keymap-key hidden-key">
                    <span class="letter"></span>
                  </div>
                </div>
              </div>
              <div id="largeLiveWpmAndAcc" class="timerMain">
                <div id="liveWpm" class="hidden">123</div>
                <div id="liveAcc" class="hidden">100%%</div>
                <div id="liveBurst" class="hidden">1</div>
              </div>
              <div
                id="restartTestButton"
                aria-label="Restart Test"
                data-balloon-pos="down"
                class=""
                tabindex="0"
                onclick="this.blur();"
              >
                <i class="fas fa-fw fa-redo-alt"></i>
              </div>
              <div id="monkey" class="hidden">
                <div class="up"></div>
                <div class="left hidden"></div>
                <div class="right hidden"></div>
                <div class="both hidden"></div>
                <div class="fast">
                  <div class="up"></div>
                  <div class="left hidden"></div>
                  <div class="right hidden"></div>
                  <div class="both hidden"></div>
                </div>
              </div>
              <div id="premidTestMode" class="hidden"></div>
              <div id="premidSecondsLeft" class="hidden"></div>
            </div>
            <div id="result" tabindex="0" class="hidden">
              <div class="stats">
                <!-- <div class="info">words 10<br>punctuation</div> -->

                <div class="group wpm">
                  <div class="top">
                    <div class="text">wpm</div>
                    <div
                      class="crown hidden"
                      aria-label=""
                      data-balloon-pos="up"
                    >
                      <i class="fas fa-crown"></i>
                    </div>
                  </div>
                  <div class="bottom" aria-label="" data-balloon-pos="up">
                    -
                  </div>
                </div>
                <div class="group acc">
                  <div class="top">acc</div>
                  <div class="bottom" aria-label="" data-balloon-pos="up">
                    -
                  </div>
                </div>
              </div>
              <div class="stats morestats">
                <div class="group testType">
                  <div class="top">test type</div>
                  <div class="bottom">-</div>
                  <div class="tags hidden" style="margin-top: 0.5rem">
                    <div class="top">tags</div>
                    <div class="bottom">-</div>
                  </div>
                </div>
                <!-- <div class="group infoAndTags"> -->
                <div class="group info">
                  <div class="top">other</div>
                  <div class="bottom">-</div>
                </div>

                <!-- </div> -->
                <!-- <div class="subgroup"> -->
                <div class="group raw">
                  <div class="top">raw</div>
                  <div class="bottom" aria-label="" data-balloon-pos="up">
                    -
                  </div>
                </div>
                <div class="group key">
                  <div class="top">characters</div>
                  <div
                    class="bottom"
                    aria-label="correct, incorrect, extra, and missed"
                    data-balloon-break=""
                    data-balloon-pos="up"
                  >
                    -
                  </div>
                </div>

                <!-- </div> -->

                <!-- <div class="subgroup"> -->
                <div class="group flat consistency">
                  <div class="top">consistency</div>
                  <div class="bottom" aria-label="" data-balloon-pos="up">
                    2 -
                  </div>
                </div>
                <div class="group time">
                  <div class="top">time</div>
                  <div class="bottom" aria-label="" data-balloon-pos="up">
                    <div class="text">-</div>
                    <div class="afk"></div>
                    <div class="timeToday"></div>
                  </div>
                </div>
                <!-- </div> -->

                <div class="group source hidden">
                  <div class="top">
                    source
                    <span
                      id="rateQuoteButton"
                      class="icon-button hidden"
                      aria-label="Rate quote"
                      data-balloon-pos="up"
                      style="display: inline-block"
                    >
                      <i class="icon far fa-star"></i>
                      <span class="rating"></span>
                    </span>
                  </div>
                  <div class="bottom">-</div>
                </div>
              </div>
              <div class="chart">
                <!-- <div class="title">wpm over time</div> -->
                <canvas id="wpmChart"></canvas>
              </div>
              <div class="loginTip">
                <span class="link">Sign in</span>
                to save your result
              </div>
              <div class="bottom" style="grid-column: 1/3">
                <div id="resultWordsHistory" class="hidden">
                  <div class="title">
                    input history
                    <span
                      id="copyWordsListButton"
                      class="icon-button"
                      aria-label="Copy words list"
                      data-balloon-pos="up"
                      style="display: inline-block"
                    >
                      <i class="fas fa-fw fa-copy"></i>
                    </span>
                    <span
                      id="toggleBurstHeatmap"
                      class="icon-button"
                      aria-label="Toggle burst heatmap"
                      data-balloon-pos="up"
                      style="display: inline-block"
                    >
                      <i class="fas fa-fw fa-fire-alt"></i>
                    </span>
                    <div class="heatmapLegend hidden">
                      <div>slow</div>
                      <div class="boxes">
                        <div class="box"></div>
                        <div class="box"></div>
                        <div class="box"></div>
                        <div class="box"></div>
                        <div class="box"></div>
                      </div>
                      <div>fast</div>
                    </div>
                  </div>
                  <div class="words"></div>
                </div>
                <div id="resultReplay" class="hidden">
                  <div class="title">
                    watch replay
                    <span
                      id="playpauseReplayButton"
                      class="icon-button"
                      aria-label="Start replay"
                      data-balloon-pos="up"
                      style="display: inline-block"
                    >
                      <i class="fas fa-play"></i>
                    </span>
                    <p id="replayStopwatch">0s</p>
                  </div>
                  <div id="wordsWrapper">
                    <div id="replayWords" class="words"></div>
                  </div>
                </div>
                <div
                  id="retrySavingResultButton"
                  class="hidden"
                  tabindex="0"
                  onclick="this.blur();"
                >
                  <i class="fas fa-redo"></i>
                  Retry saving result
                </div>
                <div class="buttons">
                  <div
                    id="nextTestButton"
                    aria-label="Next test"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="fas fa-fw fa-chevron-right"></i>
                  </div>
                  <div
                    id="restartTestButtonWithSameWordset"
                    aria-label="Repeat test"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="fas fa-fw fa-sync-alt"></i>
                  </div>
                  <div
                    id="practiseWordsButton"
                    aria-label="Practice words"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="fas fa-fw fa-exclamation-triangle"></i>
                  </div>
                  <div
                    id="showWordHistoryButton"
                    aria-label="Toggle words history"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="fas fa-fw fa-align-left"></i>
                  </div>
                  <div
                    id="watchReplayButton"
                    aria-label="Watch replay"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="fas fa-fw fa-backward"></i>
                  </div>
                  <div
                    id="saveScreenshotButton"
                    aria-label="Save screenshot"
                    data-balloon-pos="down"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    <i class="far fa-fw fa-image"></i>
                  </div>
                </div>
              </div>
              <div class="ssWatermark hidden">monkeytype.com</div>
            </div>
          </div>
          <div class="page pageAbout hidden">
            <div class="scrollToTopButton invisible">
              <i class="fas fa-angle-double-up"></i>
            </div>
            <div class="created">
              Created with love by Miodec.
              <a href="#supporters_title">Supported</a>
              and
              <a href="#contributors_title">expanded</a>
              by many awesome people. Launched on 15th of May, 2020.
            </div>
            <div class="section">
              <div class="title">about</div>
              <!-- <h1>about</h1> -->
              <p>
                Monkeytype is a minimalistic typing test, featuring many test
                modes, an account system to save your typing speed history and
                user configurable features like themes, a smooth caret and more.
              </p>
            </div>
            <div class="section">
              <h1>word set</h1>
              <p>
                By default, this website uses the most common 200 words in the
                English language to generate its tests. You can change to an
                expanded set (1000 most common words) in the options, or change
                the language entirely.
              </p>
            </div>
            <div class="section">
              <h1>keybinds</h1>
              <p>
                You can use
                <key>tab</key>
                and
                <key>enter</key>
                (or just
                <key>tab</key>
                if you have quick tab mode enabled) to restart the typing test.
                Open the command line by pressing
                <key>ctrl/cmd</key>
                +
                <key>shift</key>
                +
                <key>p</key>
                or
                <key>esc</key>
                - there you can access all the functionality you need without
                touching your mouse
              </p>
            </div>
            <div class="section">
              <h1>stats</h1>
              <p>
                wpm - total amount of characters in the correctly typed words
                (including spaces), divided by 5 and normalised to 60 seconds.
              </p>
              <p>
                raw wpm - calculated just like wpm, but also includes incorrect
                words.
              </p>
              <p>acc - percentage of correctly pressed keys.</p>
              <p>
                char - correct characters / incorrect characters. Calculated
                after the test has ended.
              </p>
              <p>
                consistency - based on the variance of your raw wpm. Closer to
                100% is better. Calculated using the coefficient of variation of
                raw wpm and mapped onto a scale from 0 to 100.
              </p>
            </div>
            <div id="ad_about1" class="hidden"></div>
            <div class="section">
              <h1>results screen</h1>
              <p>
                After completing a test you will be able to see your wpm, raw
                wpm, accuracy, character stats, test length, leaderboards info
                and test info. (you can hover over some values to get floating
                point numbers). You can also see a graph of your wpm and raw
                over the duration of the test. Remember that the wpm line is a
                global average, while the raw wpm line is a local, momentary
                value. (meaning if you stop, the value is 0)
              </p>
            </div>
            <div class="section">
              <h1>bug report or feature request</h1>
              <p>
                If you encounter a bug, or have a feature request - join the
                Discord server, send me an email, a direct message on Twitter or
                create an issue on GitHub.
              </p>
            </div>
            <div class="section">
              <div class="title">support</div>
              <p>
                Thanks to everyone who has supported this project. It would not
                be possible without you and your continued support.
              </p>
              <div class="supportButtons">
                <a class="button ads">
                  <div class="fas fa-ad"></div>
                  Ads
                </a>
                <a
                  class="button"
                  href="https://ko-fi.com/monkeytype"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fas fa-donate"></div>
                  Ko-Fi
                </a>
                <a
                  class="button"
                  href="https://www.patreon.com/monkeytype"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fab fa-patreon"></div>
                  Patreon
                </a>
                <a
                  class="button"
                  href="https://monkeytype.store"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fas fa-tshirt"></div>
                  Merch
                </a>
              </div>
            </div>
            <div class="section">
              <div class="title">contact</div>
              <p>
                If you encounter a bug, have a feature request or just want to
                say hi - here are the different ways you can contact me
                directly.
              </p>
              <div class="contactButtons">
                <a
                  class="button"
                  href="mailto:jack@monkeytype.com"
                  target="_blank"
                >
                  <div class="fas fa-envelope"></div>
                  Mail
                </a>
                <a
                  class="button"
                  href="https://twitter.com/monkeytypegame"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fab fa-twitter"></div>
                  Twitter
                </a>
                <a
                  class="button"
                  href="https://discord.gg/monkeytype"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fab fa-discord"></div>
                  Discord
                </a>
                <a
                  class="button"
                  href="https://github.com/Miodec/monkeytype"
                  target="_blank"
                  rel="noreferrer"
                >
                  <div class="fab fa-github"></div>
                  GitHub
                </a>
              </div>
            </div>
            <div class="section">
              <div class="title">credits</div>
              <p>
                <a
                  href="https://www.reddit.com/user/montydrei"
                  target="_blank"
                  rel="noreferrer"
                >
                  Montydrei
                </a>
                for the name suggestion
              </p>
              <p>
                <a
                  href="https://www.reddit.com/r/MechanicalKeyboards/comments/gc6wx3/experimenting_with_a_completely_new_type_of/"
                  target="_blank"
                  rel="noreferrer"
                >
                  Everyone
                </a>
                who provided valuable feedback on the original reddit post for
                the prototype of this website
              </p>
              <p>
                <a href="#supporters_title">Supporters</a>
                who helped financially by donating, enabling optional ads or
                buying merch
              </p>
              <p>
                <a
                  href="https://github.com/Miodec/monkeytype/graphs/contributors"
                  target="_blank"
                  rel="noreferrer"
                >
                  Contributors
                </a>
                on GitHub that have helped with implementing various features,
                adding themes and more
              </p>
            </div>
            <div id="ad_about2" class="hidden"></div>
            <div class="section">
              <h1 id="supporters_title">supporters</h1>
              <div class="supporters"></div>
            </div>
            <div class="section">
              <h1 id="contributors_title">contributors</h1>
              <div class="contributors"></div>
            </div>
          </div>
          <div class="page pageSettings hidden">
            <div class="scrollToTopButton">
              <i class="fas fa-angle-double-up"></i>
            </div>
            <div class="tip">
              tip: You can also change all these settings quickly using the
              command line (
              <key>ctrl/cmd</key>
              +
              <key>shift</key>
              +
              <key>p</key>
              or
              <key>esc</key>
              )
            </div>
            <!-- <div class="sectionGroupTitle">quick navigation</div> -->
            <div class="settingsGroup quickNav">
              <div class="links">
                <a href="#group_account">account</a>
                <a href="#group_behavior">behavior</a>
                <a href="#group_input">input</a>
                <a href="#group_sound">sound</a>
                <a href="#group_caret">caret</a>
                <a href="#group_appearance">appearance</a>
                <a href="#group_theme">theme</a>
                <a href="#group_hideElements">hide elements</a>
                <a href="#group_dangerZone">danger zone</a>
              </div>
            </div>
            <div id="ad_settings0" class="hidden"></div>
            <div
              id="group_account"
              class="sectionGroupTitle hidden"
              group="account"
            >
              account
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup account hidden">
              <div class="section discordIntegration">
                <h1>discord integration</h1>
                <div class="text">
                  When you connect your monkeytype account to your Discord
                  account, you will be automatically assigned a new role every
                  time you achieve a new personal best in a 60 second test. If
                  you pair your accounts before joining the Discord server the
                  bot
                  <i>will not</i>
                  give you a role.
                </div>
                <div class="buttons">
                  <a
                    class="button"
                    href="https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=https%3A%2F%2Fmonkeytype.com%2Fverify&response_type=token&scope=identify"
                    style="text-decoration: none"
                  >
                    Link with Discord
                  </a>
                </div>
                <div class="info hidden">
                  <div>
                    <i class="fas fa-check"></i>
                    Your accounts are paired!
                  </div>
                  <div
                    id="unlinkDiscordButton"
                    class="text-button"
                    aria-label="Unlink"
                    data-balloon-pos="up"
                  >
                    <i class="fas fa-unlink" aria-hidden="true"></i>
                  </div>
                </div>
              </div>
              <div class="section tags">
                <h1>tags</h1>
                <div class="text">
                  With tags, you can compare how fast you're typing in different
                  situations. You can see your active tags above the test words.
                  They will remain active until you deactivate them, or refresh
                  the page.
                </div>
                <div class="tagsListAndButton">
                  <div class="tagsList">
                    <div class="tag" id="0">
                      <div class="active">
                        <i class="fas fa-check-square"></i>
                      </div>
                      <div class="title">staggered</div>
                      <div class="editButton"><i class="fas fa-pen"></i></div>
                      <div class="removeButton">
                        <i class="fas fa-trash"></i>
                      </div>
                    </div>
                  </div>
                  <div class="addTagButton"><i class="fas fa-plus"></i></div>
                </div>
              </div>
              <div class="section presets">
                <h1>presets</h1>
                <div class="text">
                  Create settings presets that can be applied with one click.
                  Remember to edit your preset if you make any changes - they
                  don't save on their own.
                </div>
                <div class="presetsListAndButton">
                  <div class="presetsList">
                    <div class="buttons preset" id="0">
                      <div class="presetButton button">
                        <div class="title">staggered</div>
                      </div>
                      <div class="editButton button">
                        <i class="fas fa-pen"></i>
                      </div>
                      <div class="removeButton button">
                        <i class="fas fa-trash"></i>
                      </div>
                    </div>
                  </div>
                  <div class="addPresetButton"><i class="fas fa-plus"></i></div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div id="group_behavior" class="sectionGroupTitle" group="behavior">
              behavior
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup behavior">
              <div class="section difficulty">
                <h1>test difficulty</h1>
                <div class="text">
                  Normal is the classic type test experience. Expert fails the
                  test if you submit (press space) an incorrect word. Master
                  fails if you press a single incorrect key (meaning you have to
                  achieve 100% accuracy).
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    difficulty="normal"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    normal
                  </div>
                  <div
                    class="button"
                    difficulty="expert"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    expert
                  </div>
                  <div
                    class="button"
                    difficulty="master"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    master
                  </div>
                </div>
              </div>
              <div class="section quickTab" id="quickTab">
                <h1>quick tab mode</h1>
                <div class="text">
                  Press
                  <key>tab</key>
                  to quickly restart the test, or to quickly jump to the test
                  page. This function disables tab navigation on the website.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>

              <div class="section repeatQuotes" id="repeatQuotes">
                <h1>repeat quotes</h1>
                <div class="text">
                  This setting changes the restarting behavior when typing in
                  quote mode. Changing it to 'typing' will repeat the quote if
                  you restart while typing.
                </div>
                <!-- , and 'always' will always repeat the quote (you will need to press <key>shift + tab</key> to move on to the next quote). -->
                <div class="buttons">
                  <div
                    class="button"
                    repeatQuotes="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    repeatQuotes="typing"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    typing
                  </div>
                  <!-- <div class="button" repeatQuotes="always" tabindex="0" onclick="this.blur();">
                    always
                  </div> -->
                </div>
              </div>

              <div class="section blindMode">
                <h1>blind mode</h1>
                <div class="text">
                  No errors or incorrect words are highlighted. Helps you to
                  focus on raw speed. If enabled, quick end is recommended.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    
                  </div>
                </div>
              </div>
              <!-- <div class="section readAheadMode">
                <h1>read ahead mode</h1>
                <div class="text">
                  When enabled, the active and immediately following test words
                  will be hidden. Helps you practice reading ahead.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div> -->
              <div class="section alwaysShowWordsHistory">
                <h1>always show words history</h1>
                <div class="text">
                  This option will automatically show the words history at the
                  end of the test. Can cause slight lag with a lot of words.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section singleListCommandLine">
                <h1>single list command line</h1>
                <div class="text">
                  When enabled, it will show the command line with all commands
                  in a single list instead of submenu arrangements. Selecting
                  'manual' will expose all commands only after typing
                  <key>></key>
                  .
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    singleListCommandLine="manual"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    manual
                  </div>
                  <div
                    class="button"
                    singleListCommandLine="on"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    on
                  </div>
                </div>
              </div>
              <div class="section minWpm" section="">
                <h1>min wpm</h1>
                <div class="text">
                  Automatically fails a test if your WPM falls below a
                  threshold.
                </div>
                <div>
                  <div class="inputAndButton">
                    <input
                      type="number"
                      placeholder="min wpm"
                      class="input customMinWpmSpeed"
                      tabindex="0"
                      min="0"
                      step="1"
                      value=""
                    />
                    <div
                      class="button save"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      <i class="fas fa-save fa-fw"></i>
                    </div>
                  </div>
                  <div class="buttons">
                    <div
                      class="button"
                      minWpm="off"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      off
                    </div>
                    <div
                      class="button"
                      minWpm="custom"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      custom
                    </div>
                  </div>
                </div>
              </div>
              <div class="section minAcc" section="">
                <h1>min accuracy</h1>
                <div class="text">
                  Automatically fails a test if your accuracy falls below a
                  threshold.
                </div>
                <div>
                  <div class="inputAndButton">
                    <input
                      type="number"
                      placeholder="min accuracy"
                      class="input customMinAcc"
                      tabindex="0"
                      min="0"
                      step="1"
                      value=""
                    />
                    <div
                      class="button save"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      <i class="fas fa-save fa-fw"></i>
                    </div>
                  </div>
                  <div class="buttons">
                    <div
                      class="button"
                      minAcc="off"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      off
                    </div>
                    <div
                      class="button"
                      minAcc="custom"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      custom
                    </div>
                  </div>
                </div>
              </div>
              <div class="section minBurst" section="">
                <h1>min burst</h1>
                <div class="text">
                  Automatically fails a test if your raw for a single word falls
                  below this threshold. Selecting 'flex' allows for this
                  threshold to automatically decrease for longer words.
                </div>
                <div>
                  <div class="inputAndButton">
                    <input
                      type="number"
                      placeholder="min burst"
                      class="input customMinBurst"
                      tabindex="0"
                      min="0"
                      step="1"
                      value=""
                    />
                    <div
                      class="button save"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      <i class="fas fa-save fa-fw"></i>
                    </div>
                  </div>
                  <div class="buttons">
                    <div
                      class="button"
                      minBurst="off"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      off
                    </div>
                    <div
                      class="button"
                      minBurst="fixed"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      fixed
                    </div>
                    <div
                      class="button"
                      minBurst="flex"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      flex
                    </div>
                  </div>
                </div>
              </div>
              <div class="section britishEnglish" section="">
                <h1>british english</h1>
                <div class="text">
                  When enabled, the website will use the British spelling
                  instead of American. Note that this might not replace all
                  words correctly. If you find any issues, please let us know.
                </div>
                <div>
                  <div class="buttons">
                    <div
                      class="button off active"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      off
                    </div>
                    <div class="button on" tabindex="0" onclick="this.blur();">
                      on
                    </div>
                  </div>
                </div>
              </div>
              <div class="section languageGroups fullWidth">
                <h1>language groups</h1>
                <div class="buttons"></div>
              </div>
              <div class="section language fullWidth">
                <h1>language</h1>
                <div class="buttons"></div>
              </div>
              <div class="section funbox fullWidth">
                <h1>funbox</h1>
                <div class="text">
                  These are special modes that change the website in some
                  special way (by altering the word generation, behavior of the
                  website or the looks). Give each one of them a try!
                </div>
                <div class="buttons"></div>
              </div>
              <div class="sectionSpacer"></div>
              <div class="section customLayoutfluid">
                <h1>custom layoutfluid</h1>
                <div class="text">
                  Select which layouts you want the layoutfluid funbox to cycle
                  through.
                </div>
                <div class="inputAndButton">
                  <input
                    type="text"
                    placeholder="layouts (separated by space)"
                    class="input"
                    tabindex="0"
                  />
                  <div class="button save" tabindex="0" onclick="this.blur();">
                    <i class="fas fa-save fa-fw"></i>
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>
            <div id="group_input" class="sectionGroupTitle" group="input">
              input
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup input">
              <div class="section freedomMode">
                <h1>freedom mode</h1>
                <div class="text">
                  Allows you to delete any word, even if it was typed correctly.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section strictSpace">
                <h1>strict space</h1>
                <div class="text">
                  Pressing space at the beginning of a word will insert a space
                  character when this mode is enabled.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section oppositeShiftMode">
                <h1>opposite shift mode</h1>
                <div class="text">
                  This mode will force you to use opposite
                  <key>shift</key>
                  keys for shifting. Using an incorrect one will count as an
                  error. This feature ignores keys in locations
                  <key>B</key>
                  ,
                  <key>Y</key>
                  , and
                  <key>^</key>
                  because many people use the other hand for those keys. If
                  you're using external software to emulate your layout
                  (including QMK), you should use the "keymap" mode - the
                  standard "on" will not work. This will enforce opposite shift
                  based on the "keymap layout" setting.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    oppositeShiftMode="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    oppositeShiftMode="on"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    on
                  </div>
                  <div
                    class="button"
                    oppositeShiftMode="keymap"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    keymap
                  </div>
                </div>
              </div>
              <div class="section stopOnError">
                <h1>stop on error</h1>
                <div class="text">
                  Letter mode will stop input when pressing any incorrect
                  letters. Word mode will not allow you to continue to the next
                  word until you correct all mistakes.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    stopOnError="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    stopOnError="word"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    word
                  </div>
                  <div
                    class="button"
                    stopOnError="letter"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    letter
                  </div>
                </div>
              </div>
              <div class="section confidenceMode">
                <h1>confidence mode</h1>
                <div class="text">
                  When enabled, you will not be able to go back to previous
                  words to fix mistakes. When turned up to the max, you won't be
                  able to backspace at all.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    confidenceMode="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>

                  <div
                    class="button"
                    confidenceMode="on"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    on
                  </div>
                  <div
                    class="button"
                    confidenceMode="max"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    max
                  </div>
                </div>
              </div>
              <div class="section quickEnd">
                <h1>quick end</h1>
                <div class="text">
                  This only applies to the words mode - when enabled, the test
                  will end as soon as the last word has been typed, even if it's
                  incorrect. When disabled, you need to manually confirm the
                  last incorrect entry with a space.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section indicateTypos" section="">
                <h1>indicate typos</h1>
                <div class="text">Shows typos underneath the letters.</div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section hideExtraLetters" section="">
                <h1>hide extra letters</h1>
                <div class="text">
                  Hides extra letters. This will completely avoid words jumping
                  lines (due to changing width), but might feel a bit confusing
                  when you press a key and nothing happens.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section swapEscAndTab" section="">
                <h1>swap esc and tab</h1>
                <div class="text">
                  Swap the behavior of tab and escape keys.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section lazyMode">
                <h1>lazy mode</h1>
                <div class="text">
                  Replaces accented letters with their normal equivalents.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section layout fullWidth">
                <h1>layout emulator</h1>
                <div class="text">
                  With this setting you can emulate other layouts. This setting
                  is best kept off, as it can break things like dead keys and
                  alt layers.
                </div>
                <div class="buttons"></div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div id="ad_settings1" class="hidden"></div>

            <div id="group_sound" class="sectionGroupTitle" group="sound">
              sound
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup sound">
              <div class="section soundVolume">
                <h1>sound volume</h1>
                <div class="text">Change the volume of the sound effects.</div>
                <div class="buttons">
                  <div
                    class="button"
                    soundVolume="0.1"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    quiet
                  </div>
                  <div
                    class="button"
                    soundVolume="0.5"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    medium
                  </div>
                  <div
                    class="button"
                    soundVolume="1.0"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    loud
                  </div>
                </div>
              </div>
              <div class="section playSoundOnClick fullWidth">
                <h1>play sound on click</h1>
                <div class="text">
                  Plays a short sound when you press a key.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    playSoundOnClick="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="1"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    click
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="2"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    beep
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="3"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    pop
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="4"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    nk creams
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="5"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    typewriter
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="6"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    osu
                  </div>
                  <div
                    class="button"
                    playSoundOnClick="7"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    hitmarker
                  </div>
                </div>
              </div>
              <div class="section playSoundOnError">
                <h1>play sound on error</h1>
                <div class="text">
                  Plays a short sound if you press an incorrect key or press
                  space too early.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div id="group_caret" class="sectionGroupTitle" group="caret">
              caret
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup caret">
              <div class="section smoothCaret" section="">
                <h1>smooth caret</h1>
                <div class="text">
                  The caret will move smoothly between letters and words.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section caretStyle" section="">
                <h1>caret style</h1>
                <div class="text">
                  Change the style of the caret during the test.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    caretStyle="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    caretStyle="default"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    |
                  </div>
                  <div
                    class="button"
                    caretStyle="block"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                     
                  </div>
                  <div
                    class="button"
                    caretStyle="outline"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                     
                  </div>
                  <div
                    class="button"
                    caretStyle="underline"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    _
                  </div>
                </div>
              </div>
              <div class="section paceCaret" section="">
                <h1>pace caret</h1>
                <div class="text">
                  Displays a second caret that moves at constant speed. The
                  'average' option averages the speed of last 10 results.
                </div>
                <div>
                  <div class="inputAndButton">
                    <input
                      type="number"
                      placeholder="wpm"
                      class="input customPaceCaretSpeed"
                      tabindex="0"
                      min="0"
                      step="1"
                      value=""
                    />
                    <div
                      class="button save"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      <i class="fas fa-save fa-fw"></i>
                    </div>
                  </div>
                  <div class="buttons">
                    <div
                      class="button"
                      paceCaret="off"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      off
                    </div>
                    <div
                      class="button"
                      paceCaret="average"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      average
                    </div>
                    <div
                      class="button"
                      paceCaret="pb"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      pb
                    </div>
                    <div
                      class="button"
                      paceCaret="custom"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      custom
                    </div>
                  </div>
                </div>
              </div>
              <div class="section repeatedPace" section="">
                <h1>repeated pace</h1>
                <div class="text">
                  When repeating a test, a pace caret will automatically be
                  enabled for one test with the speed of your previous test. It
                  does not override the pace caret if it's already enabled.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section paceCaretStyle" section="">
                <h1>pace caret style</h1>
                <div class="text">
                  Change the style of the pace caret during the test.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    paceCaretStyle="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    paceCaretStyle="default"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    |
                  </div>
                  <div
                    class="button"
                    paceCaretStyle="block"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                     
                  </div>
                  <div
                    class="button"
                    paceCaretStyle="outline"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                     
                  </div>
                  <div
                    class="button"
                    paceCaretStyle="underline"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    _
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div
              id="group_appearance"
              class="sectionGroupTitle"
              group="appearance"
            >
              appearance
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup appearance">
              <div class="section timerStyle" section="">
                <h1>timer/progress style</h1>
                <div class="text">
                  Change the style of the timer/progress during a timed test.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    timerStyle="bar"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    bar
                  </div>
                  <div
                    class="button"
                    timerStyle="text"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    text
                  </div>
                  <div
                    class="button"
                    timerStyle="mini"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    mini
                  </div>
                </div>
              </div>
              <div class="section timerColor" section="">
                <h1>timer/progress color</h1>
                <div class="text">
                  Change the color of the timer/progress number/bar and live wpm
                  number.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    timerColor="black"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    black
                  </div>
                  <div
                    class="button"
                    timerColor="sub"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    sub
                  </div>
                  <div
                    class="button"
                    timerColor="text"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    text
                  </div>
                  <div
                    class="button"
                    timerColor="main"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    main
                  </div>
                </div>
              </div>
              <div class="section timerOpacity" section="">
                <h1>timer/progress opacity</h1>
                <div class="text">
                  Change the opacity of the timer/progress number/bar and live
                  wpm number.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    timerOpacity="0.25"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    0.25
                  </div>
                  <div
                    class="button"
                    timerOpacity="0.5"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    0.5
                  </div>
                  <div
                    class="button"
                    timerOpacity="0.75"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    0.75
                  </div>
                  <div
                    class="button"
                    timerOpacity="1"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    1
                  </div>
                </div>
              </div>
              <div class="section highlightMode" section="">
                <h1>highlight mode</h1>
                <div class="text">
                  Change what is highlighted during the test.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    highlightMode="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    highlightMode="letter"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    letter
                  </div>
                  <div
                    class="button"
                    highlightMode="word"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    word
                  </div>
                </div>
              </div>
              <div class="section smoothLineScroll">
                <h1>smooth line scroll</h1>
                <div class="text">
                  When enabled, the line transition will be animated.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section showAllLines">
                <h1>show all lines</h1>
                <div class="text">
                  When enabled, the website will show all lines for word, custom
                  and quote mode tests - otherwise the lines will be limited to
                  3, and will automatically scroll. Using this could cause the
                  timer text and live wpm to not be visible.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section alwaysShowDecimalPlaces">
                <h1>always show decimal places</h1>
                <div class="text">
                  Always shows decimal places for values on the result page,
                  without the need to hover over the stats.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section alwaysShowCPM">
                <h1>always show cpm</h1>
                <div class="text">
                  Always shows characters per minute calculation instead of the
                  default words per minute calculation.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section startGraphsAtZero">
                <h1>start graphs at zero</h1>
                <div class="text">
                  Force graph axis to always start at zero, no matter what the
                  data is. Turning this off may exaggerate the value changes.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section fontSize">
                <h1>font size</h1>
                <div class="text">Change the font size of the test words.</div>
                <div class="buttons">
                  <div
                    class="button"
                    fontsize="1"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    1
                  </div>
                  <div
                    class="button"
                    fontsize="125"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    1.25
                  </div>
                  <div
                    class="button"
                    fontsize="15"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    1.5
                  </div>
                  <div
                    class="button"
                    fontsize="2"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    2
                  </div>
                  <div
                    class="button"
                    fontsize="3"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    3
                  </div>
                  <div
                    class="button"
                    fontsize="4"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    4
                  </div>
                </div>
              </div>
              <div class="section fontFamily fullWidth">
                <h1>font family</h1>
                <!-- <div class="text">Change the font family for the site</div> -->
                <div class="buttons"></div>
              </div>
              <div class="section pageWidth">
                <h1>page width</h1>
                <div class="text">Control the width of the content.</div>
                <div class="buttons">
                  <div
                    class="button"
                    pageWidth="100"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    100%
                  </div>
                  <div
                    class="button"
                    pageWidth="125"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    125%
                  </div>
                  <div
                    class="button"
                    pageWidth="150"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    150%
                  </div>
                  <div
                    class="button"
                    pageWidth="200"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    200%
                  </div>
                  <div
                    class="button"
                    pageWidth="max"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    Max
                  </div>
                </div>
              </div>
              <div class="section keymapMode">
                <h1>keymap</h1>
                <div class="text">
                  Displays your current layout while taking a test. React shows
                  what you pressed and Next shows what you need to press next.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    keymapMode="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    keymapMode="static"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    static
                  </div>
                  <div
                    class="button"
                    keymapMode="react"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    react
                  </div>
                  <div
                    class="button"
                    keymapMode="next"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    next
                  </div>
                </div>
              </div>
              <div class="section keymapStyle fullWidth">
                <h1>keymap style</h1>
                <!-- <div class="text">Displays the keymap in a different style.</div> -->
                <div class="buttons">
                  <div
                    class="button"
                    keymapStyle="staggered"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    staggered
                  </div>
                  <div
                    class="button"
                    keymapStyle="alice"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    alice
                  </div>
                  <div
                    class="button"
                    keymapStyle="matrix"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    matrix
                  </div>
                  <div
                    class="button"
                    keymapStyle="split"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    split
                  </div>
                  <div
                    class="button"
                    keymapStyle="split_matrix"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    split matrix
                  </div>
                </div>
              </div>
              <div class="section keymapLegendStyle fullWidth">
                <h1>keymap legend style</h1>
                <div class="buttons">
                  <div
                    class="button"
                    keymapLegendStyle="lowercase"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    lowercase
                  </div>
                  <div
                    class="button"
                    keymapLegendStyle="uppercase"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    uppercase
                  </div>
                  <div
                    class="button"
                    keymapLegendStyle="blank"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    blank
                  </div>
                </div>
              </div>
              <div class="section keymapLayout fullWidth">
                <h1>keymap layout</h1>
                <div class="buttons"></div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div id="ad_settings2" class="hidden"></div>

            <div id="group_theme" class="sectionGroupTitle" group="theme">
              theme
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup theme">
              <div class="section flipTestColors">
                <h1>flip test colors</h1>
                <div class="text">
                  By default, typed text is brighter than the future text. When
                  enabled, the colors will be flipped and the future text will
                  be brighter than the already typed text.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section colorfulMode">
                <h1>colorful mode</h1>
                <div class="text">
                  When enabled, the test words will use the main color, instead
                  of the text color, making the website more colorful.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    off
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    on
                  </div>
                </div>
              </div>
              <div class="section customBackgroundSize">
                <h1>custom background</h1>
                <div class="text">
                  Set an image url to be a custom background image. Cover fits
                  the image to cover the screen. Contain fits the image to be
                  fully visible. Max fits the image corner to corner.
                </div>
                <div>
                  <div class="inputAndButton">
                    <input
                      type="text"
                      placeholder="image url"
                      class="input"
                      tabindex="0"
                      onClick="this.select();"
                    />
                    <div
                      class="button save"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      <i class="fas fa-save fa-fw"></i>
                    </div>
                  </div>
                  <div class="buttons">
                    <div
                      class="button"
                      customBackgroundSize="cover"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      cover
                    </div>
                    <div
                      class="button"
                      customBackgroundSize="contain"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      contain
                    </div>
                    <div
                      class="button"
                      customBackGroundSize="max"
                      tabindex="0"
                      onclick="this.blur();"
                    >
                      max
                    </div>
                  </div>
                </div>
              </div>
              <div class="section customBackgroundFilter fullWidth">
                <h1>custom background filter</h1>
                <div class="text">
                  Apply various effects to the custom background.
                </div>
                <div class="groups">
                  <div class="group blur">
                    <div class="title">blur</div>
                    <div class="value"></div>
                    <input type="range" min="0" max="5" value="0" step="0.1" />
                  </div>
                  <div class="group brightness">
                    <div class="title">brightness</div>
                    <div class="value"></div>
                    <input type="range" min="0" max="2" value="1" step="0.1" />
                  </div>
                  <div class="group saturate">
                    <div class="title">saturate</div>
                    <div class="value"></div>
                    <input type="range" min="0" max="2" value="1" step="0.1" />
                  </div>
                  <div class="group opacity">
                    <div class="title">opacity</div>
                    <div class="value"></div>
                    <input type="range" min="0" max="1" value="1" step="0.1" />
                  </div>
                  <div class="saveContainer">
                    <div class="save button" style="grid-column: 3">
                      <!-- <i class="fas fa-save fa-fw"></i> -->
                      save
                    </div>
                  </div>
                </div>
              </div>
              <div class="section randomTheme fullWidth">
                <h1>randomize theme</h1>
                <div class="text">
                  After completing a test, the theme will be set to a random
                  one. The random themes are not saved to your config. If set to
                  'fav' only favourite themes will be randomized. If set to
                  'light' or 'dark', only presets with light or dark background
                  colors will be randomized, respectively.
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    randomTheme="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    randomTheme="on"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    on
                  </div>
                  <div
                    class="button"
                    randomTheme="fav"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    favorite
                  </div>
                  <div
                    class="button"
                    randomTheme="light"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    light
                  </div>
                  <div
                    class="button"
                    randomTheme="dark"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    dark
                  </div>
                </div>
              </div>
              <div class="section themes fullWidth">
                <h1>theme</h1>
                <div class="tabs">
                  <div
                    class="button"
                    tab="preset"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    preset
                  </div>
                  <div
                    class="button"
                    tab="custom"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    custom
                  </div>
                </div>
                <!-- <div class='tabs'>
                  <button tab="preset" class="tab">preset</button>
                  <button tab="custom" class="tab">custom</button>
                </div> -->

                <!-- Used to convert colors to hex  -->
                <div class="colorConverter"></div>

                <div class="tabContainer">
                  <div
                    tabContent="custom"
                    class="tabContent section customTheme hidden"
                  >
                    <label class="colorText">background</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--bg-color-txt"
                      />
                      <label
                        for="--bg-color"
                        class="button"
                        style="
                          color: var(--text-color);
                          background: rgba(0, 0, 0, 0.1);
                        "
                      >
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--bg-color"
                      />
                    </div>
                    <label class="colorText">main</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--main-color-txt"
                      />
                      <label for="--main-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--main-color"
                      />
                    </div>
                    <label class="colorText">caret</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--caret-color-txt"
                      />
                      <label for="--caret-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--caret-color"
                      />
                    </div>
                    <label class="colorText">sub</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--sub-color-txt"
                      />
                      <label for="--sub-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--sub-color"
                      />
                    </div>
                    <label class="colorText">text</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--text-color-txt"
                      />
                      <label for="--text-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--text-color"
                      />
                    </div>

                    <span class="spacer"></span>

                    <label class="colorText">error</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--error-color-txt"
                      />
                      <label for="--error-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--error-color"
                      />
                    </div>
                    <label class="colorText">extra error</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--error-extra-color-txt"
                      />
                      <label for="--error-extra-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--error-extra-color"
                      />
                    </div>

                    <p>colorful mode</p>

                    <label class="colorText">error</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--colorful-error-color-txt"
                      />
                      <label for="--colorful-error-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--colorful-error-color"
                      />
                    </div>

                    <label class="colorText">extra error</label>
                    <div class="colorPicker inputAndButton">
                      <input
                        type="text"
                        value="#000000"
                        class="input"
                        id="--colorful-error-extra-color-txt"
                      />
                      <label for="--colorful-error-extra-color" class="button">
                        <i class="fas fa-fw fa-palette"></i>
                      </label>
                      <input
                        type="color"
                        class="color"
                        value="#000000"
                        id="--colorful-error-extra-color"
                      />
                    </div>
                    <!-- <div
                      style="
                        display: grid;
                        gap: 2rem;
                        grid-auto-flow: column;
                        grid-column: 1/5;
                      "
                    > -->
                    <div
                      class="button"
                      id="loadCustomColorsFromPreset"
                      style="grid-column: 1/3"
                    >
                      load from preset
                    </div>

                    <div class="button" id="shareCustomThemeButton">share</div>
                    <div class="button saveCustomThemeButton">save</div>
                    <!-- </div> -->
                  </div>
                  <div tabContent="preset" class="tabContent">
                    <div class="favThemes buttons"></div>
                    <div class="allThemes buttons"></div>
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div
              id="group_hideElements"
              class="sectionGroupTitle"
              group="hideElements"
            >
              hide elements
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup hideElements">
              <div class="section showLiveWpm">
                <h1>live wpm</h1>
                <div class="text">
                  Displays a live WPM speed during the test. Updates once every
                  second.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section showLiveAcc">
                <h1>live accuracy</h1>
                <div class="text">Displays live accuracy during the test.</div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section showLiveBurst">
                <h1>live burst</h1>
                <div class="text">
                  Displays live burst during the test of the last word you
                  typed.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section showTimerProgress">
                <h1>timer/progress</h1>
                <div class="text">
                  Displays a live timer for timed tests and progress for
                  words/custom tests.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section showKeyTips">
                <h1>key tips</h1>
                <div class="text">
                  Shows the keybind tips at the bottom of the page.
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section showOutOfFocusWarning">
                <h1>out of focus warning</h1>
                <div class="text">
                  Shows an out of focus reminder after 1 second of being 'out of
                  focus' (not being able to type).
                </div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="section capsLockWarning">
                <h1>caps lock warning</h1>
                <div class="text">Displays a warning when caps lock is on.</div>
                <div class="buttons">
                  <div class="button off" tabindex="0" onclick="this.blur();">
                    hide
                  </div>
                  <div class="button on" tabindex="0" onclick="this.blur();">
                    show
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>

            <div
              id="group_dangerZone"
              class="sectionGroupTitle"
              group="dangerZone"
            >
              danger zone
              <i class="fas fa-chevron-down"></i>
            </div>
            <div class="settingsGroup dangerZone">
              <div class="section importexportSettings">
                <h1>import/export settings</h1>
                <div class="text">Import or export the settings as JSON.</div>
                <div class="buttons">
                  <div
                    class="button off"
                    id="importSettingsButton"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    import
                  </div>
                  <div
                    class="button off"
                    id="exportSettingsButton"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    export
                  </div>
                </div>
              </div>
              <div class="section enableAds">
                <h1>enable ads</h1>
                <div class="text">
                  If you wish to support me without directly donating you can
                  enable ads that will be visible at the bottom of the screen.
                  Sellout mode also shows ads on both sides of the screen.
                  <br />
                  <br />
                  (changes will take effect after a refresh).
                </div>
                <div class="buttons">
                  <div
                    class="button"
                    enableAds="off"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    off
                  </div>
                  <div
                    class="button"
                    enableAds="on"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    on
                  </div>
                  <div
                    class="button"
                    enableAds="max"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    sellout
                  </div>
                </div>
              </div>
              <div class="section resetSettings">
                <h1>reset settings</h1>
                <div class="text">
                  Resets settings to the default (but doesn't touch your tags).
                  Warning: you can't undo this action!
                </div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="resetSettingsButton"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    reset settings
                  </div>
                </div>
              </div>
              <div class="section resetPersonalBests needsAccount hidden">
                <h1>reset personal bests</h1>
                <div class="text">
                  Resets all your personal bests (but doesn't delete any tests
                  from your history). Warning: you can't undo this action!
                </div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="resetPersonalBestsButton"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    reset personal bests
                  </div>
                </div>
              </div>
              <div class="section updateAccountName needsAccount hidden">
                <h1>update account name</h1>
                <div class="text">
                  Change the name of your account. You can only do this once
                  every 30 days.
                </div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="updateAccountName"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    update name
                  </div>
                </div>
              </div>
              <div class="section passwordAuthSettings needsAccount hidden">
                <h1>password authentication settings</h1>
                <div class="text">
                  Add password authentication, update your password or email.
                </div>
                <div class="buttons vertical">
                  <div
                    class="button danger"
                    id="addPasswordAuth"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    add password authentication
                  </div>
                  <div
                    class="button danger"
                    id="emailPasswordAuth"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    update email
                  </div>
                  <div
                    class="button danger"
                    id="passPasswordAuth"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    update password
                  </div>
                </div>
              </div>
              <div class="section googleAuthSettings needsAccount hidden">
                <h1>google authentication settings</h1>
                <div class="text">Add or remove Google authentication.</div>
                <div class="buttons vertical">
                  <div
                    class="button danger"
                    id="addGoogleAuth"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    add google authentication
                  </div>
                  <div
                    class="button danger"
                    id="removeGoogleAuth"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    remove google authentication
                  </div>
                </div>
              </div>
              <!-- <div class="section updateAccountEmail needsAccount hidden">
                <h1>update account email</h1>
                <div class="text">
                  In case you misspell it or get a new address.
                </div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="updateAccountEmail"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    update email
                  </div>
                </div>
              </div>
              <div class="section updateAccountPassword needsAccount hidden">
                <h1>update account password</h1>
                <div class="text">Change the password you use to sign in.</div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="updateAccountPassword"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    update password
                  </div>
                </div>
              </div> -->
              <div class="section deleteAccount needsAccount hidden">
                <h1>delete account</h1>
                <div class="text">
                  Deletes your account and all data connected to it.
                </div>
                <div class="buttons">
                  <div
                    class="button off danger"
                    id="deleteAccount"
                    tabindex="0"
                    onclick="this.blur();"
                  >
                    delete account
                  </div>
                </div>
              </div>
              <div class="sectionSpacer"></div>
            </div>
          </div>

          <div class="page pageLogin hidden">
            <div class="preloader hidden">
              <i class="fas fa-fw fa-spin fa-circle-notch"></i>
            </div>
            <div class="register side">
              <div class="title">register</div>
              <form action="" autocomplete="nope">
                <input
                  type="text"
                  placeholder="username"
                  autocomplete="new-username"
                />
                <input
                  type="email"
                  placeholder="email"
                  autocomplete="new-email"
                />
                <input
                  type="email"
                  placeholder="verify email"
                  autocomplete="verify-email"
                />
                <input
                  type="password"
                  placeholder="password"
                  autocomplete="new-password"
                  name="new-password"
                />
                <input
                  type="password"
                  placeholder="verify password"
                  autocomplete="verify-password"
                  name="verify-password"
                />
                <div class="button">
                  <i class="fas fa-user-plus"></i>
                  Sign Up
                </div>
              </form>
            </div>
            <div class="login side">
              <div class="title">login</div>
              <div id="forgotPasswordButton">Forgot password?</div>
              <form action="">
                <input
                  name="current-email"
                  type="email"
                  placeholder="email"
                  autocomplete="current-username"
                />
                <input
                  name="current-password"
                  type="password"
                  placeholder="password"
                  autocomplete="current-password"
                />
                <div>
                  <label id="rememberMe" class="checkbox">
                    <input type="checkbox" checked />
                    <div class="customTextCheckbox"></div>
                    Remember me
                  </label>
                </div>
                <div class="button signIn">
                  <i class="fas fa-sign-in-alt"></i>
                  Sign In
                </div>
                <div style="font-size: 0.75rem; text-align: center">or</div>
                <div class="button signInWithGoogle">
                  <i class="fab fa-google"></i>
                  Google Sign In
                </div>
                <!-- <div class="button signInWithGitHub">
                  <i class="fab fa-github"></i>
                  GitHub Sign In
                </div> -->
              </form>
            </div>
          </div>

          <div class="page pageAccount hidden">
            <div class="scrollToTopButton">
              <i class="fas fa-angle-double-up"></i>
            </div>
            <div class="preloader">
              <div class="icon">
                <i class="fas fa-fw fa-spin fa-circle-notch"></i>
              </div>
              <div class="barWrapper hidden">
                <div class="bar">
                  <div class="fill"></div>
                </div>
                <div class="text"></div>
              </div>
            </div>
            <div class="content hidden">
              <div class="miniResultChartWrapper">
                <canvas id="miniResultChart"></canvas>
              </div>
              <div class="miniResultChartBg"></div>
              <div class="triplegroup">
                <div class="group globalTestsStarted">
                  <div class="title">tests started</div>
                  <div class="val">-</div>
                </div>
                <div class="group globalTestsCompleted">
                  <div class="title">tests completed</div>
                  <div class="val">-</div>
                </div>
                <div class="group globalTimeTyping">
                  <div class="title">time typing</div>
                  <div class="val">-</div>
                </div>
              </div>
              <div class="group createdDate">Account created on -</div>
              <div class="group personalBestTables">
                <div class="title">personal bests</div>
                <div class="tables">
                  <div class="titleAndTable timePbTable">
                    <table width="100%">
                      <thead>
                        <tr>
                          <td width="1%">time</td>
                          <td width="33%">
                            wpm
                            <br />
                            <span class="sub">accuracy</span>
                          </td>
                          <td width="33%">
                            raw
                            <br />
                            <span class="sub">consistency</span>
                          </td>
                          <td width="33%">date</td>
                        </tr>
                      </thead>
                      <tbody>
                        <tr>
                          <td>15</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>30</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>60</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>120</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                      </tbody>
                    </table>
                    <div class="button showAllTimePbs">show all</div>
                  </div>
                  <div class="titleAndTable wordsPbTable">
                    <table width="100%">
                      <thead>
                        <tr>
                          <td width="1%">words</td>
                          <td width="33%">
                            wpm
                            <br />
                            <span class="sub">accuracy</span>
                          </td>
                          <td width="33%">
                            raw
                            <br />
                            <span class="sub">consistency</span>
                          </td>
                          <td width="33%">date</td>
                        </tr>
                      </thead>
                      <tbody>
                        <tr>
                          <td>10</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>25</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>50</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                        <tr>
                          <td>100</td>
                          <td>-</td>
                          <td>-</td>
                          <td>-</td>
                        </tr>
                      </tbody>
                    </table>
                    <div class="button showAllWordsPbs">show all</div>
                  </div>
                </div>
              </div>

              <div id="ad_account" class="hidden"></div>

              <div class="group topFilters">
                <!-- <div
                  class="button"
                  id="currentConfigFilter"
                  style="grid-column: 1/3;"
                >
                  set filters to current settings
                </div> -->
                <div class="buttonsAndTitle" style="grid-column: 1/3">
                  <div class="title">filters</div>
                  <div class="buttons">
                    <div class="button allFilters">all</div>
                    <div class="button currentConfigFilter">
                      current settings
                    </div>
                    <div class="button toggleAdvancedFilters">advanced</div>
                  </div>
                </div>
                <div
                  class="buttonsAndTitle testDate"
                  style="grid-column: 1/3; margin-top: 1rem"
                >
                  <!-- <div class="title">date</div> -->
                  <div class="buttons filterGroup" group="date">
                    <div class="button" filter="last_day">last day</div>
                    <div class="button" filter="last_week">last week</div>
                    <div class="button" filter="last_month">last month</div>
                    <div class="button" filter="last_3months">
                      last 3 months
                    </div>
                    <div class="button" filter="all">all time</div>
                  </div>
                </div>
              </div>
              <div class="group filterButtons" style="display: none">
                <div class="buttonsAndTitle" style="grid-column: 1/3">
                  <div class="title">advanced filters</div>
                  <div class="buttons">
                    <div class="button noFilters">clear filters</div>
                  </div>
                </div>
                <div class="buttonsAndTitle" style="grid-column: 1/3">
                  <div class="title">difficulty</div>
                  <div class="buttons filterGroup" group="difficulty">
                    <div class="button" filter="normal">normal</div>
                    <div class="button" filter="expert">expert</div>
                    <div class="button" filter="master">master</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">mode</div>
                  <div class="buttons filterGroup" group="mode">
                    <div class="button" filter="words">words</div>
                    <div class="button" filter="time">time</div>
                    <div class="button" filter="quote">quote</div>
                    <div class="button" filter="zen">zen</div>
                    <div class="button" filter="custom">custom</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">quote length</div>
                  <div class="buttons filterGroup" group="quoteLength">
                    <div class="button" filter="short">short</div>
                    <div class="button" filter="medium">medium</div>
                    <div class="button" filter="long">long</div>
                    <div class="button" filter="thicc">thicc</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">words</div>
                  <div class="buttons filterGroup" group="words">
                    <div class="button" filter="10">10</div>
                    <div class="button" filter="25">25</div>
                    <div class="button" filter="50">50</div>
                    <div class="button" filter="100">100</div>
                    <div class="button" filter="custom">custom</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">time</div>
                  <div class="buttons filterGroup" group="time">
                    <div class="button" filter="15">15</div>
                    <div class="button" filter="30">30</div>
                    <div class="button" filter="60">60</div>
                    <div class="button" filter="120">120</div>
                    <div class="button" filter="custom">custom</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">punctuation</div>
                  <div class="buttons filterGroup" group="punctuation">
                    <div class="button" filter="on">on</div>
                    <div class="button" filter="off">off</div>
                  </div>
                </div>
                <div class="buttonsAndTitle">
                  <div class="title">numbers</div>
                  <div class="buttons filterGroup" group="numbers">
                    <div class="button" filter="on">on</div>
                    <div class="button" filter="off">off</div>
                  </div>
                </div>
                <div class="buttonsAndTitle tags" style="grid-column: 1/3">
                  <div class="title">tags</div>
                  <div class="buttons filterGroup" group="tags"></div>
                </div>
                <div class="buttonsAndTitle languages" style="grid-column: 1/3">
                  <div class="title">language</div>
                  <div class="buttons filterGroup" group="language"></div>
                </div>
                <div class="buttonsAndTitle funbox" style="grid-column: 1/3">
                  <div class="title">funbox</div>
                  <div class="buttons filterGroup" group="funbox"></div>
                </div>
              </div>
              <div class="group noDataError hidden">
                No data found. Check your filters.
              </div>
              <div class="group chart">
                <!-- <div class="chartPreloader">
                <i class="fas fa-fw fa-spin fa-circle-notch"></i>
              </div> -->
                <div class="above"></div>
                <div class="chart">
                  <canvas id="accountHistoryChart"></canvas>
                </div>
                <div class="below">
                  <div class="text"></div>
                  <div class="buttons">
                    <div class="toggleAccuracyOnChart button">
                      <i class="fas fa-bullseye"></i>
                      Toggle Accuracy
                    </div>
                    <div class="toggleChartStyle button">
                      <i class="fas fa-chart-line"></i>
                      Toggle Chart Style
                    </div>
                  </div>
                </div>
              </div>
              <div class="group dailyActivityChart">
                <div class="chart" style="height: 200px">
                  <canvas id="accountActivityChart"></canvas>
                </div>
              </div>
              <div class="triplegroup stats">
                <div class="group testsStarted">
                  <div class="title">tests started</div>
                  <div class="val">-</div>
                </div>
                <div class="group testsCompleted">
                  <div class="title">
                    tests completed
                    <span
                      data-balloon-length="xlarge"
                      aria-label="Due to the increasing number of results in the database, you can now only see your last 1000 results in detail. Total time spent typing, started and completed tests stats will still be up to date at the top of the page, above the filters."
                      data-balloon-pos="up"
                    >
                      <i class="fas fa-question-circle"></i>
                    </span>
                  </div>
                  <div class="val">-</div>
                  <div class="avgres">-</div>
                </div>
                <div class="group timeTotalFiltered">
                  <div class="title">time typing</div>
                  <div class="val">-</div>
                </div>
                <div class="group highestWpm">
                  <div class="title">highest wpm</div>
                  <div class="val">-</div>
                  <div class="mode"></div>
                </div>
                <div class="group averageWpm">
                  <div class="title">average wpm</div>
                  <div class="val">-</div>
                </div>
                <div class="group averageWpm10">
                  <div class="title">
                    average wpm
                    <br />
                    (last 10 tests)
                  </div>
                  <div class="val">-</div>
                </div>

                <div class="group highestRaw">
                  <div class="title">highest raw wpm</div>
                  <div class="val">-</div>
                  <div class="mode"></div>
                </div>
                <div class="group averageRaw">
                  <div class="title">average raw wpm</div>
                  <div class="val">-</div>
                </div>
                <div class="group averageRaw10">
                  <div class="title">
                    average raw wpm
                    <br />
                    (last 10 tests)
                  </div>
                  <div class="val">-</div>
                </div>
                <div class="emptygroup"></div>
                <div class="group avgAcc">
                  <div class="title">avg accuracy</div>
                  <div class="val">-</div>
                </div>
                <div class="group avgAcc10">
                  <div class="title">
                    avg accuracy
                    <br />
                    (last 10 tests)
                  </div>
                  <div class="val">-</div>
                </div>
                <div class="emptygroup"></div>
                <div class="group avgCons">
                  <div class="title">avg consistency</div>
                  <div class="val">-</div>
                </div>
                <div class="group avgCons10">
                  <div class="title">
                    avg consistency
                    <br />
                    (last 10 tests)
                  </div>
                  <div class="val">-</div>
                </div>
                <!-- <div class="group favouriteTest">
                <div class="title">favourite test</div>
                <div class="val">words 10</div>
              </div> -->
              </div>
              <div class="group history">
                <!-- <div class="title">result history</div> -->
                <table width="100%">
                  <thead>
                    <tr>
                      <td></td>
                      <td type="button" class="sortable history-wpm-header">
                        wpm
                      </td>
                      <td type="button" class="sortable history-raw-header">
                        raw
                      </td>
                      <td type="button" class="sortable history-acc-header">
                        accuracy
                      </td>
                      <td
                        type="button"
                        class="sortable history-consistency-header"
                      >
                        consistency
                      </td>
                      <td
                        aria-label="correct/incorrect/extra/missed"
                        data-balloon-pos="up"
                      >
                        chars
                      </td>
                      <td>mode</td>
                      <!-- <td>punctuation</td> -->
                      <td>info</td>
                      <td>tags</td>
                      <td type="button" class="sortable history-date-header">
                        date
                        <i class="fas fa-sort-down" aria-hidden="true"></i>
                      </td>
                    </tr>
                  </thead>
                  <tbody></tbody>
                </table>
                <div class="loadMoreButton">load more</div>
              </div>
            </div>
          </div>
        </div>
        <div id="bottom">
          <div id="commandLineMobileButton">
            <i class="fas fa-terminal"></i>
          </div>
          <div class="keyTips">
            <key>tab</key>
            and
            <key>enter</key>
            - Restart Test
            <br />
            <key>ctrl/cmd</key>
            +
            <key>shift</key>
            +
            <key>p</key>
            or
            <key>esc</key>
            - Command Line
          </div>
          <div class="leftright">
            <div class="left">
              <a id="contactPopupButton">
                <i class="fas fa-fw fa-envelope"></i>
                <div class="text">Contact</div>
              </a>
              <a
                href="https://github.com/Miodec/monkeytype"
                target="_blank"
                rel="noreferrer"
              >
                <i class="fas fa-fw fa-code"></i>
                <div class="text">GitHub</div>
              </a>
              <a
                href="https://www.discord.gg/monkeytype"
                target="_blank"
                rel="noreferrer"
                class="discordLink"
              >
                <i class="fab fa-fw fa-discord"></i>
                <div class="text">Discord</div>
              </a>
              <a
                href="https://twitter.com/monkeytypegame"
                target="_blank"
                rel="noreferrer"
              >
                <i class="fab fa-fw fa-twitter"></i>
                <div class="text">Twitter</div>
              </a>
              <a href="terms-of-service.html" target="_blank">
                <i class="fas fa-fw fa-file-contract"></i>
                <div class="text">Terms</div>
              </a>
              <a href="security-policy.html" target="_blank">
                <i class="fas fa-fw fa-shield-alt"></i>
                <div class="text">Security</div>
              </a>
              <a href="privacy-policy.html" target="_blank">
                <i class="fas fa-fw fa-lock"></i>
                <div class="text">Privacy</div>
              </a>
              <a id="supportMeButton">
                <i class="fas fa-fw fa-donate"></i>
                <div class="text">Donate</div>
              </a>
            </div>
            <div class="right">
              <a
                class="current-theme"
                aria-label="Shift-click to toggle custom theme"
                data-balloon-pos="left"
              >
                <i class="fas fa-fw fa-palette"></i>
                <div class="text">serika dark</div>
              </a>
              <a class="version">
                <i class="fas fa-fw fa-code-branch"></i>
                <div class="text">version</div>
              </a>
              <!-- <div>
                <i class="fas fa-file"></i>
                Terms & Conditions
              </div> -->
            </div>
          </div>
        </div>
        <div class="footerads">
          <div
            id="ad_footer"
            style="display: flex; justify-content: center; justify-self: center"
          ></div>
          <div
            id="ad_footer2"
            class="hidden"
            style="display: flex; justify-content: center; justify-self: center"
          ></div>
          <!-- <div
            id="ad_footer3"
            class="hidden"
            style="display: flex; justify-content: center; justify-self: center"
          ></div> -->
        </div>
      </div>
    </div>
  </body>
  <!-- Global site tag (gtag.js) - Google Analytics -->
  <script
    async
    src="https://www.googletagmanager.com/gtag/js?id=UA-165993088-1"
  ></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      dataLayer.push(arguments);
    }
    gtag("js", new Date());

    gtag("config", "UA-165993088-1");
  </script>

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="/__/firebase/8.4.2/firebase-app.js"></script>
  <!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
  <script src="/__/firebase/8.4.2/firebase-analytics.js"></script>
  <script src="/__/firebase/8.4.2/firebase-auth.js"></script>
  <!-- <script src="/__/firebase/8.4.2/firebase-firestore.js"></script> -->
  <!-- <script src="/__/firebase/8.4.2/firebase-functions.js"></script> -->

  <!-- Initialize Firebase -->
  <script src="/__/firebase/init.js"></script>
  <script src="js/jquery-3.5.1.min.js"></script>
  <script src="js/jquery.color.min.js"></script>
  <script src="js/easing.min.js"></script>
  <script src="js/jquery.cookie-1.4.1.min.js"></script>
  <script src="js/moment.min.js"></script>
  <script src="js/html2canvas.min.js"></script>
  <script
    src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"
    async
    defer
  ></script>
  <script src="https://www.google.com/recaptcha/api.js" async defer></script>
  <script src="js/monkeytype.js"></script>
</html>

==> ./monkeytype/static/privacy-policy.html <==
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Privacy Policy | Monkeytype</title>
    <!-- <link rel="stylesheet" href="css/fa.css" /> -->
    <link rel="stylesheet" href="css/balloon.css" />
    <link rel="stylesheet" href="css/style.css" />
    <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" />
    <link rel="stylesheet" href="" id="funBoxTheme" />
    <link id="favicon" rel="shortcut icon" href="images/fav.png" />
    <link rel="shortcut icon" href="images/fav.png" />
    <meta name="name" content="Monkeytype" />
    <meta name="image" content="https://monkeytype.com/mtsocial.png" />
    <meta
      name="description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta
      name="keywords"
      content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test"
    />
    <meta name="author" content="Miodec" />
    <meta property="og:title" content="Monkeytype" />
    <meta property="og:url" content="https://monkeytype.com/" />
    <meta property="og:type" content="website" />
    <meta
      property="og:description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta property="og:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="theme-color" content="#e2b714" id="metaThemeColor" />
    <meta name="twitter:title" content="Monkeytype" />
    <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="twitter:card" content="summary_large_image" />
    <style>
      #top {
        font-size: 2.5rem;
      }

      #middle {
        color: var(--text-color);
      }

      h1 {
        font-weight: unset;
        color: var(--main-color);
        font-size: 2rem;
        margin-top: 3rem;
      }

      body {
        justify-content: center;
        display: flex;
      }
    </style>
  </head>

  <body>
    <div id="centerContent">
      <div id="top">
        <div class="logo">
          <div class="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              style="isolation: isolate"
              viewBox="-680 -1030 300 180"
            >
              <g>
                <path
                  d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
                />
                <path
                  d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
                />
                <path
                  d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
                />
                <path
                  d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
                />
                <path
                  d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
                />
                <path
                  d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
                />
                <path
                  d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
                />
                <path
                  d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
                />
                <path
                  d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
                />
              </g>
            </svg>
          </div>
          <div class="text">
            <div class="top">monkey see</div>
            monkeytype
            <span style="color: var(--main-color)">Privacy Policy</span>
          </div>
        </div>
      </div>
      <div id="middle">
        <p>
          <!-- make sure to update this date every time the policy is changed -->
        </p>

        <p>Effective date: September 8, 2021</p>
        <p>
          Thanks for trusting Monkeytype ('Monkeytype', 'we', 'us', 'our') with
          your personal information! We take our
          responsibility to you very seriously, and so this Privacy Statement
          describes how we handle your data.
        </p>
        <p>
          This Privacy Statement applies to all websites we own and operate and
          to all services we provide (collectively, the 'Services'). So...PLEASE
          READ THIS PRIVACY STATEMENT CAREFULLY. By using the Services, you are
          expressly and voluntarily accepting the terms and conditions of this
          Privacy Statement and our Terms of Service, which include allowing us
          to process information about you.
        </p>
        <p>
          Under this Privacy Statement, we are the data controller responsible
          for processing your personal information. Our contact information
          appears at the end of this Privacy Statement.
        </p>
        <p>Table of Contents</p>
        <!-- The last three internal links are redunant because the anchor stops at the same location but gives more context to the user when they are viewing the table of contents -->
        <ul>
          <li><a href="#Data_Collection">What data do we collect?</a></li>
          <li>
            <a href="#How_is_Data_Collected">How do we collect your data?</a>
          </li>
          <li><a href="#Data_Usage">How will we use your data?</a></li>
          <li><a href="#Data_Storage">How do we store your data?</a></li>
          <li>
            <a href="#Data_Protection_Rights">
              What are your data protection rights?
            </a>
          </li>
          <li><a href="#Log_Data">What log data do we collect?</a></li>
          <li><a href="#Cookies">What are cookies?</a></li>
          <li><a href="#Usage_of_Cookies">How do we use cookies?</a></li>
          <li>
            <a href="#Types_of_Cookies_Used">
              What types of cookies do we use?
            </a>
          </li>
          <li><a href="#Managing_Cookies">How to manage your cookies</a></li>
          <li>
            <a href="#External_Websites">Privacy policies of other websites</a>
          </li>
          <li>
            <a href="#Privacy_Policy_Modifications">
              Changes to our privacy policy
            </a>
          </li>
          <li><a href="#Contact_Info">How to contact us</a></li>
        </ul>

        <h1 id="Data_Collection">What data do we collect?</h1>
        <p>Monkeytype collects the following data:</p>
        <ul>
          <li>Email</li>
          <li>Username</li>
          <li>Information about each typing test</li>
          <li>Your currently active settings</li>
          <li>How many typing tests you've started and completed</li>
          <li>How long you've been typing on the website</li>
        </ul>

        <h1 id="How_is_Data_Collected">How do we collect your data?</h1>

        <p>
          You directly provide most of the data we collect. We collect data and
          process data when you:
        </p>
        <ul>
          <li>Create an account</li>
          <li>Complete a typing test</li>
          <li>Change settings on the website</li>
        </ul>

        <h1 id="Data_Usage">How will we use your data?</h1>
        <p>Monkeytype collects your data so that we can:</p>

        <ul>
          <li>
            Allow you to view result history of previous tests you completed
          </li>
          <li>
            Save results from tests you take and show you statistics based on
            them
          </li>
          <li>Remember your settings</li>
          <li>Display leaderboards</li>
        </ul>

        <h1 id="Data_Storage">How do we store your data?</h1>
        <p>Monkeytype securely stores your data using Firebase Firestore.</p>

        <h1 id="Data_Protection_Rights">
          What are your data protection rights?
        </h1>
        <p>
          Monkeytype would like to make sure you are fully aware of all of your
          data protection rights. Every user is entitled to the following:
        </p>
        <ul>
          <li>
            The right to access – You have the right to request Monkeytype for
            copies of your personal data. We may limit the number of times this
            request can be made to depending on the size of the request.
          </li>
          <li>
            The right to rectification – You have the right to request that
            Monkeytype correct any information you believe is inaccurate. You
            also have the right to request Monkeytype to complete the
            information you believe is incomplete.
          </li>
          <li>
            The right to erasure – You have the right to request that Monkeytype
            erase your personal data, under certain conditions.
          </li>
          <li>
            The right to restrict processing – You have the right to request
            that Monkeytype restrict the processing of your personal data, under
            certain conditions.
          </li>
          <li>
            The right to object to processing – You have the right to object to
            Monkeytype processing of your personal data, under certain
            conditions.
          </li>
          <li>
            The right to data portability – You have the right to request that
            Monkeytype transfer the data that we have collected to another
            organization, or directly to you, under certain conditions.
          </li>
        </ul>
        <!-- <p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us at our email: jack@monkeytype.com</p> -->

        <h1 id="Log_Data">What log data do we collect?</h1>
        <p>
          Like most websites, Monkeytype collects information that your browser
          sends whenever you visit the website. This data may include internet
          protocol (IP) addresses, browser type, Internet Service Provider
          (ISP), date and time stamp, referring/exit pages, and time spent on
          each page.
          <b>
            THIS DATA DOES NOT CONTAIN ANY PERSONALLY IDENTIFIABLE INFORMATION.
          </b>
          We use this information for analyzing trends, administering the site,
          tracking users' movement on the website, and gathering demographic
          information.
        </p>
        <p>In our case, this service is provided by Google Analytics.</p>

        <h1 id="Cookies">What are cookies?</h1>
        <p>
          Cookies are text files placed on your computer to collect standard
          Internet log information and visitor behavior information. When you
          visit our websites, we may collect information from you automatically
          through cookies or similar technology
        </p>
        <p>
          For further information, visit
          <a href="https://en.wikipedia.org/wiki/HTTP_cookie" target="_blank">
            www.wikipedia.org/wiki/HTTP_cookie
          </a>.
        </p>

        <h1 id="Usage_of_Cookies">How do we use cookies?</h1>
        <p>
          Monkeytype uses cookies in a range of ways to improve your experience
          on our website, including:
        </p>
        <ul>
          <li>Keeping you signed in</li>
          <li>Remembering your active settings</li>
          <li>Remembering your active tags</li>
        </ul>
        <h1 id="Types_of_Cookies_Used">What types of cookies do we use?</h1>
        <p>
          There are a number of different types of cookies; however, our website
          uses functionality cookies. Monkeytype uses these cookies so we
          recognize you on our website and remember your previously selected
          settings.
        </p>
        <h1 id="Managing_Cookies">How to manage your cookies</h1>
        <p>
          You can set your browser not to accept cookies, and the above website
          tells you how to remove cookies from your browser. However, in a few
          cases, some of our website features may behave unexpectedly or fail to
          function as a result.
        </p>
        <h1 id="External_Websites">Privacy policies of other websites</h1>
        <p>
          Monkeytype contains links to other external websites.
          <b>
            <u>
              Our privacy policy only applies to our website, so if you click on
              a link to another website, you should read their privacy policy.
            </u>
          </b>
        </p>

        <h1 id="Privacy_Policy_Modifications">Changes to our privacy policy</h1>
        <p>
          Monkeytype keeps its privacy policy under regular review and places
          any updates on this web page. The Monkeytype privacy policy may be
          subject to change at any given time without notice. This privacy
          policy was last updated on 22 April 2021.
        </p>
        <!-- TODO: add way to view when file was last committed to using the GitHub api-->

        <h1 id="Contact_Info">How to contact us</h1>
        <p>
          If you have any questions about Monkeytype’s privacy policy, the data
          we hold on you, or you would like to exercise one of your data
          protection rights, please do not hesitate to contact us.
        </p>
        <p>
          Email:
          <a
            href="mailto: jack@monkeytype.com"
            target="_blank"
            rel="noopener noreferrer"
          >
            jack@monkeytype.com
          </a>
        </p>
        Discord:
        <span
          aria-label="Click To Copy"
          data-balloon-pos="up"
          onclick="copyUserName()"
        >
          Miodec#1512
        </span>
      </div>
    </div>
    <!-- TODO: Add image to go back to top of page -->
  </body>
  <script defer>
    // TODO: Add notification that appears when username copy is successful from notifications module
    function copyUserName() {
      if (true) {
        navigator.clipboard.writeText("Miodec#1512");
        alert("Copied To Clipboard!");
      } else {
        alert("Unable to copy username");
      }
    }

    document.querySelector("#top").addEventListener("click", () => {
      window.location = "/";
    });
  </script>
</html>

==> ./monkeytype/static/.well-known <==

==> ./monkeytype/static/.well-known/security.txt <==
Contact: mailto:jack@monkeytype.com
Contact: message @Miodec on discord.gg/monkeytype
Expires: 2022-06-03T21:00:00.000Z
Preferred-Languages: en
Canonical: https://monkeytype.com/.well-known/security.txt
Policy: https://monkeytype.com/security-policy

==> ./monkeytype/static/funbox/earthquake.css <==
@keyframes shake_dat_ass {
  0% {
    transform: translate(1px, 1px) rotate(0deg);
  }
  10% {
    transform: translate(-1px, -2px) rotate(-1deg);
  }
  20% {
    transform: translate(-3px, 0px) rotate(1deg);
  }
  30% {
    transform: translate(3px, 2px) rotate(0deg);
  }
  40% {
    transform: translate(1px, -1px) rotate(1deg);
  }
  50% {
    transform: translate(-1px, 2px) rotate(-1deg);
  }
  60% {
    transform: translate(-3px, 1px) rotate(0deg);
  }
  70% {
    transform: translate(3px, 1px) rotate(-1deg);
  }
  80% {
    transform: translate(-1px, -1px) rotate(1deg);
  }
  90% {
    transform: translate(1px, 2px) rotate(0deg);
  }
  100% {
    transform: translate(1px, -2px) rotate(-1deg);
  }
}

letter {
  animation: shake_dat_ass 0.25s infinite linear;
}

==> ./monkeytype/static/funbox/nausea.css <==
@keyframes woah {
  0% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2)
      scaleY(0.9);
  }

  25% {
    transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8);
  }

  50% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9)
      scaleY(0.9);
  }

  75% {
    transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5)
      scaleY(1.1);
  }

  100% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2)
      scaleY(0.9);
  }
}

#middle {
  animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1);
}

#centerContent {
  transform: rotate(5deg);
  perspective: 500px;
}

body {
  overflow: hidden;
}

==> ./monkeytype/static/funbox/read_ahead_hard.css <==
#words .word.active:nth-of-type(n + 2),
#words .word.active:nth-of-type(n + 2) + .word,
#words .word.active:nth-of-type(n + 2) + .word + .word {
  color: transparent;
}

==> ./monkeytype/static/funbox/space_balls.css <==
:root {
  --bg-color: #000000;
  --main-color: #ffffff;
  --caret-color: #ffffff;
  --sub-color: rgba(255, 255, 255, 0.1);
  --text-color: #ffd100;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

body {
  background-image: url("https://thumbs.gfycat.com/SlimyClassicAsianconstablebutterfly-size_restricted.gif");
  background-size: cover;
  background-position: center;
}

#middle {
  transform: rotateX(35deg);
}

#centerContent {
  perspective: 500px;
}

==> ./monkeytype/static/funbox/read_ahead_easy.css <==
#words .word.active:nth-of-type(n + 2) {
  color: transparent;
}

==> ./monkeytype/static/funbox/mirror.css <==
#middle {
  transform: scaleX(-1);
}

==> ./monkeytype/static/funbox/round_round_baby.css <==
@keyframes woah {
  0% {
    transform: rotateZ(0deg);
  }

  50% {
    transform: rotateZ(180deg);
  }

  100% {
    transform: rotateZ(360deg);
  }
}

#middle {
  animation: woah 5s infinite linear;
}

body {
  overflow: hidden;
}

==> ./monkeytype/static/funbox/choo_choo.css <==
@keyframes woah {
  0% {
    transform: rotateZ(0deg);
  }

  50% {
    transform: rotateZ(180deg);
  }

  100% {
    transform: rotateZ(360deg);
  }
}

letter {
  animation: woah 2s infinite linear;
}

==> ./monkeytype/static/funbox/read_ahead.css <==
#words .word.active:nth-of-type(n + 2),
#words .word.active:nth-of-type(n + 2) + .word {
  color: transparent;
}

==> ./monkeytype/static/funbox/simon_says.css <==
/* #words {
  opacity: 0 !important;
} */

#words .word {
  color: transparent !important;
}

==> ./monkeytype/static/email-handler.html <==
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Email Handler | Monkeytype</title>
    <!-- <link rel="stylesheet" href="css/fa.css" /> -->
    <link rel="stylesheet" href="css/balloon.css" />
    <link rel="stylesheet" href="css/style.css" />
    <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" />
    <link rel="stylesheet" href="" id="funBoxTheme" />
    <link id="favicon" rel="shortcut icon" href="images/fav.png" />
    <link rel="shortcut icon" href="images/fav.png" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
      integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    />
    <meta name="name" content="Monkeytype" />
    <meta name="image" content="https://monkeytype.com/mtsocial.png" />
    <meta
      name="description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta
      name="keywords"
      content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test"
    />
    <meta name="author" content="Miodec" />
    <meta property="og:title" content="Monkeytype" />
    <meta property="og:url" content="https://monkeytype.com/" />
    <meta property="og:type" content="website" />
    <meta
      property="og:description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta property="og:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="theme-color" content="#e2b714" id="metaThemeColor" />
    <meta name="twitter:title" content="Monkeytype" />
    <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="twitter:card" content="summary_large_image" />
    <style>
      #top {
        font-size: 2.5rem;
      }

      #middle {
        color: var(--text-color);
        display: grid;
        justify-content: center;
      }

      h1 {
        font-weight: unset;
        color: var(--main-color);
        font-size: 2rem;
        margin-top: 3rem;
      }

      body {
        justify-content: center;
        display: flex;
      }

      .preloader {
        text-align: center;
        display: grid;
        width: 300px;
        /* gap: 1rem; */
      }
      .preloader .icon {
        font-size: 2rem;
        color: var(--main-color);
        margin-bottom: 1rem;
      }
      .preloader .subText {
        font-size: 1rem;
        color: var(--sub-color);
        font-style: italic;
      }

      .resetPassword {
        display: grid;
        width: 300px;
        gap: 1rem;
      }

      .hidden {
        display: none;
      }
    </style>
  </head>

  <body>
    <div id="centerContent">
      <div id="top">
        <div class="logo">
          <div class="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              style="isolation: isolate"
              viewBox="-680 -1030 300 180"
            >
              <g>
                <path
                  d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
                />
                <path
                  d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
                />
                <path
                  d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
                />
                <path
                  d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
                />
                <path
                  d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
                />
                <path
                  d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
                />
                <path
                  d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
                />
                <path
                  d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
                />
                <path
                  d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
                />
              </g>
            </svg>
          </div>
          <div class="text">
            <div class="top">monkey see</div>
            monkeytype
            <span style="color: var(--main-color)">Email Handler</span>
          </div>
        </div>
      </div>
      <div id="middle">
        <div class="preloader">
          <div class="icon">
            <i class="fas fa-fw fa-spin fa-circle-notch"></i>
          </div>
          <div class="text"></div>
          <div class="subText"></div>
        </div>
        <div class="resetPassword hidden">
          <input type="password" placeholder="New password" />
          <div class="button">Change</div>
        </div>
      </div>
    </div>
    <!-- TODO: Add image to go back to top of page -->
  </body>
  <script src="js/jquery-3.5.1.min.js"></script>
  <script src="/__/firebase/8.4.2/firebase-app.js"></script>

  <!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
  <script src="/__/firebase/8.4.2/firebase-analytics.js"></script>
  <script src="/__/firebase/8.4.2/firebase-auth.js"></script>
  <script src="/__/firebase/8.4.2/firebase-firestore.js"></script>
  <script src="/__/firebase/8.4.2/firebase-functions.js"></script>

  <!-- Initialize Firebase -->
  <script src="/__/firebase/init.js?useEmulator=true'"></script>
  <script defer>
    function handleVerifyEmail(actionCode, continueUrl) {
      firebase
        .auth()
        .applyActionCode(actionCode)
        .then((resp) => {
          // Email address has been verified.

          $("#middle .preloader .icon").html(
            `<i class="fas fa-fw fa-check"></i>`
          );
          $("#middle .preloader .text").text(
            `Your email address has been verified.`
          );
          $("#middle .preloader .subText").text(`You can now close this tab.`);

          // TODO: Display a confirmation message to the user.
          // You could also provide the user with a link back to the app.

          // TODO: If a continue URL is available, display a button which on
          // click redirects the user back to the app via continueUrl with
          // additional state determined from that URL's parameters.
        })
        .catch((error) => {
          $("#middle .preloader .icon").html(
            `<i class="fas fa-fw fa-times"></i>`
          );
          $("#middle .preloader .text").text(error.message);

          // Code is invalid or expired. Ask the user to verify their email address
          // again.
        });
    }

    function showResetPassword() {
      $("#middle .preloader").addClass("hidden");
      $("#middle .resetPassword").removeClass("hidden");
      $("#middle .resetPassword input").focus();
    }

    function hideResetPassword() {
      $("#middle .preloader").removeClass("hidden");
      $("#middle .resetPassword").addClass("hidden");
    }

    function handleResetPassword(actionCode, continueUrl) {
      // Verify the password reset code is valid.
      hideResetPassword();
      firebase
        .auth()
        .verifyPasswordResetCode(actionCode)
        .then((email) => {
          var accountEmail = email;

          // TODO: Show the reset screen with the user's email and ask the user for
          // the new password.
          var newPassword = $("#middle .resetPassword input").val();

          // Save the new password.
          firebase
            .auth()
            .confirmPasswordReset(actionCode, newPassword)
            .then((resp) => {
              // Password reset has been confirmed and new password updated.
              $("#middle .preloader .icon").html(
                `<i class="fas fa-fw fa-check"></i>`
              );
              $("#middle .preloader .text").text(
                `Your password has been changed.`
              );
              $("#middle .preloader .subText").text(
                `You can now close this tab.`
              );
              // TODO: Display a link back to the app, or sign-in the user directly
              // if the page belongs to the same domain as the app:
              firebase
                .auth()
                .signInWithEmailAndPassword(accountEmail, newPassword);

              // TODO: If a continue URL is available, display a button which on
              // click redirects the user back to the app via continueUrl with
              // additional state determined from that URL's parameters.
            })
            .catch((error) => {
              $("#middle .preloader .icon").html(
                `<i class="fas fa-fw fa-times"></i>`
              );
              $("#middle .preloader .text").text(error.message);
              // Error occurred during confirmation. The code might have expired or the
              // password is too weak.
            });
        })
        .catch((error) => {
          $("#middle .preloader .icon").html(
            `<i class="fas fa-fw fa-times"></i>`
          );
          $("#middle .preloader .text").text(error.message);
          // $("#middle .preloader .subText").text(error);

          // Invalid or expired action code. Ask user to try to reset the password
          // again.
        });
    }

    function getParameterByName(name, url = window.location.href) {
      name = name.replace(/[\[\]]/g, "\\$&");
      var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
      if (!results) return null;
      if (!results[2]) return "";
      return decodeURIComponent(results[2].replace(/\+/g, " "));
    }

    document.addEventListener(
      "DOMContentLoaded",
      () => {
        try {
          // Get the action to complete.
          var mode = getParameterByName("mode");
          // Get the one-time code from the query parameter.
          var actionCode = getParameterByName("oobCode");
          // (Optional) Get the continue URL from the query parameter if available.
          var continueUrl = getParameterByName("continueUrl");
          // (Optional) Get the language code if available.
          var lang = getParameterByName("lang") || "en";

          // Configure the Firebase SDK.
          // // This is the minimum configuration required for the API to be used.
          // var config = {
          //   'apiKey': "YOU_API_KEY" // Copy this key from the web initialization
          //                           // snippet found in the Firebase console.
          // };
          // var app = firebase.initializeApp(config);
          // var auth = firebase.auth();

          if (!mode) {
            $("#middle .preloader .icon").html(
              `<i class="fas fa-fw fa-times"></i>`
            );
            $("#middle .preloader .text").text(`Mode parameter not found`);
            return;
          }

          if (!actionCode) {
            $("#middle .preloader .icon").html(
              `<i class="fas fa-fw fa-times"></i>`
            );
            $("#middle .preloader .text").text(
              `Action code parameter not found`
            );
            return;
          }

          // Handle the user management action.
          switch (mode) {
            case "resetPassword":
              // Display reset password handler and UI.
              $(".logo .text span").text("Reset Password");
              document.title = "Reset Password | Monkeytype";
              showResetPassword();
              break;
            case "recoverEmail":
              // Display email recovery handler and UI.
              handleRecoverEmail(actionCode);
              break;
            case "verifyEmail":
              $(".logo .text span").text("Verify Email");
              document.title = "Verify Email | Monkeytype";
              // Display email verification handler and UI.
              handleVerifyEmail(actionCode, continueUrl);
              break;
            default:
              $("#middle .preloader .icon").html(
                `<i class="fas fa-fw fa-times"></i>`
              );
              $("#middle .preloader .text").text(`Invalid mode`);
              console.error("no mode found");
            // Error: invalid mode.
          }

          $("#middle .resetPassword .button").click(() => {
            handleResetPassword(actionCode, continueUrl);
          });

          $("#middle .resetPassword input").keypress((e) => {
            if (e.key == "Enter") handleResetPassword(actionCode, continueUrl);
          });
        } catch (e) {
          $("#middle .preloader .icon").html(
            `<i class="fas fa-fw fa-times"></i>`
          );
          $("#middle .preloader .text").text(
            `Fatal error: ${e.message}. If this issue persists, please report it.`
          );
        }
      },
      false
    );

    document.querySelector("#top").addEventListener("click", () => {
      window.location = "/";
    });
  </script>
</html>

==> ./monkeytype/static/terms-of-service.html <==
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Terms of Service | Monkeytype</title>
    <!-- <link rel="stylesheet" href="css/fa.css" /> -->
    <link rel="stylesheet" href="css/balloon.css" />
    <link rel="stylesheet" href="css/style.css" />
    <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" />
    <link rel="stylesheet" href="" id="funBoxTheme" />
    <link id="favicon" rel="shortcut icon" href="images/fav.png" />
    <link rel="shortcut icon" href="images/fav.png" />
    <meta name="name" content="Monkeytype" />
    <meta name="image" content="https://monkeytype.com/mtsocial.png" />
    <meta
      name="description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta
      name="keywords"
      content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test"
    />
    <meta name="author" content="Miodec" />
    <meta property="og:title" content="Monkeytype" />
    <meta property="og:url" content="https://monkeytype.com/" />
    <meta property="og:type" content="website" />
    <meta
      property="og:description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta property="og:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="theme-color" content="#e2b714" id="metaThemeColor" />
    <meta name="twitter:title" content="Monkeytype" />
    <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="twitter:card" content="summary_large_image" />
    <style>
      #top {
        font-size: 2.5rem;
      }

      #middle {
        color: var(--text-color);
      }

      h1 {
        font-weight: unset;
        color: var(--main-color);
        font-size: 2rem;
        margin-top: 3rem;
      }

      body {
        justify-content: center;
        display: flex;
      }
    </style>
  </head>

  <body>
    <div id="centerContent">
      <div id="top">
        <div class="logo">
          <div class="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              style="isolation: isolate"
              viewBox="-680 -1030 300 180"
            >
              <g>
                <path
                  d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
                />
                <path
                  d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
                />
                <path
                  d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
                />
                <path
                  d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
                />
                <path
                  d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
                />
                <path
                  d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
                />
                <path
                  d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
                />
                <path
                  d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
                />
                <path
                  d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
                />
              </g>
            </svg>
          </div>
          <div class="text">
            <div class="top">monkey see</div>
            monkeytype
            <span style="color: var(--main-color)">Terms of Service</span>
          </div>
        </div>
      </div>
      <div id="middle">
        <!-- make sure to update this date every time the tos is changed -->
        <p>These terms of service were last updated on September 11, 2021.</p>
        <h1>Agreement</h1>
        <p>
          By accessing this Website, accessible from monkeytype.com, you are
          agreeing to be bound by these Website Terms of Service and agree that
          you are responsible for the agreement in accordance with any
          applicable local laws.
          <strong>
            IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS
            AGREEMENT, YOU ARE NOT PERMITTED TO ACCESS OR USE OUR SERVICES.
          </strong>
        </p>

        <h1>Limitations</h1>
        <p>
          You are responsible for your account's security and all activities on
          your account. You must not, in the use of this site, violate any
          applicable laws, including, without limitation, copyright laws, or any
          other laws regarding the security of your personal data, or otherwise
          misuse this site.
        </p>
        <p>
          Monkeytype reserves the right to remove or disable any account or any
          other content on this site at any time for any reason, without prior
          notice to you, if we believe that you have violated this agreement.
        </p>
        <p>
          You agree that you will not upload, post, host, or transmit any
          content that:
        </p>
        <ol>
          <li>is unlawful or promotes unlawful activities;</li>
          <li>is or contains sexually obscene content;</li>
          <li>is libelous, defamatory, or fraudulent;</li>
          <li>is discriminatory or abusive toward any individual or group;</li>
          <li>
            is degrading to others on the basis of gender, race, class,
            ethnicity, national origin, religion, sexual preference,
            orientation, or identity, disability, or other classification, or
            otherwise represents or condones content that: is hate speech,
            discriminating, threatening, or pornographic; incites violence; or
            contains nudity or graphic or gratuitous violence;
          </li>
          <li>
            violates any person's right to privacy or publicity, or otherwise
            solicits, collects, or publishes data, including personal
            information and login information, about other Users without consent
            or for unlawful purposes in violation of any applicable
            international, federal, state, or local law, statute, ordinance, or
            regulation; or
          </li>
          <li>
            contains or installs any active malware or exploits/uses our
            platform for exploit delivery (such as part of a command or control
            system); or infringes on any proprietary right of any party,
            including patent, trademark, trade secret, copyright, right of
            publicity, or other rights.
          </li>
        </ol>

        <p>While using the Services, you agree that you will not:</p>
        <ol>
          <li>
            harass, abuse, threaten, or incite violence towards any individual
            or group, including other Users and Monkeytype contributors;
          </li>
          <li>
            use our servers for any form of excessive automated bulk activity
            (e.g., spamming), or rely on any other form of unsolicited
            advertising or solicitation through our servers or Services;
          </li>
          <li>
            attempt to disrupt or tamper with our servers in ways that could a)
            harm our Website or Services or b) place undue burden on our
            servers;
          </li>
          <li>access the Services in ways that exceed your authorization;</li>
          <li>
            falsely impersonate any person or entity, including any of our
            contributors, misrepresent your identity or the site's purpose, or
            falsely associate yourself with Monkeytype;
          </li>
          <li>
            violate the privacy of any third party, such as by posting another
            person's personal information without their consent;
          </li>
          <li>
            access or attempt to access any service on the Services by any means
            other than as permitted in this Agreement, or operating the Services
            on any computers or accounts which you do not have permission to
            operate;
          </li>
          <li>
            facilitate or encourage any violations of this Agreement or
            interfere with the operation, appearance, security, or functionality
            of the Services; or
          </li>
          <li>use the Services in any manner that is harmful to minors.</li>
        </ol>
        <p>
          Without limiting the foregoing, you will not transmit or post any
          content anywhere on the Services that violates any laws. Monkeytype
          absolutely does not tolerate engaging in activity that significantly
          harms our Users. We will resolve disputes in favor of protecting our
          Users as a whole.
        </p>
        <h1>Privacy Policy</h1>
        If you use our Services, you must abide by our Privacy Policy. You
        acknowledge that you have read our
        <a href="https://monkeytype.com/privacy-policy" target="_blank">
          Privacy Policy
        </a>
        and understand that it sets forth how we collect, use, and store your
        information. If you do not agree with our Privacy Statement, then you
        must stop using the Services immediately. Any person, entity, or service
        collecting data from the Services must comply with our Privacy
        Statement. Misuse of any User's Personal Information is prohibited. If
        you collect any Personal Information from a User, you agree that you
        will only use the Personal Information you gather for the purpose for
        which the User has authorized it. You agree that you will reasonably
        secure any Personal Information you have gathered from the Services, and
        you will respond promptly to complaints, removal requests, and 'do not
        contact' requests from us or Users.

        <h1>Limitations on Automated Use</h1>
        You shouldn't use bots or access our Services in malicious or
        un-permitted ways. While accessing or using the Services, you may not:
        <ol>
          <li>use bots, hacks, or cheats while using our site;</li>
          <li>create manual requests to Monkeytype servers;</li>
          <li>
            tamper with or use non-public areas of the Services, or the computer
            or delivery systems of Monkeytype and/or its service providers;
          </li>
          <li>
            probe, scan, or test any system or network (particularly for
            vulnerabilities), or otherwise attempt to breach or circumvent any
            security or authentication measures, or search or attempt to access
            or search the Services by any means (automated or otherwise) other
            than through our currently available, published interfaces that are
            provided by Monkeytype (and only pursuant to those terms and
            conditions), unless you have been specifically allowed to do so in a
            separate agreement with Monkeytype, Inc., or unless specifically
            permitted by Monkeytype, Inc.'s robots.txt file or other robot
            exclusion mechanisms;
          </li>
          <li>
            scrape the Services, scrape Content from the Services, or use
            automated means, including spiders, robots, crawlers, data mining
            tools, or the like to download data from the Services or otherwise
            access the Services;
          </li>
          <li>
            employ misleading email or IP addresses or forged headers or
            otherwise manipulated identifiers in order to disguise the origin of
            any content transmitted to or through the Services;
          </li>
          <li>
            use the Services to send altered, deceptive, or false
            source-identifying information, including, without limitation, by
            forging TCP-IP packet headers or e-mail headers; or
          </li>
          <li>
            interfere with, or disrupt or attempt to interfere with or disrupt,
            the access of any User, host, or network, including, without
            limitation, by sending a virus to, spamming, or overloading the
            Services, or by scripted use of the Services in such a manner as to
            interfere with or create an undue burden on the Services.
          </li>
        </ol>
        <h1>Links</h1>
        Monkeytype is not responsible for the contents of any linked sites. The
        use of any linked website is at the user's own risk.

        <h1>Changes</h1>
        Monkeytype may revise these Terms of Service for its Website at any time
        without prior notice. By using this Website, you are agreeing to be
        bound by the current version of these Terms of Service.
        <h1>Disclaimer</h1>
        <p>
          EXCLUDING THE EXPLICITLY STATED WARRANTIES WITHIN THESE TERMS, WE ONLY
          OFFER OUR SERVICES ON AN 'AS-IS' BASIS. YOUR ACCESS TO AND USE OF THE
          SERVICES OR ANY CONTENT IS AT YOUR OWN RISK. YOU UNDERSTAND AND AGREE
          THAT THE SERVICES AND CONTENT ARE PROVIDED TO YOU ON AN 'AS IS,' 'WITH
          ALL FAULTS,' AND 'AS AVAILABLE' BASIS. WITHOUT LIMITING THE FOREGOING,
          TO THE FULL EXTENT PERMITTED BY LAW, MONKEYTYPE DISCLAIMS ALL
          WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
          WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
          NON-INFRINGEMENT. TO THE EXTENT SUCH DISCLAIMER CONFLICTS WITH
          APPLICABLE LAW, THE SCOPE AND DURATION OF ANY APPLICABLE WARRANTY WILL
          BE THE MINIMUM PERMITTED UNDER SUCH LAW. MONKEYTYPE MAKES NO
          REPRESENTATIONS, WARRANTIES, OR GUARANTEES AS TO THE RELIABILITY,
          TIMELINESS, QUALITY, SUITABILITY, AVAILABILITY, ACCURACY, OR
          COMPLETENESS OF ANY KIND WITH RESPECT TO THE SERVICES, INCLUDING ANY
          REPRESENTATION OR WARRANTY THAT THE USE OF THE SERVICES WILL (A) BE
          TIMELY, UNINTERRUPTED, OR ERROR-FREE, OR OPERATE IN COMBINATION WITH
          ANY OTHER HARDWARE, SOFTWARE, SYSTEM, OR DATA, (B) MEET YOUR
          REQUIREMENTS OR EXPECTATIONS, (C) BE FREE FROM ERRORS OR THAT DEFECTS
          WILL BE CORRECTED, OR (D) BE FREE OF VIRUSES OR OTHER HARMFUL
          COMPONENTS. MONKEYTYPE ALSO MAKES NO REPRESENTATIONS OR WARRANTIES OF
          ANY KIND WITH RESPECT TO CONTENT; USER CONTENT IS PROVIDED BY AND IS
          SOLELY THE RESPONSIBILITY OF THE RESPECTIVE USER PROVIDING THAT
          CONTENT. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED
          FROM MONKEYTYPE OR THROUGH THE SERVICES, WILL CREATE ANY WARRANTY NOT
          EXPRESSLY MADE HEREIN. MONKEYTYPE DOES NOT WARRANT, ENDORSE,
          GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY USER CONTENT ON THE
          SERVICES OR ANY HYPERLINKED WEBSITE OR THIRD-PARTY SERVICE, AND
          MONKEYTYPE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR
          TRANSACTIONS BETWEEN YOU AND THIRD PARTIES. IF APPLICABLE LAW DOES NOT
          ALLOW THE EXCLUSION OF SOME OR ALL OF THE ABOVE IMPLIED OR STATUTORY
          WARRANTIES TO APPLY TO YOU, THE ABOVE EXCLUSIONS WILL APPLY TO YOU TO
          THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW.
        </p>
        <h1>Contact</h1>
        <p>
          If you have any questions about Monkeytype’s privacy policy, the data
          we hold on you, or you would like to exercise one of your data
          protection rights, please do not hesitate to contact us.
        </p>
        <p>
          Email:
          <a
            href="mailto:jack@monkeytype.com"
            target="_blank"
            rel="noopener noreferrer"
          >
            jack@monkeytype.com
          </a>
        </p>
        Discord:
        <span
          aria-label="Click To Copy"
          data-balloon-pos="up"
          onclick="copyUserName()"
        >
          Miodec#1512
        </span>
      </div>
      <p>
        Terms based on
        <a href="https://glitch.com/legal" target="_blank">Glitch terms</a>
      </p>
    </div>
    <!-- TODO: Add button to go back to top of page -->
  </body>
  <script defer>
    // TODO: Add notification that appears when username copy is successful from notifications module
    function copyUserName() {
      if (true) {
        navigator.clipboard.writeText("Miodec#1512");
        alert("Copied To Clipboard!");
      } else {
        alert("Unable to copy username");
      }
    }

    document.querySelector("#top").addEventListener("click", () => {
      window.location = "/";
    });
  </script>
</html>

==> ./monkeytype/static/robots.txt <==
User-agent: *
Disallow: 

==> ./monkeytype/static/security-policy.html <==
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Security Policy | Monkeytype</title>
    <!-- <link rel="stylesheet" href="css/fa.css" /> -->
    <link rel="stylesheet" href="css/balloon.css" />
    <link rel="stylesheet" href="css/style.css" />
    <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" />
    <link rel="stylesheet" href="" id="funBoxTheme" />
    <link id="favicon" rel="shortcut icon" href="images/fav.png" />
    <link rel="shortcut icon" href="images/fav.png" />
    <meta name="name" content="Monkeytype" />
    <meta name="image" content="https://monkeytype.com/mtsocial.png" />
    <meta
      name="description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta
      name="keywords"
      content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test"
    />
    <meta name="author" content="Miodec" />
    <meta property="og:title" content="Monkeytype" />
    <meta property="og:url" content="https://monkeytype.com/" />
    <meta property="og:type" content="website" />
    <meta
      property="og:description"
      content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed."
    />
    <meta property="og:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="theme-color" content="#e2b714" id="metaThemeColor" />
    <meta name="twitter:title" content="Monkeytype" />
    <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" />
    <meta name="twitter:card" content="summary_large_image" />
    <style>
      #top {
        font-size: 2.5rem;
      }

      #middle {
        color: var(--text-color);
      }

      #centerContent {
        align-items: flex-start;
      }

      h1 {
        font-weight: unset;
        color: var(--main-color);
        font-size: 2rem;
        margin-top: 3rem;
      }

      body {
        justify-content: center;
        display: flex;
      }
    </style>
  </head>

  <body>
    <div id="centerContent">
      <div id="top">
        <div class="logo">
          <div class="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              style="isolation: isolate"
              viewBox="-680 -1030 300 180"
            >
              <g>
                <path
                  d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
                />
                <path
                  d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
                />
                <path
                  d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
                />
                <path
                  d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
                />
                <path
                  d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
                />
                <path
                  d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
                />
                <path
                  d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
                />
                <path
                  d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
                />
                <path
                  d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
                />
              </g>
            </svg>
          </div>
          <div class="text">
            <div class="top">monkey see</div>
            monkeytype
            <span style="color: var(--main-color)">Security Policy</span>
          </div>
        </div>
      </div>
      <div id="middle">
        <p>
          We take the security and integrity of Monkeytype very seriously. If
          you have found a vulnerability, please report it
          <abbr title="As Soon As Possible">ASAP</abbr>
          so we can quickly remediate the issue.
        </p>
        <p>Table of Contents</p>
        <!-- The last three internal links are redunant but give more context to the user when viewing the table of contents -->
        <ul>
          <li>
            <a href="#Vulnerability_Disclosure">
              How to Disclose a Vulnerability
            </a>
          </li>
          <li><a href="#Submission_Guidelines">Submission Guidelines</a></li>
        </ul>

        <h1 id="Vulnerability_Disclosure">How to Disclose a Vulnerability</h1>
        <p>
          For vulnerabilities that impact the confidentiality, integrity, and
          availability of Monkeytype services, please send your disclosure via
          (1)
          <a href="mailto:jack@monkeytype.com">email</a>
          , or (2) ping
          <span
            aria-label="Click To Copy"
            data-balloon-pos="up"
            onclick="copyUserName()"
          >
            Miodec#1512
          </span>
          on the
          <a href="https://www.discord.gg/monkeytype">
            Monkeytype Discord server in the #development channel
          </a>
          and he can discuss the situation with you further in private. For
          non-security related platform bugs, follow the bug submission
          <a
            href="https://github.com/Miodec/monkeytype#bug-report-or-feature-request"
          >
            guidelines
          </a>
          . Include as much detail as possible to ensure reproducibility. At a
          minimum, vulnerability disclosures should include:
        </p>
        <ul>
          <li>Vulnerability Description</li>
          <li>Proof of Concept</li>
          <li>Impact</li>
          <li>Screenshots or Proof</li>
        </ul>

        <h1 id="Submission_Guidelines">Submission Guidelines</h1>
        <p>
          Do not engage in activities that might cause a denial of service
          condition, create significant strains on critical resources, or
          negatively impact users of the site outside of test accounts.
        </p>
      </div>
    </div>
    <!-- TODO: Add image to go back to top of page -->
  </body>
  <script defer>
    // TODO: Add notification that appears when username copy is successful from notifications module
    function copyUserName() {
      if (true) {
        navigator.clipboard.writeText("Miodec#1512");
        alert("Copied To Clipboard!");
      } else {
        alert("Unable to copy username");
      }
    }

    document.querySelector("#top").addEventListener("click", () => {
      window.location = "/";
    });
  </script>
</html>

==> ./monkeytype/static/themes/terminal.css <==
:root {
  --bg-color: #191a1b;
  --caret-color: #79a617;
  --main-color: #79a617;
  --sub-color: #48494b;
  --text-color: #e7eae0;
  --error-color: #a61717;
  --error-extra-color: #731010;
  --colorful-error-color: #a61717;
  --colorful-error-extra-color: #731010;
}

==> ./monkeytype/static/themes/ms_cupcakes.css <==
:root {
  --bg-color: #ffffff;
  --main-color: #5ed5f3;
  --caret-color: #303030;
  --sub-color: #d64090;
  --text-color: #0a282f;
  --error-color: #000000;
  --error-extra-color: #c9c9c9;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/fledgling.css <==
:root {
  --bg-color: #3b363f;
  --main-color: #fc6e83;
  --caret-color: #474747;
  --sub-color: #ead8d6;
  --text-color: #fc6e83;
  --error-color: #f52443;
  --error-extra-color: #bd001c;
  --colorful-error-color: #ff0a2f;
  --colorful-error-extra-color: #000000;
}

==> ./monkeytype/static/themes/horizon.css <==
:root {
  --bg-color: #1C1E26;
  --main-color:#c4a88a;
  --caret-color: #BBBBBB;
  --sub-color: #db886f;
  --text-color: #bbbbbb;
  --error-color: #D55170;
  --error-extra-color: #ff3d3d;
  --colorful-error-color: #D55170;
  --colorful-error-extra-color:#D55170;
}

#menu .icon-button:nth-child(1) {
  color: #D55170;
}

#menu .icon-button:nth-child(2) {
  color: #E4A88A;
}

#menu .icon-button:nth-child(3) {
  color: #DB886F;
}

#menu .icon-button:nth-child(4) {
  color: #DB887A;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #FFC819;
}

==> ./monkeytype/static/themes/vaporwave.css <==
:root {
  --bg-color: #a4a7ea;
  --main-color: #e368da;
  --caret-color: #28cafe;
  --sub-color: #7c7faf;
  --text-color: #f1ebf1;
  --error-color: #573ca9;
  --error-extra-color: #3d2b77;
  --colorful-error-color: #28cafe;
  --colorful-error-extra-color: #25a9ce;
}

==> ./monkeytype/static/themes/witch_girl.css <==
:root {
  --bg-color: #f3dbda;
  --main-color: #56786a;
  --caret-color: #afc5bd;
  --sub-color: #ddb4a7;
  --text-color: #56786a;
  --error-color: #b29a91;
  --error-extra-color: #b29a91;
  --colorful-error-color: #b29a91;
  --colorful-error-extra-color: #b29a91;
}

==> ./monkeytype/static/themes/gruvbox_light.css <==
:root {
 --bg-color: #fbf1c7;
 --main-color: #689d6a;
 --caret-color: #689d6a;
 --sub-color: #a89984;
 --text-color: #3c3836;
 --error-color: #cc241d;
 --error-extra-color: #9d0006;
 --colorful-error-color: #cc241d;
 --colorful-error-extra-color: #9d0006;
}

==> ./monkeytype/static/themes/soaring_skies.css <==
:root {
  --bg-color: #fff9f2;
  --main-color: #55c6f0;
  --caret-color: #1e107a;
  --sub-color: #1e107a;
  --text-color: #1d1e1e;
  --error-color: #fb5745;
  --error-extra-color: #b03c30;
  --colorful-error-color: #fb5745;
  --colorful-error-extra-color: #b03c30;
}

==> ./monkeytype/static/themes/terra.css <==
:root {
  --bg-color: #0c100e;
  --main-color: #89c559;
  --caret-color: #89c559;
  --sub-color: #436029;
  --text-color: #f0edd1;
  --error-color: #d3ca78;
  --error-extra-color: #89844d;
  --colorful-error-color: #d3ca78;
  --colorful-error-extra-color: #89844d;
}

==> ./monkeytype/static/themes/camping.css <==
:root {
  --bg-color: #faf1e4;
  --main-color: #618c56;
  --caret-color: #618c56;
  --sub-color: #c2b8aa;
  --text-color: #3c403b;
  --error-color: #ad4f4e;
  --error-extra-color: #7e3a39;
  --colorful-error-color: #ad4f4e;
  --colorful-error-extra-color: #7e3a39;
}

#top .logo .bottom {
  color: #ad4f4e;
}

==> ./monkeytype/static/themes/lil_dragon.css <==
:root {
  --bg-color: #ebe1ef;
  --main-color: #8a5bd6;
  --caret-color: #212b43;
  --sub-color: #ac76e5;
  --text-color: #212b43;
  --error-color: #f794ca;
  --error-extra-color: #f279c2;
  --colorful-error-color: #f794ca;
  --colorful-error-extra-color: #f279c2;
}

#menu .icon-button {
  color: #ba96db;
}

#menu .icon-button:hover {
  color: #212b43;
}

==> ./monkeytype/static/themes/laser.css <==
:root {
  --bg-color: #221b44;
  --main-color: #009eaf;
  --caret-color: #009eaf;
  --sub-color: #b82356;
  --text-color: #dbe7e8;
  --error-color: #a8d400;
  --error-extra-color: #668000;
  --colorful-error-color: #a8d400;
  --colorful-error-extra-color: #668000;
}

==> ./monkeytype/static/themes/desert_oasis.css <==
:root {
  --bg-color: #fff2d5; /*Background*/
  --main-color: #d19d01; /*Color after typing, monkeytype logo, WPM Number acc number etc*/
  --caret-color: #3a87fe; /*Cursor Color*/
  --sub-color: #0061fe; /*WPM text color of scrollbar and general color, before typed color*/
  --text-color: #332800; /*Color of text after hovering over it*/
  --error-color: #76bb40;
  --error-extra-color: #4e7a27;
  --colorful-error-color: #76bb40;
  --colorful-error-extra-color: #4e7a27;
}

#menu .icon-button:nth-child(1) {
  color: #76bb40;
}

#menu .icon-button:nth-child(2) {
  color: #76bb40;
}

#menu .icon-button:nth-child(3) {
  color: #76bb40;
}

#menu .icon-button:nth-child(4) {
  color: #76bb40;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #76bb40;
}

==> ./monkeytype/static/themes/honey.css <==
:root {
  --bg-color: #f2aa00;
  --main-color: #fff546;
  --caret-color: #795200;
  --sub-color: #a66b00;
  --text-color: #f3eecb;
  --error-color: #df3333;
  --error-extra-color: #6d1f1f;
  --colorful-error-color: #df3333;
  --colorful-error-extra-color: #6d1f1f;
}

==> ./monkeytype/static/themes/9009.css <==
:root {
  --bg-color: #eeebe2;
  --main-color: #080909;
  --caret-color: #7fa480;
  --sub-color: #99947f;
  --text-color: #080909;
  --error-color: #c87e74;
  --colorful-error-color: #a56961;
  --colorful-error-color: #c87e74;
  --colorful-error-extra-color: #a56961;
}

.word letter.incorrect {
  color: var(--error-color);
}

.word letter.incorrect.extra {
  color: var(--colorful-error-color);
}

.word.error {
  border-bottom: solid 2px var(--error-color);
}

key {
  color: var(--sub-color);
  background-color: var(--main-color);
}

#menu .icon-button {
  color: var(--main-color);
}

#menu .icon-button:nth-child(1) {
  color: var(--error-color);
}

#menu .icon-button:nth-child(4) {
  color: var(--caret-color);
}

==> ./monkeytype/static/themes/sweden.css <==
:root {
  --bg-color: #0058a3;
  --main-color: #ffcc02;
  --caret-color: #b5b5b5;
  --sub-color: #57abdb;
  --text-color: #ffffff;
  --error-color: #e74040;
  --error-extra-color: #a22f2f;
  --colorful-error-color: #f56674;
  --colorful-error-extra-color: #e33546;
}

==> ./monkeytype/static/themes/solarized_dark.css <==
:root {
  --bg-color: #002b36;
  --main-color: #859900;
  --caret-color: #dc322f;
  --sub-color: #2aa198;
  --text-color: #268bd2;
  --error-color: #d33682;
  --error-extra-color: #9b225c;
  --colorful-error-color: #d33682;
  --colorful-error-extra-color: #9b225c;
}

==> ./monkeytype/static/themes/mizu.css <==
:root {
  --bg-color: #afcbdd;
  --main-color: #fcfbf6;
  --caret-color: #fcfbf6;
  --sub-color: #85a5bb;
  --text-color: #1a2633;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/terror_below.css <==
:root {
  --bg-color: #0b1e1a;
  --caret-color: #66ac92;
  --main-color: #66ac92;
  --sub-color: #015c53;
  --text-color: #dceae5;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/bingsu.css <==
:root {
  /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */
  --bg-color: #b8a7aa;
  --main-color: #83616e;
  --caret-color: #ebe6ea;
  --sub-color: #48373d;
  --text-color: #ebe6ea;
  --error-color: #921341;
  --error-extra-color: #640b2c;
  --colorful-error-color: #921341;
  --colorful-error-extra-color: #640b2c;
}

/* .word.error{
  border-bottom: double 4px var(--error-color);
} */

#menu .icon-button:nth-child(1) {
  color: var(--caret-color);
}

==> ./monkeytype/static/themes/botanical.css <==
:root {
  --bg-color: #7b9c98;
  --main-color: #eaf1f3;
  --caret-color: #abc6c4;
  --sub-color: #495755;
  --text-color: #eaf1f3;
  --error-color: #f6c9b4;
  --error-extra-color: #f59a71;
  --colorful-error-color: #f6c9b4;
  --colorful-error-extra-color: #f59a71;
}

==> ./monkeytype/static/themes/iceberg_dark.css <==
:root {
  --bg-color: #161821;
  --caret-color: #d2d4de;
  --main-color: #84a0c6;
  --sub-color: #595e76;
  --text-color: #c6c8d1;
  --error-color: #e27878;
  --error-extra-color: #e2a478;
  --colorful-error-color: #e27878;
  --colorful-error-extra-color: #e2a478;
}

==> ./monkeytype/static/themes/aether.css <==
:root {
  --bg-color: #101820;
  --main-color: #eedaea;
  --caret-color: #eedaea;
  --sub-color: #cf6bdd;
  --text-color: #eedaea;
  --error-color: #ff5253;
  --error-extra-color: #e3002b;
  --colorful-error-color: #ff5253;
  --colorful-error-extra-color: #e3002b;
}

#menu .icon-button:nth-child(1) {
  color: #e4002b;
}

#menu .icon-button:nth-child(2) {
  color: #c53562;
}

#menu .icon-button:nth-child(3) {
  color: #95549e;
}

#menu .icon-button:nth-child(4) {
  color: #6744a1;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #393c73;
}

==> ./monkeytype/static/themes/magic_girl.css <==
:root {
  --bg-color: #ffffff;
  --main-color: #f5b1cc;
  --caret-color: #e45c96;
  --sub-color: #93e8d3;
  --text-color: #00ac8c;
  --error-color: #ffe495;
  --error-extra-color: #e45c96;
  --colorful-error-color: #ffe485;
  --colorful-error-extra-color: #e45c96;
}

==> ./monkeytype/static/themes/mashu.css <==
:root {
  --bg-color: #2b2b2c;
  --main-color: #76689a;
  --caret-color: #76689a;
  --sub-color: #d8a0a6;
  --text-color: #f1e2e4;
  --error-color: #d44729;
  --error-extra-color: #8f2f19;
  --colorful-error-color: #d44729;
  --colorful-error-extra-color: #8f2f19;
}

==> ./monkeytype/static/themes/dark_magic_girl.css <==
:root {
  --bg-color: #091f2c;
  --main-color: #f5b1cc;
  --caret-color: #93e8d3;
  --sub-color: #93e8d3;
  --text-color: #a288d9;
  --error-color: #e45c96;
  --error-extra-color: #e45c96;
  --colorful-error-color: #00b398;
  --colorful-error-extra-color: #e45c96;
}

==> ./monkeytype/static/themes/mint.css <==
:root {
  --bg-color: #05385b;
  --main-color: #5cdb95;
  --caret-color: #5cdb95;
  --sub-color: #20688a;
  --text-color: #edf5e1;
  --error-color: #f35588;
  --error-extra-color: #a3385a;
  --colorful-error-color: #f35588;
  --colorful-error-extra-color: #a3385a;
}

==> ./monkeytype/static/themes/rudy.css <==
:root {
  --bg-color: #1a2b3e;
  --caret-color: #af8f5c;
  --main-color: #af8f5c;
  --sub-color: #3a506c;
  --text-color: #c9c8bf;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/dev.css <==
/*this theme is based on "Dev theme by KDr3w" color pallet: https://www.deviantart.com/kdr3w/art/Dev-825722799 */
:root {
    --bg-color: #1b2028;
    --main-color: #23a9d5;
    --caret-color: #4b5975;
    --sub-color: #4b5975;
    --text-color: #ccccb5;
    --error-color: #b81b2c;
    --error-extra-color: #84131f;
    --colorful-error-color: #b81b2c;
    --colorful-error-extra-color: #84131f;
}

==> ./monkeytype/static/themes/dollar.css <==
:root {
  --bg-color: #e4e4d4;
  --main-color: #6b886b;
  --caret-color: #424643;
  --sub-color: #8a9b69;
  --text-color: #555a56;
  --error-color: #d60000;
  --error-extra-color: #f68484;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/onedark.css <==
:root {
  --bg-color: #2f343f;
  --caret-color: #61afef;
  --main-color: #61afef;
  --sub-color: #eceff4;
  --text-color: #98c379;
  --error-color: #e06c75;
  --error-extra-color: #d62436;
  --colorful-error-color: #d62436;
  --colorful-error-extra-color: #ff0019;
}

==> ./monkeytype/static/themes/red_dragon.css <==
:root {
  --bg-color: #1a0b0c;
  --main-color: #ff3a32;
  --caret-color: #ff3a32;
  --sub-color: #e2a528;
  --text-color: #4a4d4e;
  --error-color: #771b1f;
  --error-extra-color: #591317;
  --colorful-error-color: #771b1f;
  --colorful-error-extra-color: #591317;
}

==> ./monkeytype/static/themes/tiramisu.css <==
:root {
  --bg-color: #cfc6b9;
  --main-color: #c0976f;
  --caret-color: #7d5448;
  --sub-color: #c0976f;
  --text-color: #7d5448;
  --error-color: #e9632d;
  --error-extra-color: #e9632d;
  --colorful-error-color: #e9632d;
  --colorful-error-extra-color: #e9632d;
}

==> ./monkeytype/static/themes/midnight.css <==
:root {
  --bg-color: #0b0e13;
  --main-color: #60759f;
  --caret-color: #60759f;
  --sub-color: #394760;
  --text-color: #9fadc6;
  --error-color: #c27070;
  --error-extra-color: #c28b70;
  --colorful-error-color: #c27070;
  --colorful-error-extra-color: #c28b70;
}

==> ./monkeytype/static/themes/dots.css <==
:root {
  --bg-color: #121520;
  --caret-color: #fff;
  --main-color: #fff;
  --sub-color: #676e8a;
  --text-color: #fff;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

#menu {
  gap: 0.5rem;
}

#top.focus #menu .icon-button,
#top.focus #menu:before,
#top.focus #menu:after {
  background: var(--sub-color);
}

#menu .icon-button {
  border-radius: 10rem !important;
  color: #121520;
}

/* #menu:before{
    content: "";
    background: #f94348;
    width: 1.25rem;
    height: 1.25rem;
    padding: .5rem;
    border-radius: 10rem;
} */

#menu .icon-button:nth-child(1) {
  background: #f94348;
}

#menu .icon-button:nth-child(2) {
  background: #9261ff;
}

#menu .icon-button:nth-child(3) {
  background: #3cc5f8;
}

#menu .icon-button:nth-child(4) {
  background: #4acb8a;
}

#menu .icon-button:nth-child(5) {
  background: #ffd543;
}

#menu .icon-button:nth-child(6),
#menu .icon-button:nth-child(7) {
  background: #ff9349;
}

/* #menu:after{
    content: "";
    background: #ff9349;
    width: 1.25rem;
    height: 1.25rem;
    padding: .5rem;
    border-radius: 10rem;
} */

#top.focus #menu .icon-button.discord::after {
  border-color: transparent;
}

==> ./monkeytype/static/themes/ez_mode.css <==
:root {
  --bg-color: #0068c6;
  --main-color: #fa62d5;
  --caret-color: #4ddb47;
  --sub-color: #f5f5f5;
  --text-color: #fa62d5;
  --error-color: #4ddb47;
  --error-extra-color: #42ba3b;
  --colorful-error-color: #4ddb47;
  --colorful-error-extra-color: #42ba3b;
}

.pageSettings .section h1 {
  color: var(--text-color);
}

.pageSettings .section > .text {
  color: var(--sub-color);
}

.pageAbout .section .title {
  color: var(--text-color);
}

.pageAbout .section p {
  color: var(--sub-color);
}

#leaderboardsWrapper #leaderboards .title {
  color: var(--sub-color);
}

#leaderboardsWrapper #leaderboards .tables table thead {
  color: var(--sub-color);
}

#leaderboardsWrapper #leaderboards .tables table tbody {
  color: var(--sub-color);
}

==> ./monkeytype/static/themes/matrix.css <==
:root {
  --bg-color: #000000;
  --main-color: #15ff00;
  --caret-color: #15ff00;
  --sub-color: #003B00;
  --text-color: #adffa7;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

#liveWpm,
#timerNumber {
  color: white;
}

==> ./monkeytype/static/themes/aurora.css <==
:root {
  --bg-color: #011926;
  --main-color: #00e980;
  --caret-color: #00e980;
  --sub-color: #245c69;
  --text-color: #fff;
  --error-color: #b94da1;
  --error-extra-color: #9b3a76;
  --colorful-error-color: #b94da1;
  --colorful-error-extra-color: #9b3a76;
}

@keyframes rgb {
  0% {
    color: #009fb4;
  }
  25% {
    color: #00e975;
  }
  50% {
    color: #00ffea;
  }
  75% {
    color: #00e975;
  }
  100% {
    color: #009fb4;
  }
}

@keyframes rgb-bg {
  0% {
    background: #009fb4;
  }
  25% {
    background: #00e975;
  }
  50% {
    background: #00ffea;
  }
  75% {
    background: #00e975;
  }
  100% {
    background: #009fb4;
  }
}

.button.discord::after,
#caret,
.pageSettings .section .buttons .button.active,
.pageSettings .section.languages .buttons .language.active,
.pageAccount .group.filterButtons .buttons .button.active {
  animation: rgb-bg 5s linear infinite;
}

#top.focus .button.discord::after,
#top .button.discord.dotHidden::after {
  animation-name: none !important;
}

.logo .bottom,
#top .config .group .buttons .text-button.active,
#result .stats .group .bottom,
#menu .icon-button:hover,
#top .config .group .buttons .text-button:hover,
a:hover,
#words.flipped .word {
  animation: rgb 5s linear infinite;
}

#words.flipped .word letter.correct {
  color: var(--sub-color);
}

#words:not(.flipped) .word letter.correct {
  animation: rgb 5s linear infinite;
}

==> ./monkeytype/static/themes/night_runner.css <==
:root {
  --bg-color: #212121;
  --main-color: #feff04;
  --caret-color: #feff04;
  --sub-color: #5c4a9c;
  --text-color: #e8e8e8;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

==> ./monkeytype/static/themes/taro.css <==
:root {
  /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */
  --bg-color: #b3baff;
  --main-color: #130f1a;
  --caret-color: #00e9e5;
  --sub-color: #6f6c91;
  --text-color: #130f1a;
  --error-color: #ffe23e;
  --error-extra-color: #fff1c3;
  --colorful-error-color: #ffe23e;
  --colorful-error-extra-color: #fff1c3;
}

.word.error {
  border-bottom: dotted 2px var(--text-color);
}

#menu .icon-button:nth-child(1) {
  background: var(--caret-color);
  border-radius: 50%;
}

#menu .icon-button:nth-child(2) {
  background: var(--error-color);
  border-radius: 50%;
}

==> ./monkeytype/static/themes/vscode.css <==
:root {
  --bg-color: #1e1e1e;
  --main-color: #007acc;
  --caret-color: #569cd6;
  --sub-color: #4d4d4d;
  --text-color: #d4d4d4;
  --error-color: #f44747;
  --error-extra-color: #f44747;
  --colorful-error-color: #f44747;
  --colorful-error-extra-color: #f44747;
}

==> ./monkeytype/static/themes/nord.css <==
:root {
  --bg-color: #242933;
  --caret-color: #d8dee9;
  --main-color: #d8dee9;
  --sub-color: #617b94;
  --text-color: #d8dee9;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/iceberg_light.css <==
:root {
  --bg-color: #e8e9ec;
  --caret-color: #262a3f;
  --main-color: #2d539e;
  --sub-color: #adb1c4;
  --text-color: #33374c;
  --error-color: #cc517a;
  --error-extra-color: #cc3768;
  --colorful-error-color: #cc517a;
  --colorful-error-extra-color: #cc3768;
}

==> ./monkeytype/static/themes/wavez.css <==
:root {
  --bg-color: #1c292f;
  --main-color: #6bde3b;
  --caret-color: #6bde3b;
  --sub-color: #1a454e;
  --text-color: #e9efe6;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/pink_lemonade.css <==
:root {
  --bg-color: #f6d992;
  --main-color: #f6a192;
  --caret-color: #fcfcf8;
  --sub-color: #f6b092;
  --text-color: #fcfcf8;
  --error-color: #ff6f69;
  --error-extra-color: #ff6f69;
  --colorful-error-color: #ff6f69;
  --colorful-error-extra-color: #ff6f69;
}

==> ./monkeytype/static/themes/dualshot.css <==
:root {
  --bg-color: #737373;
  --main-color: #212222;
  --caret-color: #212222;
  --sub-color: #aaaaaa;
  --text-color: #212222;
  --error-color: #c82931;
  --error-extra-color: #ac1823;
  --colorful-error-color: #c82931;
  --colorful-error-extra-color: #ac1823;
}

#menu .icon-button:nth-child(1) {
  color: #2884bb;
}

#menu .icon-button:nth-child(2) {
  color: #25a5a9;
}

#menu .icon-button:nth-child(3) {
  color: #de9c24;
}

#menu .icon-button:nth-child(4) {
  color: #d82231;
}

#menu .icon-button:nth-child(5) {
  color: #212222;
}

#menu .icon-button:nth-child(6) {
  color: #212222;
}

==> ./monkeytype/static/themes/material.css <==
:root {
  --bg-color: #263238;
  --main-color: #80cbc4;
  --caret-color: #80cbc4;
  --sub-color: #4c6772;
  --text-color: #e6edf3;
  --error-color: #fb4934;
  --error-extra-color: #cc241d;
  --colorful-error-color: #fb4934;
  --colorful-error-extra-color: #cc241d;
}

==> ./monkeytype/static/themes/paper.css <==
:root {
  --bg-color: #eeeeee;
  --main-color: #444444;
  --caret-color: #444444;
  --sub-color: #b2b2b2;
  --text-color: #444444;
  --error-color: #d70000;
  --error-extra-color: #d70000;
  --colorful-error-color: #d70000;
  --colorful-error-extra-color: #d70000;
}

==> ./monkeytype/static/themes/metropolis.css <==
:root {
  --bg-color: #0f1f2c;
  --main-color: #56c3b7;
  --caret-color: #56c3b7;
  --sub-color: #326984;
  --text-color: #e4edf1;
  --error-color: #d44729;
  --error-extra-color: #8f2f19;
  --colorful-error-color: #d44729;
  --colorful-error-extra-color: #8f2f19;
}

#top .logo .bottom {
  color: #f4bc46;
}

#menu .icon-button:nth-child(1) {
  color: #d44729;
}

#menu .icon-button:nth-child(2) {
  color: #d44729;
}

#menu .icon-button:nth-child(3) {
  color: #d44729;
}

#menu .icon-button:nth-child(4) {
  color: #d44729;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6),
#menu .icon-button:nth-child(7) {
  color: #d44729;
}

==> ./monkeytype/static/themes/nebula.css <==
:root {
  --bg-color: #212135;
  --main-color: #be3c88;
  --caret-color: #78c729;
  --sub-color: #19b3b8;
  --text-color: #838686;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/alpine.css <==
:root {
  --bg-color: #6c687f; /*Background*/
  --main-color: #ffffff; /*Color after typing, monkeytype logo, WPM Number acc number etc*/
  --caret-color: #585568; /*Cursor Color*/
  --sub-color: #9994b8; /*WPM text color of scrollbar and general color, before typed color*/
  --text-color: #ffffff; /*Color of text after hovering over it*/
  --error-color: #e32b2b;
  --error-extra-color: #a62626;
  --colorful-error-color: #e32b2b;
  --colorful-error-extra-color: #a62626;
}

==> ./monkeytype/static/themes/rgb.css <==
:root {
  --bg-color: #111;
  --main-color: #eee;
  --caret-color: #eee;
  --sub-color: #444;
  --text-color: #eee;
  --error-color: #eee;
  --error-extra-color: #b3b3b3;
  --colorful-error-color: #eee;
  --colorful-error-extra-color: #b3b3b3;
}

@keyframes rgb {
  0% {
    color: #f44336;
  }
  25% {
    color: #ffc107;
  }
  50% {
    color: #4caf50;
  }
  75% {
    color: #3f51b5;
  }
  100% {
    color: #f44336;
  }
}

@keyframes rgb-bg {
  0% {
    background: #f44336;
  }
  25% {
    background: #ffc107;
  }
  50% {
    background: #4caf50;
  }
  75% {
    background: #3f51b5;
  }
  100% {
    background: #f44336;
  }
}

.button.discord::after,
#caret,
.pageSettings .section .buttons .button.active,
.pageSettings .section.languages .buttons .language.active,
.pageAccount .group.filterButtons .buttons .button.active {
  animation: rgb-bg 5s linear infinite;
}

#top.focus .button.discord::after,
#top .button.discord.dotHidden::after {
  animation-name: none !important;
}

.logo .bottom,
#top .config .group .buttons .text-button.active,
#result .stats .group .bottom,
#menu .icon-button:hover,
#top .config .group .buttons .text-button:hover,
a:hover,
#words.flipped .word {
  animation: rgb 5s linear infinite;
}

/* .word letter.correct{
  animation: rgb 5s linear infinite;

}  */

#words.flipped .word letter.correct {
  color: var(--sub-color);
}

#words:not(.flipped) .word letter.correct {
  animation: rgb 5s linear infinite;
}

==> ./monkeytype/static/themes/hanok.css <==
:root {
  --bg-color: #d8d2c3;
  --main-color: #513a2a;
  --caret-color: #513a2a;
  --sub-color: #513a2a;
  --text-color: #393b3b;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/serika_dark.css <==
:root {
  --bg-color: #323437;
  --main-color: #e2b714;
  --caret-color: #e2b714;
  --sub-color: #646669;
  --text-color: #d1d0c5;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/modern_ink.css <==
:root {
  --bg-color: #ffffff;
  --main-color: #ff360d;
  --caret-color: #ff0000;
  --sub-color: #b7b7b7;
  --text-color: #000000;
  --error-color: #d70000;
  --error-extra-color: #b00000;
  --colorful-error-color: #ff1c1c;
  --colorful-error-extra-color: #b00000;
}

#menu .icon-button:nth-child(1) {
  color: #ff0000;
}

#menu .icon-button:nth-child(5) {
  color: #ff0000;
}

/* kinda confusing to type with this */
/* .word letter.incorrect {
  -webkit-transform: scale(0.5) translate(-100%, -100%);
}

.word letter.incorrect.extra {
  -webkit-transform: scale(0.5);
} */

.word.error {
  border-bottom: solid 2px #ff0000;
}

==> ./monkeytype/static/themes/muted.css <==
:root {
    --bg-color: #525252;
    --main-color: #C5B4E3;
    --caret-color: #B1E4E3;
    --sub-color: #939eae;
    --text-color: #B1E4E3;
    --error-color: #EDC1CD;
  }

==> ./monkeytype/static/themes/nautilus.css <==
:root {
  --bg-color: #132237;
  --main-color: #ebb723;
  --caret-color: #ebb723;
  --sub-color: #0b4c6c;
  --text-color: #1cbaac;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

==> ./monkeytype/static/themes/miami_nights.css <==
:root {
  --bg-color: #18181a;
  --main-color: #e4609b;
  --caret-color: #e4609b;
  --sub-color: #47bac0;
  --text-color: #fff;
  --error-color: #fff591;
  --error-extra-color: #b6af68;
  --colorful-error-color: #fff591;
  --colorful-error-extra-color: #b6af68;
}

==> ./monkeytype/static/themes/moonlight.css <==
/*inspired by GMK MOONLIGHT*/
:root {
  --bg-color: #1f2730;
  --main-color: #c69f68;
  --caret-color: #8f744b;
  --sub-color: #4b5975;
  --text-color: #ccccb5;
  --error-color: #b81b2c;
  --error-extra-color: #84131f;
  --colorful-error-color: #b81b2c;
  --colorful-error-extra-color: #84131f;
}
#menu {
  gap: 0.5rem;
}
#top.focus #menu .icon-button,
#top.focus #menu:before,
#top.focus #menu:after {
  background: var(--bg-color);
}
#menu .icon-button {
  border-radius: rem !important;
  color: #1f2730 !important;
}
#menu .icon-button :hover {
  border-radius: rem !important;
  color: #4b5975 !important;
  transition: 0.25s;
}
#menu .icon-button:nth-child(1) {
  background: #c69f68;
}
#menu .icon-button:nth-child(2) {
  background: #c69f68;
}
#menu .icon-button:nth-child(3) {
  background: #c69f68;
}
#menu .icon-button:nth-child(4) {
  background: #c69f68;
}
#menu .icon-button:nth-child(5) {
  background: #c69f68;
}
#menu .icon-button:nth-child(6),
#menu .icon-button:nth-child(7) {
  background: #c69f68;
}
#top.focus #menu .icon-button.discord::after {
  border-color: transparent;
}

==> ./monkeytype/static/themes/darling.css <==
:root {
  --bg-color: #fec8cd;
  --main-color: #ffffff;
  --caret-color: #ffffff;
  --sub-color: #a30000;
  --text-color: #ffffff;
  --error-color: #2e7dde;
  --error-extra-color: #2e7dde;
  --colorful-error-color: #2e7dde;
  --colorful-error-extra-color: #2e7dde;
  --font: Roboto Mono;
}

==> ./monkeytype/static/themes/pastel.css <==
:root {
  --bg-color: #e0b2bd;
  --main-color: #fbf4b6;
  --caret-color: #fbf4b6;
  --sub-color: #b4e9ff;
  --text-color: #6d5c6f;
  --error-color: #ff6961;
  --error-extra-color: #c23b22;
  --colorful-error-color: #ff6961;
  --colorful-error-extra-color: #c23b22;
}

==> ./monkeytype/static/themes/dark.css <==
:root {
  --bg-color: #111;
  --main-color: #eee;
  --caret-color: #eee;
  --sub-color: #444;
  --text-color: #eee;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

==> ./monkeytype/static/themes/ishtar.css <==
:root {
  --bg-color: #202020;
  --main-color: #91170c;
  --caret-color: #c58940;
  --sub-color: #847869;
  --text-color: #fae1c3;
  --error-color: #bb1e10;
  --error-extra-color: #791717;
  --colorful-error-color: #c5da33;
  --colorful-error-extra-color: #849224;
}

#top .logo .bottom {
  color: #fae1c3;
}

==> ./monkeytype/static/themes/retro.css <==
:root {
  --bg-color: #dad3c1;
  --main-color: #1d1b17;
  --caret-color: #1d1b17;
  --sub-color: #918b7d;
  --text-color: #1d1b17;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/stealth.css <==
:root {
  --bg-color: #010203;
  --main-color: #383e42;
  --caret-color: #e25303;
  --sub-color: #5e676e;
  --text-color: #383e42;
  --error-color: #e25303;
  --error-extra-color: #73280c;
  --colorful-error-color: #e25303;
  --colorful-error-extra-color: #73280c;
}
#menu .icon-button:nth-child(4) {
  color: #e25303;
}
#timerNumber {
  color: #5e676e;
}

==> ./monkeytype/static/themes/repose_dark.css <==
:root {
  --bg-color: #2F3338;
  --main-color: #D6D2BC;
  --caret-color: #D6D2BC;
  --sub-color: #8F8E84;
  --text-color: #D6D2BC;
  --error-color: #FF4A59;
  --error-extra-color: #C43C53;
  --colorful-error-color: #FF4A59;
  --colorful-error-extra-color: #C43C53;
}

==> ./monkeytype/static/themes/lime.css <==
:root {
  --bg-color: #7c878e;
  --main-color: #93c247;
  --caret-color: #93c247;
  --sub-color: #4b5257;
  --text-color: #bfcfdc;
  --error-color: #ea4221;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ea4221;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/blueberry_light.css <==
:root {
  --bg-color: #dae0f5;
  --main-color: #506477;
  --caret-color: #df4576;
  --sub-color: #92a4be;
  --text-color: #678198;
  --error-color: #df4576;
  --error-extra-color: #d996ac;
  --colorful-error-color: #df4576;
  --colorful-error-extra-color: #d996ac;
}

#top .logo .bottom {
  color: #df4576;
}

==> ./monkeytype/static/themes/fruit_chew.css <==
:root {
  --bg-color: #d6d3d6;
  --main-color: #5c1e5f;
  --caret-color: #b92221;
  --sub-color: #b49cb5;
  --text-color: #282528;
  --error-color: #bd2621;
  --error-extra-color: #a62626;
  --colorful-error-color: #bd2621;
  --colorful-error-extra-color: #a62626;
}

#menu .icon-button:nth-child(1) {
  color: #a6bf50;
}

#menu .icon-button:nth-child(2) {
  color: #c3921a;
}

#menu .icon-button:nth-child(3) {
  color: #b92221;
}

#menu .icon-button:nth-child(4) {
  color: #88b6ce;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #661968;
}

==> ./monkeytype/static/themes/olivia.css <==
:root {
  --bg-color: #1c1b1d;
  --main-color: #deaf9d;
  --caret-color: #deaf9d;
  --sub-color: #4e3e3e;
  --text-color: #f2efed;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #e03d4e;
  --colorful-error-extra-color: #aa2f3b;
}

==> ./monkeytype/static/themes/diner.css <==
:root {
  --bg-color: #537997;
  --main-color: #c3af5b;
  --caret-color: #ad5145;
  --sub-color: #445c7f;
  --text-color: #dfdbc8;
  --error-color: #ad5145;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ad5145;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/sewing_tin_light.css <==
:root {
  --bg-color: #ffffff;
  --main-color: #2d2076;
  --caret-color: #fbdb8c;
  --sub-color: #385eca;
  --text-color: #2d2076;
  --error-color: #f2ce83;
  --error-extra-color: #f2ce83;
  --colorful-error-color: #f2ce83;
  --colorful-error-extra-color: #f2ce83;
}

#menu .icon-button {
  color: #f2ce83;
}

#menu .icon-button:hover {
  color: #c6915e;
}

#top .logo .text {
  background-color: #ffffff; /* fallback */
  background: -webkit-linear-gradient(
    #2d2076,
    #2d2076 25%,
    #2e3395 25%,
    #2e3395 50%,
    #3049ba 50%,
    #3049ba 75%,
    #385eca 75%,
    #385eca
  );
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

#top .logo .text .top {
  /* prevent it from being transparent */
  -webkit-text-fill-color: #385eca;
}

==> ./monkeytype/static/themes/carbon.css <==
:root {
  --bg-color: #313131;
  --main-color: #f66e0d;
  --caret-color: #f66e0d;
  --sub-color: #616161;
  --text-color: #f5e6c8;
  --error-color: #e72d2d;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #e72d2d;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/cafe.css <==
:root {
  --bg-color: #ceb18d;
  --main-color: #14120f;
  --caret-color: #14120f;
  --sub-color: #d4d2d1;
  --text-color: #14120f;
  --error-color: #c82931;
  --error-extra-color: #ac1823;
  --colorful-error-color: #c82931;
  --colorful-error-extra-color: #ac1823;
}

==> ./monkeytype/static/themes/future_funk.css <==
:root {
  --bg-color: #2e1a47;
  --main-color: #f7f2ea;
  --caret-color: #f7f2ea;
  --sub-color: #c18fff;
  --text-color: #f7f2ea;
  --error-color: #f04e98;
  --error-extra-color: #bd1c66;
  --colorful-error-color: #f04e98;
  --colorful-error-extra-color: #bd1c66;
}

#menu .icon-button:nth-child(1) {
  color: #f04e98;
}

#menu .icon-button:nth-child(2) {
  color: #f8bed6;
}

#menu .icon-button:nth-child(3) {
  color: #f6eb61;
}

#menu .icon-button:nth-child(4) {
  color: #a4dbe8;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #a266ed;
}

==> ./monkeytype/static/themes/fire.css <==
:root {
  --bg-color: #0f0000;
  --main-color: #b31313;
  --caret-color: #b31313;
  --sub-color: #683434;
  --text-color: #ffffff;
  --error-color: #2f3cb6;
  --error-extra-color: #434a8f;
  --colorful-error-color: #2f3cb6;
  --colorful-error-extra-color: #434a8f;
}

@keyframes rgb {
  0% {
    color: #b31313;
  }
  25% {
    color: #ff9000;
  }
  50% {
    color: #fdda16;
  }
  75% {
    color: #ff9000;
  }
  100% {
    color: #b31313;
  }
}

@keyframes rgb-bg {
  0% {
    background: #b31313;
  }
  25% {
    background: #ff9000;
  }
  50% {
    background: #fdda16;
  }
  75% {
    background: #ff9000;
  }
  100% {
    background: #b31313;
  }
}

.button.discord::after,
#caret,
.pageSettings .section .buttons .button.active,
.pageSettings .section.languages .buttons .language.active,
.pageAccount .group.filterButtons .buttons .button.active {
  animation: rgb-bg 5s linear infinite;
}

#top.focus .button.discord::after,
#top .button.discord.dotHidden::after {
  animation-name: none !important;
}

.logo .bottom,
#top .config .group .buttons .text-button.active,
#result .stats .group .bottom,
#menu .icon-button:hover,
#top .config .group .buttons .text-button:hover,
a:hover,
#words.flipped .word {
  animation: rgb 5s linear infinite;
}

#words.flipped .word letter.correct {
  color: var(--sub-color);
}

#words:not(.flipped) .word letter.correct {
  animation: rgb 5s linear infinite;
}

==> ./monkeytype/static/themes/striker.css <==
:root {
  --bg-color: #124883;
  --main-color: #d7dcda;
  --caret-color: #d7dcda;
  --sub-color: #0f2d4e;
  --text-color: #d6dbd9;
  --error-color: #fb4934;
  --error-extra-color: #cc241d;
  --colorful-error-color: #fb4934;
  --colorful-error-extra-color: #cc241d;
}

==> ./monkeytype/static/themes/monokai.css <==
:root {
  --bg-color: #272822;
  --main-color: #a6e22e;
  --caret-color: #66d9ef;
  --sub-color: #e6db74;
  --text-color: #e2e2dc;
  --error-color: #f92672;
  --error-extra-color: #fd971f;
  --colorful-error-color: #f92672;
  --colorful-error-extra-color: #fd971f;
}

==> ./monkeytype/static/themes/8008.css <==
:root {
  --bg-color: #333a45;
  --main-color: #f44c7f;
  --caret-color: #f44c7f;
  --sub-color: #939eae;
  --text-color: #e9ecf0;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #c5da33;
  --colorful-error-extra-color: #849224;
}

==> ./monkeytype/static/themes/froyo.css <==
:root {
  --bg-color: #e1dacb;
  --main-color: #7b7d7d;
  --caret-color: #7b7d7d;
  --sub-color: #b29c5e;
  --text-color: #7b7d7d;
  --error-color: #f28578;
  --error-extra-color: #d56558;
  --colorful-error-color: #f28578;
  --colorful-error-extra-color: #d56558;
}

#menu .icon-button:nth-child(1) {
  color: #ff7e73;
}

#menu .icon-button:nth-child(2) {
  color: #f5c370;
}

#menu .icon-button:nth-child(3) {
  color: #08d9a3;
}

#menu .icon-button:nth-child(4) {
  color: #0ca5e2;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #875ac6;
}

==> ./monkeytype/static/themes/evil_eye.css <==
:root {
  --bg-color: #0084c2;
  --main-color: #f7f2ea;
  --caret-color: #f7f2ea;
  --sub-color: #01589f;
  --text-color: #171718;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/deku.css <==
:root {
  --bg-color: #058b8c;
  --main-color: #b63530;
  --caret-color: #b63530;
  --sub-color: #255458;
  --text-color: #f7f2ea;
  --error-color: #b63530;
  --error-extra-color: #530e0e;
  --colorful-error-color: #ddca1f;
  --colorful-error-extra-color: #8f8610;
}

==> ./monkeytype/static/themes/alduin.css <==
:root {
  --bg-color: #1c1c1c;
  --main-color: #dfd7af;
  --caret-color: #e3e3e3;
  --sub-color: #444444;
  --text-color: #f5f3ed;
  --error-color: #af5f5f;
  --error-extra-color: #4d2113;
  --colorful-error-color: #af5f5f;
  --colorful-error-extra-color: #4d2113;
}

==> ./monkeytype/static/themes/dracula.css <==
:root {
  --bg-color: #282a36;
  --main-color: #f2f2f2;
  --caret-color: #f2f2f2;
  --sub-color: #bd93f9;
  --text-color: #f2f2f2;
  --error-color: #f758a0;
  --error-extra-color: #732e51;
  --colorful-error-color: #f758a0;
  --colorful-error-extra-color: #732e51;
}

#menu .icon-button:nth-child(1) {
  color: #ec75c4;
}

#menu .icon-button:nth-child(2) {
  color: #8be9fd;
}

#menu .icon-button:nth-child(3) {
  color: #50fa7b;
}

#menu .icon-button:nth-child(4) {
  color: #f1fa8c;
}

#menu .icon-button:nth-child(5) {
  color: #ffb86c;
}

#menu .icon-button:nth-child(6) {
  color: #ffb86c;
}

==> ./monkeytype/static/themes/metaverse.css <==
:root {
  --bg-color: #232323;
  --main-color: #d82934;
  --caret-color: #d82934;
  --sub-color: #5e5e5e;
  --text-color: #e8e8e8;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #d7da33;
  --colorful-error-extra-color: #737917;
}

==> ./monkeytype/static/themes/hammerhead.css <==
:root {
  --bg-color: #030613;
  --main-color: #4fcdb9;
  --caret-color: #4fcdb9;
  --sub-color: #1e283a;
  --text-color: #e2f1f5;
  --error-color: #e32b2b;
  --error-extra-color: #a62626;
  --colorful-error-color: #e32b2b;
  --colorful-error-extra-color: #a62626;
}

==> ./monkeytype/static/themes/bushido.css <==
:root {
  --bg-color: #242933;
  --main-color: #ec4c56;
  --caret-color: #ec4c56;
  --sub-color: #596172;
  --text-color: #f6f0e9;
  --error-color: #ec4c56;
  --error-extra-color: #9b333a;
  --colorful-error-color: #ecdc4c;
  --colorful-error-extra-color: #bdb03d;
}

==> ./monkeytype/static/themes/matcha_moccha.css <==
:root {
  --bg-color: #523525;
  --main-color: #7ec160;
  --caret-color: #7ec160;
  --sub-color: #9e6749;
  --text-color: #ecddcc;
  --error-color: #fb4934;
  --error-extra-color: #cc241d;
  --colorful-error-color: #fb4934;
  --colorful-error-extra-color: #cc241d;
}

==> ./monkeytype/static/themes/modern_dolch.css <==
:root {
  --bg-color: #2d2e30;
  --main-color: #7eddd3;
  --caret-color: #7eddd3;
  --sub-color: #54585c;
  --text-color: #e3e6eb;
  --error-color: #d36a7b;
  --error-extra-color: #994154;
  --colorful-error-color: #d36a7b;
  --colorful-error-extra-color: #994154;
}

==> ./monkeytype/static/themes/creamsicle.css <==
:root {
  --bg-color: #ff9869;
  --main-color: #fcfcf8;
  --caret-color: #fcfcf8;
  --sub-color: #ff661f;
  --text-color: #fcfcf8;
  --error-color: #6a0dad;
  --error-extra-color: #6a0dad;
  --colorful-error-color: #6a0dad;
  --colorful-error-extra-color: #6a0dad;
}

==> ./monkeytype/static/themes/strawberry.css <==
:root {
  --bg-color: #f37f83;
  --main-color: #fcfcf8;
  --caret-color: #fcfcf8;
  --sub-color: #e53c58;
  --text-color: #fcfcf8;
  --error-color: #fcd23f;
  --error-extra-color: #d7ae1e;
  --colorful-error-color: #fcd23f;
  --colorful-error-extra-color: #d7ae1e;
}

==> ./monkeytype/static/themes/shadow.css <==
:root {
  --bg-color: #000;
  --main-color: #eee;
  --caret-color: #eee;
  --sub-color: #444;
  --text-color: #eee;
  --error-color: #fff;
  --error-extra-color: #d8d8d8;
  --colorful-error-color: #fff;
  --colorful-error-extra-color: #d8d8d8;
}

#top .logo .icon{
  color: #8C3230;
}

#top .logo .text{
  color: #557D8D;
}

@keyframes shadow {
  to {
    color: #000;
  }
}

@keyframes shadow-repeat {
  50% {
    color: #000;
  }
  100% {
    color: #eee;
  }
}

#liveWpm,
#timerNumber {
  color: white;
}

#top .config .group .buttons .text-button.active,
#result .stats .group,
#menu .icon-button:hover,
#top .config .group .buttons .text-button:hover,
a:hover {
  animation: shadow-repeat 3s linear infinite forwards;
}

#logo,
#typingTest .word letter.correct {
  animation: shadow 5s linear 1 forwards;
}

==> ./monkeytype/static/themes/bento.css <==
:root {
  --bg-color: #2d394d;
  --main-color: #ff7a90;
  --caret-color: #ff7a90;
  --sub-color: #4a768d;
  --text-color: #fffaf8;
  --error-color: #ee2a3a;
  --error-extra-color: #f04040;
  --colorful-error-color: #fc2032;
  --colorful-error-extra-color: #f04040;
}

==> ./monkeytype/static/themes/menthol.css <==
:root {
  --bg-color: #00c18c;
  --main-color: #ffffff;
  --caret-color: #99fdd8;
  --sub-color: #186544;
  --text-color: #ffffff;
  --error-color: #e03c3c;
  --error-extra-color: #b12525;
  --colorful-error-color: #e03c3c;
  --colorful-error-extra-color: #b12525;
}

==> ./monkeytype/static/themes/our_theme.css <==
:root {
  --bg-color: #ce1226;
  --main-color: #fcd116;
  --caret-color: #fcd116;
  --sub-color: #6d0f19;
  --text-color: #ffffff;
  --error-color: #fcd116;
  --error-extra-color: #fcd116;
  --colorful-error-color: #1672fc;
  --colorful-error-extra-color: #1672fc;
}

==> ./monkeytype/static/themes/mr_sleeves.css <==
:root {
  --bg-color: #d1d7da;
  --main-color: #daa99b;
  --caret-color: #8fadc9;
  --sub-color: #9a9fa1;
  --text-color: #1d1d1d;
  --error-color: #bf6464;
  --error-extra-color: #793e44;
  --colorful-error-color: #8fadc9;
  --colorful-error-extra-color: #667c91;
}

#top .logo .bottom {
  color: #8fadc9;
}

#top .config .group .buttons .text-button.active {
  color: #daa99b;
}

/* #menu .icon-button:nth-child(1){
    color: #daa99b;
}

#menu .icon-button:nth-child(2){
    color: #daa99b;
}

#menu .icon-button:nth-child(3){
    color: #8fadc9;
}

#menu .icon-button:nth-child(4),
#menu .icon-button:nth-child(5){
    color: #8fadc9;
} */

==> ./monkeytype/static/themes/retrocast.css <==
:root {
  --bg-color: #07737a;
  --main-color: #88dbdf;
  --caret-color: #88dbdf;
  --sub-color: #f3e03b;
  --text-color: #ffffff;
  --error-color: #ff585d;
  --error-extra-color: #c04455;
  --colorful-error-color: #ff585d;
  --colorful-error-extra-color: #c04455;
}

#menu .icon-button:nth-child(1) {
  color: #88dbdf;
}

#menu .icon-button:nth-child(2) {
  color: #88dbdf;
}

#menu .icon-button:nth-child(3) {
  color: #88dbdf;
}

#menu .icon-button:nth-child(4) {
  color: #ff585d;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #f3e03b;
}

==> ./monkeytype/static/themes/frozen_llama.css <==
:root {
    --bg-color: #9bf2ea;
    --main-color: #6d44a6;
    --caret-color: #ffffff;
    --sub-color: #b690fd;
    --text-color: #ffffff;
    --error-color: #e42629;
    --error-extra-color: #e42629;
    --colorful-error-color: #e42629;
    --colorful-error-extra-color: #e42629;
}

==> ./monkeytype/static/themes/solarized_light.css <==
:root {
  --bg-color: #fdf6e3;
  --main-color: #859900;
  --caret-color: #dc322f;
  --sub-color: #2aa198;
  --text-color: #181819;
  --error-color: #d33682;
  --error-extra-color: #9b225c;
  --colorful-error-color: #d33682;
  --colorful-error-extra-color: #9b225c;
}

==> ./monkeytype/static/themes/bliss.css <==
:root {
  --bg-color: #262727;
  --main-color: #f0d3c9;
  --caret-color: #f0d3c9;
  --sub-color: #665957;
  --text-color: #fff;
  --error-color: #bd4141;
  --error-extra-color: #883434;
  --colorful-error-color: #bd4141;
  --colorful-error-extra-color: #883434;
}

==> ./monkeytype/static/themes/repose_light.css <==
:root {
  --bg-color: #EFEAD0;
  --main-color: #5F605E;
  --caret-color: #5F605E;
  --sub-color: #8F8E84;
  --text-color: #333538;
  --error-color: #C43C53;
  --error-extra-color: #A52632;
  --colorful-error-color: #C43C53;
  --colorful-error-extra-color: #A52632;
}

==> ./monkeytype/static/themes/fundamentals.css <==
:root {
  --bg-color: #727474;
  --main-color: #7fa482;
  --caret-color: #196378;
  --sub-color: #cac4be;
  --text-color: #131313;
  --error-color: #5e477c;
  --error-extra-color: #413157;
  --colorful-error-color: #5e477c;
  --colorful-error-extra-color: #413157;
}

#top .logo .bottom {
  color: #196378;
}

==> ./monkeytype/static/themes/miami.css <==
:root {
  --bg-color: #f35588;
  --main-color: #05dfd7;
  --caret-color: #a3f7bf;
  --text-color: #f0e9ec;
  --sub-color: #94294c;
  --error-color: #fff591;
  --error-extra-color: #b9b269;
  --colorful-error-color: #fff591;
  --colorful-error-extra-color: #b9b269;
}

==> ./monkeytype/static/themes/sewing_tin.css <==
:root {
  --bg-color: #241963;
  --main-color: #f2ce83;
  --caret-color: #fbdb8c;
  --sub-color: #446ad5;
  --text-color: #ffffff;
  --error-color: #c6915e;
  --error-extra-color: #c6915e;
  --colorful-error-color: #c6915e;
  --colorful-error-extra-color: #c6915e;
}

#menu .icon-button {
  color: #f2ce83;
}

#menu .icon-button:hover {
  color: #c6915e;
}

==> ./monkeytype/static/themes/fleuriste.css <==
:root {
  --bg-color: #c6b294;
  --main-color: #405a52;
  --caret-color: #8a785b;
  --sub-color: #64374d;
  --text-color: #091914;
  --error-color: #990000;
  --error-extra-color: #8a1414;
  --colorful-error-color: #a63a3a;
  --colorful-error-extra-color: #bd4c4c;
}

#menu .icon-button:nth-child(1, 3, 5) {
  background: #405a52;
}
#menu .icon-button:nth-child(2, 4) {
  background: #64374d;
}



==> ./monkeytype/static/themes/rose_pine.css <==
:root {
  --bg-color: #1f1d27; /*Background*/
  --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/
  --caret-color: #f6c177; /*Cursor Color*/
  --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/
  --text-color: #e0def4; /*Color of text after hovering over it*/
  --error-color: #eb6f92;
  --error-extra-color: #ebbcba;
  --colorful-error-color: #eb6f92;
  --colorful-error-extra-color: #ebbcba;
}

==> ./monkeytype/static/themes/sonokai.css <==
:root {
  --bg-color: #2c2e34;
  --main-color: #9ed072;
  --caret-color: #f38c71;
  --sub-color: #e7c664;
  --text-color: #e2e2e3;
  --error-color: #fc5d7c;
  --error-extra-color: #ecac6a;
  --colorful-error-color: #fc5d7c;
  --colorful-error-extra-color: #ecac6a;
}

==> ./monkeytype/static/themes/trackday.css <==
:root {
  --bg-color: #464d66;
  --main-color: #e0513e;
  --caret-color: #475782;
  --sub-color: #5c7eb9;
  --text-color: #cfcfcf;
  --error-color: #e44e4e;
  --error-extra-color: #fd3f3f;
  --colorful-error-color: #ff2e2e;
  --colorful-error-extra-color: #bb2525;
}

#menu .icon-button:nth-child(1) {
  color: #e0513e;
}

#menu .icon-button:nth-child(3) {
  color: #cfcfcf;
}

#menu .icon-button:nth-child(2) {
  color: #ccc500;
}

==> ./monkeytype/static/themes/milkshake.css <==
:root {
  --bg-color: #ffffff;
  --main-color: #212b43;
  --caret-color: #212b43;
  --sub-color: #62cfe6;
  --text-color: #212b43;
  --error-color: #f19dac;
  --error-extra-color: #e58c9d;
  --colorful-error-color: #f19dac;
  --colorful-error-extra-color: #e58c9d;
}

#menu .icon-button:nth-child(1) {
  color: #f19dac;
}

#menu .icon-button:nth-child(2) {
  color: #f6f4a0;
}

#menu .icon-button:nth-child(3) {
  color: #73e4d0;
}

#menu .icon-button:nth-child(4) {
  color: #61cfe6;
}

#menu .icon-button:nth-child(5) {
  color: #ba96db;
}

#menu .icon-button:nth-child(6) {
  color: #ba96db;
}

==> ./monkeytype/static/themes/trance.css <==
:root {
  --bg-color: #00021b;
  --main-color: #e51376;
  --caret-color: #e51376;
  --sub-color: #3c4c79;
  --text-color: #fff;
  --error-color: #02d3b0;
  --error-extra-color: #3f887c;
  --colorful-error-color: #02d3b0;
  --colorful-error-extra-color: #3f887c;
}

@keyframes rgb {
  0% {
    color: #e51376;
  }
  50% {
    color: #0e77ee;
  }
  100% {
    color: #e51376;
  }
}

@keyframes rgb-bg {
  0% {
    background: #e51376;
  }
  50% {
    background: #0e77ee;
  }
  100% {
    background: #e51376;
  }
}

.button.discord::after,
#caret,
.pageSettings .section .buttons .button.active,
.pageSettings .section.languages .buttons .language.active,
.pageAccount .group.filterButtons .buttons .button.active {
  animation: rgb-bg 5s linear infinite;
}

#top.focus .button.discord::after,
#top .button.discord.dotHidden::after {
  animation-name: none !important;
}

.logo .bottom,
#top .config .group .buttons .text-button.active,
#result .stats .group .bottom,
#menu .icon-button:hover,
#top .config .group .buttons .text-button:hover,
a:hover,
#words.flipped .word {
  animation: rgb 5s linear infinite;
}

#words.flipped .word letter.correct {
  color: var(--sub-color);
}

#words:not(.flipped) .word letter.correct {
  animation: rgb 5s linear infinite;
}

==> ./monkeytype/static/themes/nausea.css <==
:root {
  --bg-color: #323437;
  --main-color: #e2b714;
  --caret-color: #e2b714;
  --sub-color: #646669;
  --text-color: #d1d0c5;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

@keyframes woah {
  0% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2)
      scaleY(0.9);
  }

  25% {
    transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8);
  }

  50% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9)
      scaleY(0.9);
  }

  75% {
    transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5)
      scaleY(1.1);
  }

  100% {
    transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2)
      scaleY(0.9);
  }
}

@keyframes plsstop {
  0% {
    background: #323437;
  }

  50% {
    background: #3e4146;
  }

  100% {
    background: #323437;
  }
}

#middle {
  animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1);
}

#centerContent {
  transform: rotate(5deg);
  perspective: 500px;
}

body {
  animation: plsstop 10s infinite cubic-bezier(0.5, 0, 0.5, 1);
  overflow: hidden;
}

==> ./monkeytype/static/themes/superuser.css <==
:root {
  --bg-color: #262a33;
  --main-color: #43ffaf;
  --caret-color: #43ffaf;
  --sub-color: #526777;
  --text-color: #e5f7ef;
  --error-color: #ff5f5f;
  --error-extra-color: #d22a2a;
  --colorful-error-color: #ff5f5f;
  --colorful-error-extra-color: #d22a2a;
}

==> ./monkeytype/static/themes/serika.css <==
:root {
  --main-color: #e2b714;
  --caret-color: #e2b714;
  --sub-color: #aaaeb3;
  --bg-color: #e1e1e3;
  --text-color: #323437;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

==> ./monkeytype/static/themes/gruvbox_dark.css <==
:root {
 --bg-color: #282828;
 --main-color: #d79921;
 --caret-color: #fabd2f;
 --sub-color: #665c54;
 --text-color: #ebdbb2;
 --error-color: #fb4934;
 --error-extra-color: #cc241d;
 --colorful-error-color: #cc241d;
 --colorful-error-extra-color: #9d0006;
}

==> ./monkeytype/static/themes/godspeed.css <==
:root {
  --bg-color: #eae4cf;
  --main-color: #9abbcd;
  --caret-color: #f4d476;
  --sub-color: #c0bcab;
  --text-color: #646669;
  --error-color: #ca4754;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/joker.css <==
:root {
  --bg-color: #1a0e25;
  --main-color: #99de1e;
  --caret-color: #99de1e;
  --sub-color: #7554a3;
  --text-color: #e9e2f5;
  --error-color: #e32b2b;
  --error-extra-color: #a62626;
  --colorful-error-color: #e32b2b;
  --colorful-error-extra-color: #a62626;
}

==> ./monkeytype/static/themes/rose_pine_dawn.css <==
:root {
  --bg-color: #fffaf3; /*Background*/
  --main-color: #56949f; /*Color after typing, monkeytype logo, WPM Number acc number etc*/
  --caret-color: #ea9d34; /*Cursor Color*/
  --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/
  --text-color: #286983; /*Color of text after hovering over it*/
  --error-color: #b4637a;
  --error-extra-color: #d7827e;
  --colorful-error-color: #b4637a;
  --colorful-error-extra-color: #d7827e;
}

==> ./monkeytype/static/themes/grand_prix.css <==
:root {
  --bg-color: #36475c;
  --main-color: #c0d036;
  --caret-color: #c0d036;
  --sub-color: #5c6c80;
  --text-color: #c1c7d7;
  --error-color: #fc5727;
  --error-extra-color: #fc5727;
  --colorful-error-color: #fc5727;
  --colorful-error-extra-color: #fc5727;
}

==> ./monkeytype/static/themes/lavender.css <==
:root {
    --bg-color: #ada6c2;
    --main-color: #e4e3e9;
    --caret-color: #e4e3e9;
    --sub-color: #e4e3e9;
    --text-color: #2f2a41;
    --error-color: #ca4754;
    --error-extra-color: #7e2a33;
    --colorful-error-color: #ca4754;
    --colorful-error-extra-color: #7e2a33;
  }
  
  #menu .icon-button {
    border-radius: 10rem !important;
    background: #2f2a41;
    color: #e4e3e9;

  }
  
  #menu .icon-button:hover {
    color: #ada6c2;
  }
  
==> ./monkeytype/static/themes/watermelon.css <==
:root {
  --bg-color: #1f4437;
  --main-color: #d6686f;
  --caret-color: #d6686f;
  --sub-color: #3e7a65;
  --text-color: #cdc6bc;
  --error-color: #c82931;
  --error-extra-color: #ac1823;
  --colorful-error-color: #c82931;
  --colorful-error-extra-color: #ac1823;
}

==> ./monkeytype/static/themes/copper.css <==
:root {
  --bg-color: #442f29;
  --main-color: #b46a55;
  --caret-color: #c25c42;
  --sub-color: #7ebab5;
  --text-color: #e7e0de;
  --error-color: #a32424;
  --error-extra-color: #ec0909;
  --colorful-error-color: #a32424;
  --colorful-error-extra-color: #ec0909;
}

==> ./monkeytype/static/themes/beach.css <==
:root {
   --bg-color: #ffeead;
   --main-color: #96ceb4;
   --caret-color: #ffcc5c;
   --sub-color: #ffcc5c;
   --text-color: #5b7869;
   --error-color: #ff6f69;
   --error-extra-color: #ff6f69;
   --colorful-error-color: #ff6f69;
   --colorful-error-extra-color: #ff6f69;
 }

 #menu .icon-button:nth-child(1),
 #menu .icon-button:nth-child(2),
 #menu .icon-button:nth-child(3),
 #menu .icon-button:nth-child(4),
 #menu .icon-button:nth-child(5),
 #menu .icon-button:nth-child(6) {
   color: #ff6f69;
 }

==> ./monkeytype/static/themes/pulse.css <==
:root {
  --bg-color: #181818;
  --main-color: #17b8bd;
  --caret-color: #17b8bd;
  --sub-color: #53565a;
  --text-color: #e5f4f4;
  --error-color: #da3333;
  --error-extra-color: #791717;
  --colorful-error-color: #da3333;
  --colorful-error-extra-color: #791717;
}

==> ./monkeytype/static/themes/drowning.css <==
:root {
  --bg-color: #191826;
  --main-color: #4a6fb5;
  --caret-color: #4f85e8;
  --sub-color: #50688c;
  --text-color: #9393a7;
  --error-color: #be555f;
  --error-extra-color: #7e2a33;
  --colorful-error-color: #be555f;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/blueberry_dark.css <==
:root {
  --bg-color: #212b42;
  --main-color: #add7ff;
  --caret-color: #962f7e;
  --sub-color: #5c7da5;
  --text-color: #91b4d5;
  --error-color: #df4576;
  --error-extra-color: #d996ac;
  --colorful-error-color: #df4576;
  --colorful-error-extra-color: #d996ac;
}

#top .logo .bottom {
  color: #962f7e;
}

==> ./monkeytype/static/themes/arch.css <==
:root {
  --bg-color: #0c0d11;
  --main-color: #7ebab5;
  --caret-color: #7ebab5;
  --sub-color: #454864;
  --text-color: #f6f5f5;
  --error-color: #ff4754;
  --error-extra-color: #b02a33;
  --colorful-error-color: #ff4754;
  --colorful-error-extra-color: #b02a33;
}

==> ./monkeytype/static/themes/olive.css <==
:root {
  --bg-color: #e9e5cc;
  --caret-color: #92946f;
  --main-color: #92946f;
  --sub-color: #b7b39e;
  --text-color: #373731;
  --error-color: #cf2f2f;
  --error-extra-color: #a22929;
  --colorful-error-color: #cf2f2f;
  --colorful-error-extra-color: #a22929;
}

==> ./monkeytype/static/themes/luna.css <==
:root {
  --bg-color: #221c35;
  --main-color: #f67599;
  --caret-color: #f67599;
  --sub-color: #5a3a7e;
  --text-color: #ffe3eb;
  --error-color: #efc050;
  --error-extra-color: #c5972c;
  --colorful-error-color: #efc050;
  --colorful-error-extra-color: #c5972c;
}

==> ./monkeytype/static/themes/red_samurai.css <==
:root {
  --bg-color: #84202c;
  --main-color: #c79e6e;
  --caret-color: #c79e6e;
  --sub-color: #55131b;
  --text-color: #e2dad0;
  --error-color: #33bbda;
  --error-extra-color: #176b79;
  --colorful-error-color: #33bbda;
  --colorful-error-extra-color: #176779;
}

==> ./monkeytype/static/themes/cyberspace.css <==
:root {
  --bg-color: #181c18;
  --main-color: #00ce7c;
  --caret-color: #00ce7c;
  --sub-color: #9578d3;
  --text-color: #c2fbe1;
  --error-color: #ff5f5f;
  --error-extra-color: #d22a2a;
  --colorful-error-color: #ff5f5f;
  --colorful-error-extra-color: #d22a2a;
}

==> ./monkeytype/static/themes/shoko.css <==
:root {
  --bg-color: #ced7e0;
  --main-color: #81c4dd;
  --caret-color: #81c4dd;
  --sub-color: #7599b1;
  --text-color: #3b4c58;
  --error-color: #bf616a;
  --error-extra-color: #793e44;
  --colorful-error-color: #bf616a;
  --colorful-error-extra-color: #793e44;
}

==> ./monkeytype/static/themes/oblivion.css <==
:root {
  --bg-color: #313231;
  --main-color: #a5a096;
  --caret-color: #a5a096;
  --sub-color: #5d6263;
  --text-color: #f7f5f1;
  --error-color: #dd452e;
  --error-extra-color: #9e3423;
  --colorful-error-color: #dd452e;
  --colorful-error-extra-color: #9e3423;
}

#menu .icon-button:nth-child(1) {
  color: #9a90b4;
}

#menu .icon-button:nth-child(2) {
  color: #8db14b;
}

#menu .icon-button:nth-child(3) {
  color: #fca321;
}

#menu .icon-button:nth-child(4) {
  color: #2984a5;
}

#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #dd452e;
}

==> ./monkeytype/static/themes/comfy.css <==
:root {
  --bg-color: #4a5b6e;
  --main-color: #f8cdc6;
  --caret-color: #9ec1cc;
  --sub-color: #9ec1cc;
  --text-color: #f5efee;
  --error-color: #c9465e;
  --error-extra-color: #c9465e;
  --colorful-error-color: #c9465e;
  --colorful-error-extra-color: #c9465e;
}

==> ./monkeytype/static/themes/chaos_theory.css <==
:root {
  --bg-color: #141221;
  --main-color: #fd77d7;
  --caret-color: #dde5ed;
  --text-color: #dde5ed;
  --error-color: #fd77d7;
  --sub-color: #676e8a;
  --error-color: #FF5869;
  --error-extra-color: #b03c47;
  --colorful-error-color: #FF5869;
  --colorful-error-extra-color: #b03c47;
}

#top .logo .text {
  -webkit-transform: rotateY(180deg);
  unicode-bidi: bidi-override;
  transition: 0.5s;
}

#top .logo .top {
  font-family: "Comic Sans MS", "Comic Sans", cursive;
}

#top .logo .icon {
  -webkit-transform: rotateX(180deg);
  transition: 0.5s;
}

#words .incorrect.extra {
  -webkit-transform: rotateY(180deg);
  unicode-bidi: bidi-override;
  direction: rtl;
}

#bottom .leftright .right .current-theme .text {
  /* font-family: "Comic Sans MS", "Comic Sans", cursive; */
}

#caret {
  background-image: url(https://i.imgur.com/yN31JmJ.png);
  background-color: transparent;
  background-size: 1rem;
  background-position: center;
  background-repeat: no-repeat;
}

#caret.default {
  width: 4px;
}

.config .toggleButton {
  -webkit-transform: rotateY(180deg);
  unicode-bidi: bidi-override;
  direction: rtl;
  align-content: right;
}

.config .mode .text-button {
  -webkit-transform: rotateY(180deg);
  unicode-bidi: bidi-override;
  direction: rtl;
  align-content: right;
}

.config .wordCount .text-button,
.config .time .text-button,
.config .quoteLength .text-button,
.config .customText .text-button {
  -webkit-transform: rotateY(180deg);
  unicode-bidi: bidi-override;
  direction: rtl;
  align-content: right;
}

#top.focus #menu .icon-button,
#top.focus #menu:before,
#top.focus #menu:after {
  background: var(--sub-color);
  -webkit-transform: rotateY(180deg) !important;
}

#top.focus .logo .text,
#top.focus .logo:before,
#top.focus .logo:after {
  -webkit-transform: rotateY(0deg);
  direction: ltr;
}

#top.focus .logo .icon,
#top.focus .logo:before,
#top.focus .logo:after {
  -webkit-transform: rotateX(0deg);
  direction: ltr;
}

#bottom .leftright .right .current-theme:hover .fas.fa-fw.fa-palette {
  -webkit-transform: rotateY(180deg);
  transition: 0.5s;
}
#menu {
  gap: 0.5rem;
}

#menu .icon-button {
  border-radius: 10rem i !important;
  color: var(--bg-color);
  transition: 0.5s;
}

#menu .icon-button:nth-child(1) {
  background: #ab92e1;
}

#menu .icon-button:nth-child(2) {
  background: #f3ea5d;
}

#menu .icon-button:nth-child(3) {
  background: #7ae1bf;
}

#menu .icon-button:nth-child(4) {
  background: #ff5869;
}

#menu .icon-button:nth-child(5) {
  background: #fc76d9;
}

#menu .icon-button:nth-child(6) {
  background: #fc76d9;
}

==> ./monkeytype/static/themes/bouquet.css <==
:root {
  --bg-color: #173f35;
  --main-color: #eaa09c;
  --caret-color: #eaa09c;
  --sub-color: #408e7b;
  --text-color: #e9e0d2;
  --error-color: #d44729;
  --error-extra-color: #8f2f19;
  --colorful-error-color: #d44729;
  --colorful-error-extra-color: #8f2f19;
}

==> ./monkeytype/static/themes/ryujinscales.css <==
:root {
    --bg-color: #081426;
    --main-color: #f17754;
    --caret-color: #ef6d49;
    --sub-color: #ffbc90;
    --text-color: #ffe4bc;
    --error-color: #ca4754;
    --error-extra-color: #7e2a33;
    --colorful-error-color: #ca4754;
    --colorful-error-extra-color: #7e2a33;
  }

/* your theme has been added to the _list file and the textColor property is the theme's main color */
==> ./monkeytype/static/themes/graen.css <==
:root {
  --bg-color: #303c36;
  --main-color: #a59682;
  --caret-color: #601420;
  --sub-color: #181d1a;
  --text-color: #a59682;
  --error-color: #601420;
  --error-extra-color: #5f0715;
  --colorful-error-color: #601420;
  --colorful-error-extra-color: #5f0715;
}

#menu .icon-button:nth-child(1),
#menu .icon-button:nth-child(2),
#menu .icon-button:nth-child(3),
#menu .icon-button:nth-child(4),
#menu .icon-button:nth-child(5),
#menu .icon-button:nth-child(6) {
  color: #601420;
}

==> ./monkeytype/static/themes/mountain.css <==
:root {
  --bg-color: #0f0f0f;
  --main-color: #e7e7e7;
  --caret-color: #f5f5f5;
  --sub-color: #4c4c4c;
  --text-color: #e7e7e7;
  --error-color: #ac8c8c;
  --error-extra-color: #c49ea0;
  --colorful-error-color: #aca98a;
  --colorful-error-extra-color: #c4c19e;
}

==> ./monkeytype/static/themes/voc.css <==
:root {
  --bg-color: #190618;
  --main-color: #e0caac;
  --caret-color: #e0caac;
  --sub-color: #4c1e48;
  --text-color: #eeeae4;
  --error-color: #af3735;
  --error-extra-color: #7e2a29;
  --colorful-error-color: #af3735;
  --colorful-error-extra-color: #7e2a29;
}

==> ./monkeytype/static/themes/norse.css <==
:root {
  --bg-color: #242425;
  --main-color: #2b5f6d;
  --caret-color: #2b5f6d;
  --sub-color: #505b5e;
  --text-color: #ccc2b1;
  --error-color: #7e2a2a;
  --error-extra-color: #771d1d;
  --colorful-error-color: #ca4754;
  --colorful-error-extra-color: #7e2a33;
}

==> ./monkeytype/static/themes/rose_pine_moon.css <==
:root {
  --bg-color: #2a273f; /*Background*/
  --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/
  --caret-color: #f6c177; /*Cursor Color*/
  --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/
  --text-color: #e0def4; /*Color of text after hovering over it*/
  --error-color: #eb6f92;
  --error-extra-color: #ebbcba;
  --colorful-error-color: #eb6f92;
  --colorful-error-extra-color: #ebbcba;
}

==> ./monkeytype/static/themes/80s_after_dark.css <==
:root {
  --bg-color: #1b1d36;
  --main-color: #fca6d1;
  --caret-color: #99d6ea;
  --sub-color: #99d6ea;
  --text-color: #e1e7ec;
  --error-color: #fffb85;
  --error-extra-color: #fffb85;
  --colorful-error-color: #fffb85;
  --colorful-error-extra-color: #fffb85;
}

==> ./monkeytype/static/themes/peaches.css <==
:root {
  --bg-color: #e0d7c1;
  --main-color: #dd7a5f;
  --caret-color: #dd7a5f;
  --sub-color: #e7b28e;
  --text-color: #5f4c41;
  --error-color: #ff6961;
  --error-extra-color: #c23b22;
  --colorful-error-color: #ff6961;
  --colorful-error-extra-color: #c23b22;
}

==> ./monkeytype/static/sw.js <==
const staticCacheName = "sw-cache"; // this is given a unique name on build

self.addEventListener("activate", (event) => {
  caches.keys().then((names) => {
    for (let name of names) {
      if (name !== staticCacheName) event.waitUntil(caches.delete(name));
    }
  });
  event.waitUntil(self.clients.claim());
});

self.addEventListener("install", (event) => {
  event.waitUntil(self.skipWaiting());
  event.waitUntil(
    caches.open(staticCacheName).then((cache) => {
      // Cache the base file(s)
      return cache.add("/");
    })
  );
});

self.addEventListener("fetch", async (event) => {
  const host = new URL(event.request.url).host;
  if (
    [
      "localhost:5005",
      "api.monkeytype.com",
      "api.github.com",
      "www.google-analytics.com",
    ].includes(host) ||
    host.endsWith("wikipedia.org")
  ) {
    // if hostname is a non-static api, fetch request
    event.respondWith(fetch(event.request));
  } else {
    // Otherwise, assume host is serving a static file, check cache and add response to cache if not found
    event.respondWith(
      caches.open(staticCacheName).then((cache) => {
        return cache.match(event.request).then(async (response) => {
          // Check if request in cache
          if (response) {
            // if response was found in the cache, send from cache
            return response;
          } else {
            // if response was not found in cache fetch from server, cache it and send it
            response = await fetch(event.request);
            cache.put(event.request.url, response.clone());
            return response;
          }
        });
      })
    );
  }
});

==> ./monkeytype/.nvmrc <==
14.18.1
==> ./monkeytype/.prettierrc <==
{
  "tabWidth": 2,
  "useTabs": false,
  "htmlWhitespaceSensitivity": "ignore"
}

==> ./monkeytype/gulpfile.js <==
const { task, src, dest, series, watch } = require("gulp");
const axios = require("axios");
const browserify = require("browserify");
const babelify = require("babelify");
const concat = require("gulp-concat");
const del = require("del");
const source = require("vinyl-source-stream");
const buffer = require("vinyl-buffer");
const vinylPaths = require("vinyl-paths");
const eslint = require("gulp-eslint");
var sass = require("gulp-sass")(require("dart-sass"));
const replace = require("gulp-replace");
const uglify = require("gulp-uglify");
// sass.compiler = require("dart-sass");

let eslintConfig = {
  parser: "babel-eslint",
  globals: [
    "jQuery",
    "$",
    "firebase",
    "moment",
    "html2canvas",
    "ClipboardItem",
    "grecaptcha",
  ],
  envs: ["es6", "browser", "node"],
  plugins: ["json"],
  extends: ["plugin:json/recommended"],
  rules: {
    "json/*": ["error"],
    "constructor-super": "error",
    "for-direction": "error",
    "getter-return": "error",
    "no-async-promise-executor": "error",
    "no-case-declarations": "error",
    "no-class-assign": "error",
    "no-compare-neg-zero": "error",
    "no-cond-assign": "error",
    "no-const-assign": "error",
    "no-constant-condition": "error",
    "no-control-regex": "error",
    "no-debugger": "error",
    "no-delete-var": "error",
    "no-dupe-args": "error",
    "no-dupe-class-members": "error",
    "no-dupe-else-if": "warn",
    "no-dupe-keys": "error",
    "no-duplicate-case": "error",
    "no-empty": ["warn", { allowEmptyCatch: true }],
    "no-empty-character-class": "error",
    "no-empty-pattern": "error",
    "no-ex-assign": "error",
    "no-extra-boolean-cast": "error",
    "no-extra-semi": "error",
    "no-fallthrough": "error",
    "no-func-assign": "error",
    "no-global-assign": "error",
    "no-import-assign": "error",
    "no-inner-declarations": "error",
    "no-invalid-regexp": "error",
    "no-irregular-whitespace": "warn",
    "no-misleading-character-class": "error",
    "no-mixed-spaces-and-tabs": "error",
    "no-new-symbol": "error",
    "no-obj-calls": "error",
    "no-octal": "error",
    "no-prototype-builtins": "error",
    "no-redeclare": "error",
    "no-regex-spaces": "error",
    "no-self-assign": "error",
    "no-setter-return": "error",
    "no-shadow-restricted-names": "error",
    "no-sparse-arrays": "error",
    "no-this-before-super": "error",
    "no-undef": "error",
    "no-unexpected-multiline": "warn",
    "no-unreachable": "error",
    "no-unsafe-finally": "error",
    "no-unsafe-negation": "error",
    "no-unused-labels": "error",
    "no-unused-vars": ["warn", { argsIgnorePattern: "e|event" }],
    "no-use-before-define": "warn",
    "no-useless-catch": "error",
    "no-useless-escape": "error",
    "no-with": "error",
    "require-yield": "error",
    "use-isnan": "error",
    "valid-typeof": "error",
  },
};

//refactored files, which should be es6 modules
//once all files are moved here, then can we use a bundler to its full potential
const refactoredSrc = [
  "./src/js/axios-instance.js",
  "./src/js/db.js",
  "./src/js/misc.js",
  "./src/js/layouts.js",
  "./src/js/sound.js",
  "./src/js/theme-colors.js",
  "./src/js/chart-controller.js",
  "./src/js/theme-controller.js",
  "./src/js/config.js",
  "./src/js/tag-controller.js",
  "./src/js/preset-controller.js",
  "./src/js/ui.js",
  "./src/js/commandline.js",
  "./src/js/commandline-lists.js",
  "./src/js/commandline.js",
  "./src/js/challenge-controller.js",
  "./src/js/mini-result-chart.js",
  "./src/js/account-controller.js",
  "./src/js/simple-popups.js",
  "./src/js/settings.js",
  "./src/js/input-controller.js",
  "./src/js/route-controller.js",
  "./src/js/ready.js",
  "./src/js/monkey-power.js",

  "./src/js/account/all-time-stats.js",
  "./src/js/account/pb-tables.js",
  "./src/js/account/result-filters.js",
  "./src/js/account/verification-controller.js",
  "./src/js/account.js",

  "./src/js/elements/monkey.js",
  "./src/js/elements/notifications.js",
  "./src/js/elements/leaderboards.js",
  "./src/js/elements/account-button.js",
  "./src/js/elements/loader.js",
  "./src/js/elements/sign-out-button.js",
  "./src/js/elements/about-page.js",
  "./src/js/elements/psa.js",
  "./src/js/elements/new-version-notification.js",
  "./src/js/elements/mobile-test-config.js",
  "./src/js/elements/loading-page.js",
  "./src/js/elements/scroll-to-top.js",

  "./src/js/popups/custom-text-popup.js",
  "./src/js/popups/pb-tables-popup.js",
  "./src/js/popups/quote-search-popup.js",
  "./src/js/popups/quote-submit-popup.js",
  "./src/js/popups/quote-approve-popup.js",
  "./src/js/popups/rate-quote-popup.js",
  "./src/js/popups/version-popup.js",
  "./src/js/popups/support-popup.js",
  "./src/js/popups/contact-popup.js",
  "./src/js/popups/custom-word-amount-popup.js",
  "./src/js/popups/custom-test-duration-popup.js",
  "./src/js/popups/word-filter-popup.js",
  "./src/js/popups/result-tags-popup.js",
  "./src/js/popups/edit-tags-popup.js",
  "./src/js/popups/edit-preset-popup.js",
  "./src/js/popups/custom-theme-popup.js",
  "./src/js/popups/import-export-settings-popup.js",
  "./src/js/popups/custom-background-filter.js",

  "./src/js/settings/language-picker.js",
  "./src/js/settings/theme-picker.js",
  "./src/js/settings/settings-group.js",

  "./src/js/test/custom-text.js",
  "./src/js/test/british-english.js",
  "./src/js/test/lazy-mode.js",
  "./src/js/test/shift-tracker.js",
  "./src/js/test/out-of-focus.js",
  "./src/js/test/caret.js",
  "./src/js/test/manual-restart-tracker.js",
  "./src/js/test/test-stats.js",
  "./src/js/test/focus.js",
  "./src/js/test/practise-words.js",
  "./src/js/test/test-ui.js",
  "./src/js/test/keymap.js",
  "./src/js/test/result.js",
  "./src/js/test/live-wpm.js",
  "./src/js/test/caps-warning.js",
  "./src/js/test/live-acc.js",
  "./src/js/test/live-burst.js",
  "./src/js/test/timer-progress.js",
  "./src/js/test/test-logic.js",
  "./src/js/test/funbox.js",
  "./src/js/test/pace-caret.js",
  "./src/js/test/pb-crown.js",
  "./src/js/test/test-timer.js",
  "./src/js/test/test-config.js",
  "./src/js/test/layout-emulator.js",
  "./src/js/test/poetry.js",
  "./src/js/test/wikipedia.js",
  "./src/js/test/today-tracker.js",
  "./src/js/test/weak-spot.js",
  "./src/js/test/wordset.js",
  "./src/js/test/tts.js",
  "./src/js/replay.js",
];

//legacy files
//the order of files is important
const globalSrc = ["./src/js/global-dependencies.js", "./src/js/exports.js"];

//concatenates and lints legacy js files and writes the output to dist/gen/index.js
task("cat", function () {
  return src(globalSrc)
    .pipe(concat("index.js"))
    .pipe(eslint(eslintConfig))
    .pipe(eslint.format())
    .pipe(eslint.failAfterError())
    .pipe(dest("./dist/gen"));
});

task("sass", function () {
  return src("./src/sass/*.scss")
    .pipe(concat("style.scss"))
    .pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError))
    .pipe(dest("dist/css"));
});

task("static", function () {
  return src("./static/**/*", { dot: true }).pipe(dest("./dist/"));
});

//copies refactored js files to dist/gen so that they can be required by dist/gen/index.js
task("copy-modules", function () {
  return src(refactoredSrc, { allowEmpty: true }).pipe(dest("./dist/gen"));
});

//bundles the refactored js files together with index.js (the concatenated legacy js files)
//it's odd that the entry point is generated, so we should seek a better way of doing this
task("browserify", function () {
  const b = browserify({
    //index.js is generated by task "cat"
    entries: "./dist/gen/index.js",
    //a source map isn't very useful right now because
    //the source files are concatenated together
    debug: false,
  });
  return b
    .transform(
      babelify.configure({
        presets: ["@babel/preset-env"],
        plugins: ["@babel/transform-runtime"],
      })
    )
    .bundle()
    .pipe(source("monkeytype.js"))
    .pipe(buffer())
    .pipe(
      uglify({
        mangle: false,
      })
    )
    .pipe(dest("./dist/js"));
});

//lints only the refactored files
task("lint", function () {
  let filelist = refactoredSrc;
  filelist.push("./static/**/*.json");
  return src(filelist)
    .pipe(eslint(eslintConfig))
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
});

task("clean", function () {
  return src("./dist/", { allowEmpty: true }).pipe(vinylPaths(del));
});

task("updateSwCacheName", function () {
  let date = new Date();
  let dateString =
    date.getFullYear() +
    "-" +
    (date.getMonth() + 1) +
    "-" +
    date.getDate() +
    "-" +
    date.getHours() +
    "-" +
    date.getMinutes() +
    "-" +
    date.getSeconds();
  return src(["static/sw.js"])
    .pipe(
      replace(
        /const staticCacheName = .*;/g,
        `const staticCacheName = "sw-cache-${dateString}";`
      )
    )
    .pipe(dest("./dist/"));
});

task(
  "compile",
  series(
    "lint",
    "cat",
    "copy-modules",
    "browserify",
    "static",
    "sass",
    "updateSwCacheName"
  )
);

task("watch", function () {
  watch(["./static/**/*", "./src/**/*"], series("compile"));
});

task("build", series("clean", "compile"));


==> monkeytype/src/sass/about.scss <==
.pageAbout {
  display: grid;
  gap: 2rem;

  .created {
    text-align: center;
    color: var(--sub-color);
    a {
      text-decoration: none;
    }
  }

  .section {
    display: grid;
    gap: 0.25rem;

    .title {
      font-size: 2rem;
      line-height: 2rem;
      color: var(--sub-color);
      margin: 1rem 0;
    }

    .contactButtons,
    .supportButtons {
      margin-top: 1rem;
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr;
      gap: 1rem;
      .button {
        text-decoration: none;
        font-size: 1.5rem;
        padding: 2rem 0;
        .fas,
        .fab {
          margin-right: 1rem;
        }
      }
    }

    .supporters,
    .contributors {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr;
      gap: 0.25rem;
      color: var(--text-color);
    }

    h1 {
      font-size: 1rem;
      line-height: 1rem;
      color: var(--sub-color);
      margin: 0;
      font-weight: 300;
    }

    p {
      margin: 0;
      padding: 0;
      color: var(--text-color);
    }
  }
}

==> monkeytype/src/sass/banners.scss <==
#bannerCenter {
  position: fixed;
  width: 100%;
  z-index: 9999;
  .banner {
    background: var(--sub-color);
    color: var(--bg-color);
    display: flex;
    justify-content: center;
    .container {
      max-width: 1000px;
      display: grid;
      grid-template-columns: auto 1fr auto;
      gap: 1rem;
      align-items: center;
      width: 100%;
      justify-items: center;
      .image {
        // background-image: url(images/merchdropwebsite2.png);
        height: 2.3rem;
        background-size: cover;
        aspect-ratio: 6/1;
        background-position: center;
        background-repeat: no-repeat;
        margin-left: 2rem;
      }
      .icon {
        margin-left: 1rem;
        margin-top: 0.5rem;
        margin-bottom: 0.5rem;
      }
      .text {
        margin-top: 0.5rem;
        margin-bottom: 0.5rem;
      }
      .closeButton {
        margin-right: 1rem;
        margin-top: 0.5rem;
        margin-bottom: 0.5rem;
        transition: 0.125s;
        &:hover {
          cursor: pointer;
          color: var(--text-color);
        }
      }
    }
    &.good {
      background: var(--main-color);
    }
    &.bad {
      background: var(--error-color);
    }
  }
}

==> monkeytype/src/sass/popups.scss <==
.popupWrapper {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.75);
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  display: grid;
  justify-content: center;
  align-items: center;
  padding: 2rem 0;
}

#customTextPopupWrapper {
  #customTextPopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 60vw;
    .wordfilter {
      width: 33%;
      justify-self: right;
    }

    textarea {
      background: rgba(0, 0, 0, 0.1);
      padding: 1rem;
      color: var(--main-color);
      border: none;
      outline: none;
      font-size: 1rem;
      font-family: var(--font);
      width: 100%;
      border-radius: var(--roundness);
      resize: vertical;
      height: 200px;
      color: var(--text-color);
      overflow-x: hidden;
      overflow-y: scroll;
    }

    .inputs {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
      align-items: center;
      justify-items: left;
    }

    .randomInputFields {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      text-align: center;
      align-items: center;
      width: 100%;
      gap: 1rem;
    }
  }
}

#wordFilterPopupWrapper {
  #wordFilterPopup {
    color: var(--sub-color);
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 400px;

    input {
      width: 100%;
    }

    .group {
      display: grid;
      gap: 0.5rem;
    }

    .lengthgrid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      grid-template-rows: auto 1fr;
      column-gap: 1rem;
    }

    .tip {
      color: var(--sub-color);
      font-size: 0.8rem;
    }

    .loadingIndicator {
      justify-self: center;
    }
  }
}

#quoteRatePopupWrapper {
  #quoteRatePopup {
    color: var(--sub-color);
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 2rem;
    width: 800px;

    display: grid;
    grid-template-areas: "ratingStats ratingStats submitButton" "spacer spacer spacer" "quote quote quote";
    grid-template-columns: auto 1fr;

    color: var(--text-color);

    .spacer {
      grid-area: spacer;
      grid-column: 1/4;
      width: 100%;
      height: 0.1rem;
      border-radius: var(--roundness);
      background: var(--sub-color);
      opacity: 0.25;
    }

    .submitButton {
      font-size: 2rem;
      grid-area: submitButton;
      color: var(--sub-color);
      &:hover {
        color: var(--text-color);
      }
    }

    .top {
      color: var(--sub-color);
      font-size: 0.8rem;
    }

    .ratingStats {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr;
      gap: 1rem;
      grid-area: ratingStats;
      .top {
        font-size: 1rem;
      }
      .val {
        font-size: 2.25rem;
      }
    }

    .quote {
      display: grid;
      grid-area: quote;
      gap: 1rem;
      grid-template-areas:
        "text text text"
        "id length source";
      grid-template-columns: 1fr 1fr 3fr;
      .text {
        grid-area: text;
      }
      .id {
        grid-area: id;
      }
      .length {
        grid-area: length;
      }
      .source {
        grid-area: source;
      }
    }

    .stars {
      display: grid;
      color: var(--sub-color);
      font-size: 2rem;
      grid-template-columns: auto auto auto auto auto;
      justify-content: flex-start;
      align-items: center;
      cursor: pointer;
    }
    .star {
      transition: 0.125s;
    }
    i {
      pointer-events: none;
    }
    .star.active {
      color: var(--text-color);
    }
  }
}

#simplePopupWrapper {
  #simplePopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 400px;

    .title {
      font-size: 1.5rem;
      color: var(--sub-color);
    }

    .inputs {
      display: grid;
      gap: 1rem;
    }

    .text {
      font-size: 1rem;
      color: var(--text-color);
    }
  }
}

#mobileTestConfigPopupWrapper {
  #mobileTestConfigPopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 1rem;
    display: grid;
    gap: 1rem;
    width: calc(100vw - 2rem);
    // margin-left: 1rem;
    max-width: 400px;

    .title {
      font-size: 1.5rem;
      color: var(--sub-color);
    }

    .inputs {
      display: grid;
      gap: 1rem;
    }

    .text {
      font-size: 1rem;
      color: var(--text-color);
    }

    .group {
      display: grid;
      gap: 0.5rem;
    }
  }
}

#customWordAmountPopupWrapper,
#customTestDurationPopupWrapper,
#practiseWordsPopupWrapper,
#pbTablesPopupWrapper {
  #customWordAmountPopup,
  #customTestDurationPopup,
  #practiseWordsPopup,
  #pbTablesPopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 400px;

    .title {
      font-size: 1.5rem;
      color: var(--sub-color);
    }

    .tip {
      font-size: 0.75rem;
      color: var(--sub-color);
    }

    .text {
      font-size: 1rem;
      color: var(--text-color);
    }
  }

  #customTestDurationPopup {
    .preview {
      font-size: 0.75rem;
      color: var(--sub-color);
    }
  }
}

#pbTablesPopupWrapper #pbTablesPopup {
  .title {
    color: var(--text-color);
  }
  min-width: 50rem;
  max-height: calc(100vh - 10rem);
  overflow-y: scroll;
  table {
    border-spacing: 0;
    border-collapse: collapse;
    color: var(--text-color);

    td {
      padding: 0.5rem 0.5rem;
    }

    thead {
      color: var(--sub-color);
      font-size: 0.75rem;
    }

    tbody tr:nth-child(odd) td {
      background: rgba(0, 0, 0, 0.1);
    }

    td.infoIcons span {
      margin: 0 0.1rem;
    }
    .miniResultChartButton {
      opacity: 0.25;
      transition: 0.25s;
      cursor: pointer;
      &:hover {
        opacity: 1;
      }
    }
    .sub {
      opacity: 0.5;
    }
    td {
      text-align: right;
    }
    td:nth-child(6),
    td:nth-child(7) {
      text-align: center;
    }
    tbody td:nth-child(1) {
      font-size: 1.5rem;
    }
  }
}

#customThemeShareWrapper {
  #customThemeShare {
    width: 50vw;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    overflow-y: scroll;
  }
}

#quoteSearchPopupWrapper {
  #quoteSearchPopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 80vw;
    max-width: 1000px;
    height: 80vh;
    grid-template-rows: auto auto auto 1fr;

    #quoteSearchTop {
      display: flex;
      justify-content: space-between;

      .title {
        font-size: 1.5rem;
        color: var(--sub-color);
      }

      .buttons {
        width: 33%;
        display: grid;
        gap: 0.5rem;
        .button {
          width: 100%;
        }
      }
    }

    #extraResults {
      text-align: center;
      color: var(--sub-color);
    }
    #quoteSearchResults {
      display: grid;
      gap: 0.5rem;
      height: auto;
      overflow-y: scroll;

      .searchResult {
        display: grid;
        grid-template-columns: 1fr 1fr 3fr 0fr;
        grid-template-areas:
          "text text text text"
          "id len source report";
        grid-auto-rows: auto;
        width: 100%;
        gap: 0.5rem;
        transition: 0.25s;
        padding: 1rem;
        box-sizing: border-box;
        user-select: none;
        cursor: pointer;
        height: min-content;

        .text {
          grid-area: text;
          overflow: visible;
          color: var(--text-color);
        }
        .id {
          grid-area: id;
          font-size: 0.8rem;
          color: var(--sub-color);
        }
        .length {
          grid-area: len;
          font-size: 0.8rem;
          color: var(--sub-color);
        }
        .source {
          grid-area: source;
          font-size: 0.8rem;
          color: var(--sub-color);
        }
        .resultChevron {
          grid-area: chevron;
          display: flex;
          align-items: center;
          justify-items: center;
          color: var(--sub-color);
          font-size: 2rem;
        }
        .report {
          grid-area: report;
          color: var(--sub-color);
          transition: 0.25s;
          &:hover {
            color: var(--text-color);
          }
        }
        .sub {
          opacity: 0.5;
        }
      }
      .searchResult:hover {
        background: rgba(0, 0, 0, 0.1);
        border-radius: 5px;
      }
    }
  }
}
#settingsImportWrapper {
  #settingsImport {
    width: 50vw;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    overflow-y: scroll;
  }
}

#quoteSubmitPopup {
  background: var(--bg-color);
  border-radius: var(--roundness);
  padding: 2rem;
  display: grid;
  gap: 1rem;
  width: 1000px;
  grid-template-rows: auto auto auto auto auto auto auto auto auto;
  height: 100%;
  max-height: 40rem;
  overflow-y: scroll;

  label {
    color: var(--sub-color);
    margin-bottom: -1rem;
  }

  .title {
    font-size: 1.5rem;
    color: var(--sub-color);
  }
  textarea {
    resize: vertical;
    width: 100%;
    padding: 10px;
    line-height: 1.2rem;
    min-height: 5rem;
  }
  .characterCount {
    position: absolute;
    top: -1.25rem;
    right: 0.25rem;
    color: var(--sub-color);
    user-select: none;
    &.red {
      color: var(--error-color);
    }
  }
}

#quoteApprovePopup {
  background: var(--bg-color);
  border-radius: var(--roundness);
  padding: 2rem;
  display: grid;
  gap: 1rem;
  width: 1000px;
  height: 80vh;
  grid-template-rows: auto 1fr;

  .top {
    display: flex;
    justify-content: space-between;
    .title {
      font-size: 1.5rem;
      color: var(--sub-color);
    }
    .button {
      width: 33%;
    }
  }

  .quotes {
    display: grid;
    gap: 1rem;
    height: auto;
    overflow-y: scroll;
    align-content: baseline;

    .quote {
      display: grid;
      grid-template-columns: 1fr auto;
      grid-auto-rows: auto 2rem;
      width: 100%;
      gap: 1rem;
      transition: 0.25s;
      box-sizing: border-box;
      user-select: none;
      height: min-content;
      margin-bottom: 1rem;

      .text {
        grid-column: 1/2;
        grid-row: 1/2;
        overflow: visible;
        color: var(--text-color);
        resize: vertical;
        min-height: 4rem;
      }
      .source {
        grid-column: 1/2;
        grid-row: 2/3;
        color: var(--text-color);
      }
      .buttons {
        display: flex;
        flex-direction: column;
        justify-content: center;
        margin-right: 1rem;
        grid-column: 2/3;
        grid-row: 1/4;
        color: var(--sub-color);
      }

      .bottom {
        display: flex;
        justify-content: space-around;
        color: var(--sub-color);
        .length.red {
          color: var(--error-color);
        }
      }

      .sub {
        opacity: 0.5;
      }
    }
    .searchResult:hover {
      background: rgba(0, 0, 0, 0.1);
      border-radius: 5px;
    }
  }
}

#quoteReportPopupWrapper {
  #quoteReportPopup {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    width: 1000px;
    grid-template-rows: auto auto auto auto auto auto auto auto auto;
    height: auto;
    max-height: 40rem;
    overflow-y: scroll;

    label {
      color: var(--sub-color);
      margin-bottom: -1rem;
    }

    .text {
      color: var(--sub-color);
    }

    .quote {
      font-size: 1.5rem;
    }

    .title {
      font-size: 1.5rem;
      color: var(--sub-color);
    }

    textarea {
      resize: vertical;
      width: 100%;
      padding: 10px;
      line-height: 1.2rem;
      min-height: 5rem;
    }

    .characterCount {
      position: absolute;
      top: -1.25rem;
      right: 0.25rem;
      color: var(--sub-color);
      user-select: none;
      &.red {
        color: var(--error-color);
      }
    }
  }
}

#resultEditTagsPanelWrapper {
  #resultEditTagsPanel {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    overflow-y: scroll;
    width: 500px;

    .buttons {
      display: grid;
      gap: 0.1rem;
      grid-template-columns: 1fr 1fr 1fr;
    }
  }
}

#versionHistoryWrapper {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.75);
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  display: grid;
  justify-content: center;
  align-items: start;
  padding: 5rem 0;

  #versionHistory {
    width: 75vw;
    height: 100%;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    @extend .ffscroll;
    overflow-y: scroll;

    .tip {
      text-align: center;
      color: var(--sub-color);
    }

    .releases {
      display: grid;
      gap: 4rem;

      .release {
        display: grid;
        grid-template-areas:
          "title date"
          "body body";

        .title {
          grid-area: title;
          font-size: 2rem;
          color: var(--sub-color);
        }

        .date {
          grid-area: date;
          text-align: right;
          color: var(--sub-color);
          align-self: center;
        }

        .body {
          grid-area: body;
          color: var(--text-color);
        }

        &:last-child {
          margin-bottom: 2rem;
        }
      }
    }
  }
}

#supportMeWrapper {
  #supportMe {
    width: 900px;
    // height: 400px;
    overflow-y: scroll;
    max-height: 100%;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    grid-template-rows: auto auto auto;
    gap: 2rem;
    @extend .ffscroll;

    .title {
      font-size: 2rem;
      line-height: 2rem;
      color: var(--main-color);
    }

    .text {
      color: var(--text-color);
    }

    .subtext {
      color: var(--sub-color);
      font-size: 0.75rem;
    }

    .buttons {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr;
      gap: 1rem;

      .button {
        display: block;
        width: 100%;
        height: 100%;
        padding: 2rem 0;
        display: grid;
        gap: 1rem;
        text-decoration: none;
        .text {
          transition: 0.25s;
        }
        &:hover .text {
          color: var(--bg-color);
        }
        .icon {
          font-size: 5rem;
          line-height: 5rem;
        }
      }
    }
  }
}

#contactPopupWrapper {
  #contactPopup {
    // height: 400px;
    overflow-y: scroll;
    max-height: 100%;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    grid-template-rows: auto auto auto;
    gap: 2rem;
    @extend .ffscroll;
    margin: 0 2rem;
    max-width: 900px;

    .title {
      font-size: 2rem;
      line-height: 2rem;
      color: var(--main-color);
    }

    .text {
      color: var(--text-color);
      span {
        color: var(--error-color);
      }
    }

    .subtext {
      color: var(--sub-color);
      font-size: 0.75rem;
      grid-area: subtext;
    }

    .buttons {
      display: grid;
      gap: 1rem;
      grid-template-columns: 1fr 1fr;

      .button {
        display: block;
        width: 100%;
        height: 100%;
        padding: 1rem 0;
        display: grid;
        // gap: 0.5rem;
        text-decoration: none;
        grid-template-areas: "icon textgroup";
        grid-template-columns: auto 1fr;
        text-align: left;
        align-items: center;
        .textGroup {
          grid-area: textgroup;
        }
        .text {
          font-size: 1.5rem;
          line-height: 2rem;
          transition: 0.25s;
        }
        &:hover .text {
          color: var(--bg-color);
        }
        .icon {
          grid-area: icon;
          font-size: 2rem;
          line-height: 2rem;
          padding: 0 1rem;
        }
      }
    }
  }
}

#presetWrapper {
  #presetEdit {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    overflow-y: scroll;
  }
}

#tagsWrapper {
  #tagsEdit {
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 1rem;
    overflow-y: scroll;
  }
}

==> monkeytype/src/sass/account.scss <==
.signOut {
  font-size: 1rem;
  line-height: 1rem;
  justify-self: end;
  // background: var(--sub-color);
  color: var(--sub-color);
  width: fit-content;
  width: -moz-fit-content;
  padding: 0.5rem;
  border-radius: var(--roundness);
  cursor: pointer;
  transition: 0.25s;
  float: right;

  &:hover {
    color: var(--text-color);
  }

  .fas {
    margin-right: 0.5rem;
  }
}

.pageAccount {
  display: grid;
  gap: 1rem;

  .content {
    display: grid;
    gap: 2rem;
  }

  .sendVerificationEmail {
    cursor: pointer;
  }

  .timePbTable,
  .wordsPbTable {
    .sub {
      opacity: 0.5;
    }
    td {
      text-align: right;
    }
    tbody td:nth-child(1) {
      font-size: 1.5rem;
    }
  }

  .showAllTimePbs,
  .showAllWordsPbs {
    margin-top: 1rem;
  }

  .topFilters .buttons {
    display: flex;
    justify-content: space-evenly;
    gap: 1rem;
    .button {
      width: 100%;
    }
  }

  .miniResultChartWrapper {
    // pointer-events: none;
    z-index: 999;
    display: none;
    height: 15rem;
    background: var(--bg-color);
    width: 45rem;
    position: absolute;
    border-radius: var(--roundness);
    padding: 1rem;
    // box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25);
  }

  .miniResultChartBg {
    display: none;
    z-index: 998;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.25);
    position: fixed;
    left: 0;
    top: 0;
  }

  .doublegroup {
    display: grid;
    grid-auto-flow: column;
    gap: 1rem;
    .titleAndTable {
      .title {
        color: var(--sub-color);
      }
    }
  }

  .triplegroup {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: 1rem;

    .text {
      align-self: center;
      color: var(--sub-color);
    }
  }

  .group {
    &.noDataError {
      margin: 20rem 0;
      // height: 30rem;
      // line-height: 30rem;
      text-align: center;
    }

    &.createdDate {
      text-align: center;
      color: var(--sub-color);
    }

    &.personalBestTables {
      .tables {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 2rem;
      }
    }

    &.history {
      .active {
        animation: flashHighlight 4s linear 0s 1;
      }

      .loadMoreButton {
        background: rgba(0, 0, 0, 0.1);
        color: var(--text-color);
        text-align: center;
        padding: 0.5rem;
        border-radius: var(--roundness);
        cursor: pointer;
        -webkit-transition: 0.25s;
        transition: 0.25s;
        -webkit-user-select: none;
        display: -ms-grid;
        display: grid;
        -ms-flex-line-pack: center;
        align-content: center;
        margin-top: 1rem;

        &:hover,
        &:focus {
          color: var(--bg-color);
          background: var(--text-color);
        }
      }
    }

    .title {
      color: var(--sub-color);
    }

    .val {
      font-size: 3rem;
      line-height: 3.5rem;
    }

    .chartjs-render-monitor {
      width: 100% !important;
    }

    &.chart {
      position: relative;

      .above {
        display: flex;
        justify-content: center;
        margin-bottom: 1rem;
        color: var(--sub-color);
        flex-wrap: wrap;

        .group {
          display: flex;
          align-items: center;
        }

        .fas,
        .punc {
          margin-right: 0.25rem;
        }

        .spacer {
          width: 1rem;
        }
      }

      .below {
        text-align: center;
        color: var(--sub-color);
        margin-top: 1rem;
        display: grid;
        grid-template-columns: auto 300px;
        align-items: center;
        .text {
          height: min-content;
        }
        .buttons {
          display: grid;
          gap: 0.5rem;
        }
      }
      .chart {
        height: 400px;
      }
      .chartPreloader {
        position: absolute;
        width: 100%;
        background: rgba(0, 0, 0, 0.5);
        height: 100%;
        display: grid;
        align-items: center;
        justify-content: center;
        font-size: 5rem;
        text-shadow: 0 0 3rem black;
      }
    }
  }

  table {
    border-spacing: 0;
    border-collapse: collapse;
    color: var(--text-color);

    td {
      padding: 0.5rem 0.5rem;
    }

    thead {
      color: var(--sub-color);
      font-size: 0.75rem;
    }

    tbody tr:nth-child(odd) td {
      background: rgba(0, 0, 0, 0.1);
    }

    td.infoIcons span {
      margin: 0 0.1rem;
    }
    .miniResultChartButton {
      opacity: 0.25;
      transition: 0.25s;
      cursor: pointer;
      &:hover {
        opacity: 1;
      }
    }
  }

  #resultEditTags {
    transition: 0.25s;
    &:hover {
      cursor: pointer;
      color: var(--text-color);
      opacity: 1 !important;
    }
  }
}

.pageAccount {
  .group.filterButtons {
    gap: 1rem;
    display: grid;
    grid-template-columns: 1fr 1fr;

    .buttonsAndTitle {
      height: fit-content;
      height: -moz-fit-content;
      display: grid;
      gap: 0.25rem;
      color: var(--sub-color);
      line-height: 1rem;
      font-size: 1rem;

      &.testDate .buttons,
      &.languages .buttons,
      &.layouts .buttons,
      &.funbox .buttons,
      &.tags .buttons {
        grid-template-columns: repeat(4, 1fr);
        grid-auto-flow: unset;
      }
    }

    .buttons {
      display: grid;
      grid-auto-flow: column;
      gap: 1rem;

      .button {
        background: rgba(0, 0, 0, 0.1);
        color: var(--text-color);
        text-align: center;
        padding: 0.5rem;
        border-radius: var(--roundness);
        cursor: pointer;
        transition: 0.25s;
        -webkit-user-select: none;
        display: grid;
        align-content: center;

        &.active {
          background: var(--main-color);
          color: var(--bg-color);
        }

        &:hover {
          color: var(--bg-color);
          background: var(--main-color);
        }
      }
    }
  }
}

.header-sorted {
  font-weight: bold;
}

.sortable:hover {
  cursor: pointer;
  user-select: none;
  background-color: rgba(0, 0, 0, 0.1);
}

==> monkeytype/src/sass/monkey.scss <==
#monkey {
  width: 308px;
  height: 0;
  margin: 0 auto;
  animation: shake 0s infinite;
  div {
    height: 200px;
    width: 308px;
    position: fixed;
  }
  .up {
    background-image: url("../images/monkey/m3.png");
  }
  .left {
    background-image: url("../images/monkey/m1.png");
  }
  .right {
    background-image: url("../images/monkey/m2.png");
  }
  .both {
    background-image: url("../images/monkey/m4.png");
  }
  .fast {
    .up {
      background-image: url("../images/monkey/m3_fast.png");
    }
    .left {
      background-image: url("../images/monkey/m1_fast.png");
    }
    .right {
      background-image: url("../images/monkey/m2_fast.png");
    }
    .both {
      background-image: url("../images/monkey/m4_fast.png");
    }
  }
}

==> monkeytype/src/sass/core.scss <==
@import url("https://fonts.googleapis.com/css2?family=Fira+Code&family=IBM+Plex+Sans:wght@600&family=Inconsolata&family=Roboto+Mono&family=Source+Code+Pro&family=JetBrains+Mono&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Montserrat&family=Roboto&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lexend+Deca&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Oxygen&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Nunito&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Itim&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Comfortaa&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Coming+Soon&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lato&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lalezar&display=swap");
@import url("https://fonts.googleapis.com/css?family=Noto+Naskh+Arabic&display=swap");

:root {
  --roundness: 0.5rem;
  --font: "Roboto Mono";
  // scroll-behavior: smooth;
  scroll-padding-top: 2rem;
}

::placeholder {
  color: var(--sub-color);
  opacity: 1;
  /* Firefox */
}

#nocss {
  display: none !important;
  pointer-events: none;
}

.ffscroll {
  scrollbar-width: thin;
  scrollbar-color: var(--sub-color) transparent;
}

html {
  @extend .ffscroll;
  overflow-y: scroll;
}

a {
  display: inline-block;
  color: var(--sub-color);
  transition: 0.25s;
  &:hover {
    color: var(--text-color);
  }
}

body {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  font-family: var(--font);
  color: var(--text-color);
  overflow-x: hidden;
  background: var(--bg-color);
}

.customBackground {
  content: "";
  width: 100vw;
  height: 100vh;
  position: fixed;
  left: 0;
  top: 0;
  background-position: center center;
  background-repeat: no-repeat;
  z-index: -999;
  justify-content: center;
  align-items: center;
  display: flex;
}

#backgroundLoader {
  height: 3px;
  position: fixed;
  width: 100%;
  background: var(--main-color);
  animation: loader 2s cubic-bezier(0.38, 0.16, 0.57, 0.82) infinite;
  z-index: 9999;
}

label.checkbox {
  span {
    display: block;
    font-size: 0.76rem;
    color: var(--sub-color);
    margin-left: 1.5rem;
  }

  input {
    margin: 0 !important;
    cursor: pointer;
    width: 0;
    height: 0;
    display: none;

    & ~ .customTextCheckbox {
      width: 12px;
      height: 12px;
      background: rgba(0, 0, 0, 0.1);
      border-radius: 2px;
      box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.1);
      display: inline-block;
      margin: 0 0.5rem 0 0.25rem;
      transition: 0.25s;
    }

    &:checked ~ .customTextCheckbox {
      background: var(--main-color);
    }
  }
}

#centerContent {
  max-width: 1000px;
  // min-width: 500px;
  // margin: 0 auto;
  display: grid;
  grid-auto-flow: row;
  min-height: 100vh;
  padding-left: 2rem;
  padding-right: 2rem;
  padding-top: 2rem;
  padding-bottom: 2rem;
  gap: 2rem;
  align-items: center;
  z-index: 999;
  grid-template-rows: auto 1fr auto;
  width: 100%;
  &.wide125 {
    max-width: 1250px;
  }
  &.wide150 {
    max-width: 1500px;
  }
  &.wide200 {
    max-width: 2000px;
  }
  &.widemax {
    max-width: unset;
  }
}

key {
  color: var(--bg-color);
  background-color: var(--sub-color);
  /* font-weight: bold; */
  padding: 0.1rem 0.3rem;
  margin: 3px 0;
  border-radius: 0.1rem;
  display: inline-block;
  font-size: 0.7rem;
  line-height: 0.7rem;
}

.pageLoading {
  display: grid;
  justify-content: center;
}

.pageLoading,
.pageAccount {
  .preloader {
    text-align: center;
    justify-self: center;
    display: grid;
    .barWrapper {
      display: grid;
      gap: 1rem;
      grid-row: 1;
      grid-column: 1;
      .bar {
        width: 20rem;
        height: 0.5rem;
        background: rgba(0, 0, 0, 0.1);
        border-radius: var(--roundness);
        .fill {
          height: 100%;
          width: 0%;
          background: var(--main-color);
          border-radius: var(--roundness);
          // transition: 1s;
        }
      }
    }
    .icon {
      grid-row: 1;
      grid-column: 1;
      font-size: 2rem;
      color: var(--main-color);
      margin-bottom: 1rem;
    }
  }
}

.devIndicator {
  position: fixed;
  font-size: 3rem;
  color: var(--sub-color);
  opacity: 0.25;
  z-index: -1;

  &.tl {
    top: 2rem;
    left: 2rem;
  }

  &.tr {
    top: 2rem;
    right: 2rem;
  }

  &.bl {
    bottom: 2rem;
    left: 2rem;
  }

  &.br {
    bottom: 2rem;
    right: 2rem;
  }
}

* {
  box-sizing: border-box;
}

.hidden {
  display: none !important;
}

.invisible {
  opacity: 0 !important;
  pointer-events: none !important;
}

.button {
  color: var(--text-color);
  cursor: pointer;
  transition: 0.25s;
  padding: 0.4rem;
  border-radius: var(--roundness);
  background: rgba(0, 0, 0, 0.1);
  text-align: center;
  -webkit-user-select: none;
  // display: grid;
  align-content: center;
  height: min-content;
  height: -moz-min-content;
  line-height: 1rem;

  &:hover {
    color: var(--bg-color);
    background: var(--text-color);
    outline: none;
  }
  &:focus {
    color: var(--main-color);
    background: var(--sub-color);
    outline: none;
  }

  &.active {
    background: var(--main-color);
    color: var(--bg-color);
    &:hover {
      // color: var(--text-color);
      background: var(--text-color);
      outline: none;
    }
    &:focus {
      color: var(--bg-color);
      background: var(--main-color);
      outline: none;
    }
  }

  &.disabled {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
    &:hover {
      color: var(--text-color);
      background: rgba(0, 0, 0, 0.1);
      outline: none;
    }
  }

  &.disabled.active {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
    &:hover {
      color: var(--bg-color);
      background: var(--main-color);
      outline: none;
    }
  }
}

.text-button {
  transition: 0.25s;
  color: var(--sub-color);
  cursor: pointer;
  margin-right: 0.25rem;
  cursor: pointer;
  outline: none;

  &.active {
    color: var(--main-color);
  }

  &:hover,
  &:focus {
    color: var(--text-color);
  }
}

.icon-button {
  display: grid;
  grid-auto-flow: column;
  align-content: center;
  transition: 0.25s;
  padding: 0.5rem;
  border-radius: var(--roundness);
  cursor: pointer;

  &:hover {
    color: var(--text-color);
  }
  &:focus {
    // background: var(--sub-color);
    color: var(--sub-color);
    border: none;
    outline: none;
  }
  &.disabled {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
  }
}

.scrollToTopButton {
  bottom: 2rem;
  right: 2rem;
  position: fixed;
  font-size: 2rem;
  width: 4rem;
  height: 4rem;
  text-align: center;
  line-height: 4rem;
  background: var(--bg-color);
  border-radius: 99rem;
  z-index: 99;
  cursor: pointer;
  color: var(--sub-color);
  transition: 0.25s;
  &:hover {
    color: var(--text-color);
  }
}

==> monkeytype/src/sass/login.scss <==
.pageLogin {
  display: flex;
  grid-auto-flow: column;
  gap: 1rem;
  justify-content: space-around;
  align-items: center;

  .side {
    display: grid;
    gap: 0.5rem;
    justify-content: center;
    grid-template-columns: 1fr;

    input {
      width: 15rem;
    }

    &.login {
      grid-template-areas:
        "title forgotButton"
        "form form";

      .title {
        grid-area: title;
      }

      #forgotPasswordButton {
        grid-area: forgotButton;
        font-size: 0.75rem;
        line-height: 0.75rem;
        height: fit-content;
        height: -moz-fit-content;
        align-self: center;
        justify-self: right;
        padding: 0.25rem 0;
        color: var(--sub-color);
        cursor: pointer;
        transition: 0.25s;

        &:hover {
          color: var(--text-color);
        }
      }

      form {
        grid-area: form;
      }
    }
  }

  form {
    display: grid;
    gap: 0.5rem;
    width: 100%;
  }

  .preloader {
    position: fixed;
    left: 50%;
    top: 50%;
    font-size: 2rem;
    transform: translate(-50%, -50%);
    color: var(--main-color);
    transition: 0.25s;
  }
}

==> monkeytype/src/sass/z_media-queries.scss <==
@media only screen and (max-width: 1200px) {
  #leaderboardsWrapper {
    #leaderboards {
      .tables {
        grid-template-columns: unset;
      }
      .tables .rightTableWrapper,
      .tables .leftTableWrapper {
        height: calc(50vh - 12rem);
      }
    }
  }
}

@media only screen and (max-width: 1050px) {
  .pageSettings .section.fullWidth .buttons {
    grid-template-columns: 1fr 1fr 1fr;
  }

  #result .morestats {
    gap: 1rem;
    grid-template-rows: 1fr 1fr;
  }
  #supportMe {
    width: 90vw !important;
    .buttons {
      .button {
        .icon {
          font-size: 3rem !important;
          line-height: 3rem !important;
        }
      }
    }
  }
  #customTextPopup {
    width: 80vw !important;

    .wordfilter.button {
      width: 50% !important;
    }
  }
}

@media only screen and (max-width: 1000px) {
  #quoteRatePopup {
    width: 90vw !important;
  }
  #bottom {
    .leftright {
      .left {
        gap: 0.25rem 1rem;
        display: grid;
        grid-template-rows: 1fr 1fr;
        grid-auto-flow: row;
        grid-template-columns: auto auto auto auto;
      }
      .right {
        display: grid;
        grid-template-rows: 1fr 1fr;
        gap: 0.25rem 1rem;
      }
    }
  }
}

@media only screen and (max-width: 900px) {
  // #leaderboards {
  //   .mainTitle {
  //     font-size: 1.5rem !important;
  //     line-height: 1.5rem !important;
  //   }
  // }
  .merchBanner {
    img {
      display: none;
    }
    .text {
      padding: 0.25rem 0;
    }
  }
  .pageAccount {
    .group.personalBestTables {
      .tables {
        grid-template-columns: 1fr;
      }
    }
    .group.history {
      table {
        thead,
        tbody {
          td:nth-child(1),
          td:nth-child(8),
          td:nth-child(9) {
            display: none;
          }
        }
      }
    }
  }
}

@media only screen and (max-width: 800px) {
  .pageSettings .settingsGroup.quickNav .links {
    grid-auto-flow: unset;
    grid-template-columns: 1fr 1fr 1fr;
    justify-items: center;
  }
  #bannerCenter .banner .container {
    grid-template-columns: 1fr auto;
    .image {
      display: none;
    }
    .lefticon {
      display: none;
    }
    .text {
      margin-left: 2rem;
    }
  }
  #centerContent {
    #top {
      grid-template-areas:
        "logo config"
        "menu config";
      grid-template-columns: auto auto;
      .logo {
        margin-bottom: 0;
      }
    }

    #menu {
      gap: 0.5rem;
      font-size: 0.8rem;
      line-height: 0.8rem;
      margin-top: -0.5rem;

      .icon-button {
        padding: 0.25rem;
      }
    }
  }

  #contactPopupWrapper #contactPopup .buttons {
    grid-template-columns: 1fr;
  }

  .pageAbout .section {
    .contributors,
    .supporters {
      grid-template-columns: 1fr 1fr 1fr;
    }
    .contactButtons,
    .supportButtons {
      grid-template-columns: 1fr 1fr;
    }
  }

  .pageSettings .section.customBackgroundFilter {
    .groups {
      grid-template-columns: 1fr;
    }
    .saveContainer {
      grid-column: -1/-2;
    }
  }

  .pageSettings {
    .section.themes .tabContent.customTheme {
    }
  }

  #commandLine,
  #commandLineInput {
    width: 600px !important;
  }
}

@media only screen and (max-width: 700px) {
  #leaderboardsWrapper {
    #leaderboards {
      .leaderboardsTop {
        flex-direction: column;
        align-items: baseline;
      }
    }
  }
  .pageAccount {
    .triplegroup {
      grid-template-columns: 1fr 1fr;
      .emptygroup {
        display: none;
      }
    }
    .group.chart .below {
      grid-template-columns: 1fr;
      gap: 0.5rem;
    }
    .group.topFilters .buttonsAndTitle .buttons {
      display: grid;
      justify-content: unset;
    }
    .group.history {
      table {
        thead,
        tbody {
          td:nth-child(6) {
            display: none;
          }
        }
      }
    }
  }
}

@media only screen and (max-width: 650px) {
  #quoteRatePopup {
    .ratingStats {
      grid-template-columns: 1fr 1fr !important;
    }
    .quote {
      grid-template-areas:
        "text text text"
        "source source source"
        "id length length" !important;
    }
  }
  .pageSettings .section {
    grid-template-columns: 1fr;
    grid-template-areas:
      "title"
      "text"
      "buttons";

    & > .text {
      margin-bottom: 1rem;
    }
  }

  #result {
    .buttons {
      grid-template-rows: 1fr 1fr 1fr;
      #nextTestButton {
        grid-column: 1/5;
        width: 100%;
        text-align: center;
      }
    }
  }

  #supportMe {
    width: 80vw !important;
    .buttons {
      grid-template-columns: none !important;
      .button {
        grid-template-columns: auto auto;
        align-items: center;
        .icon {
          font-size: 2rem !important;
          line-height: 2rem !important;
        }
      }
    }
  }

  .pageSettings .section.fullWidth .buttons {
    grid-template-columns: 1fr 1fr;
  }
}

@media only screen and (max-width: 600px) {
  .pageAbout .section .supporters,
  .pageAbout .section .contributors {
    grid-template-columns: 1fr 1fr;
  }
  #top .logo .bottom {
    margin-top: 0;
  }
  .pageLogin {
    display: grid;
    gap: 5rem;
    grid-auto-flow: unset;
  }
  #middle {
    #result {
      grid-template-areas:
        "stats stats"
        "chart chart"
        "morestats morestats";
      .stats {
        grid-template-areas: "wpm acc";
        gap: 2rem;
      }
      .stats.morestats {
        grid-template-rows: 1fr 1fr 1fr;
        gap: 1rem;
      }
    }
  }
  #commandLine,
  #commandLineInput {
    width: 500px !important;
  }
  #customTextPopupWrapper {
    #customTextPopup {
      .wordfilter.button {
        width: 100% !important;
        justify-self: auto;
      }

      .inputs {
        display: flex !important;
        flex-direction: column;
        justify-content: flex-start;
      }
    }
  }
  #leaderboardsWrapper #leaderboards {
    padding: 1rem;
    gap: 1rem;
    .mainTitle {
      font-size: 2rem;
      line-height: 2rem;
    }
    .title {
      font-size: 1rem;
    }
    .leaderboardsTop {
      .buttonGroup {
        gap: 0.1rem !important;

        .button {
          padding: 0.4rem !important;
          font-size: 0.7rem !important;
        }
      }
    }
  }
  .pageAccount {
    .group.history {
      table {
        thead,
        tbody {
          td:nth-child(7),
          td:nth-child(5) {
            display: none;
          }
        }
      }
    }
  }
}

@media only screen and (max-width: 550px) {
  .keymap {
    .row {
      height: 1.25rem;
    }
    .keymap-key {
      width: 1.25rem;
      height: 1.25rem;
      border-radius: 0.3rem;
      font-size: 0.6rem;
    }
  }

  #contactPopupWrapper #contactPopup .buttons .button .text {
    font-size: 1rem;
  }
  #contactPopupWrapper #contactPopup .buttons .button .icon {
    font-size: 1.5rem;
    line-height: 1.5rem;
  }
  #contactPopupWrapper #contactPopup {
    padding: 1rem;
  }
  .pageAbout .section .supporters,
  .pageAbout .section .contributors {
    grid-template-columns: 1fr;
  }

  #simplePopupWrapper #simplePopup {
    width: 90vw;
  }

  .pageSettings {
    .settingsGroup.quickNav {
      display: none;
    }
    .section.fullWidth .buttons {
      grid-template-columns: 1fr;
    }
    .section .buttons {
      grid-auto-flow: row;
    }
    .section.customBackgroundFilter .groups .group {
      grid-template-columns: auto 1fr;
      .title {
        grid-column: 1/3;
      }
    }
  }

  .pageAbout .section {
    .contactButtons,
    .supportButtons {
      grid-template-columns: 1fr;
    }
  }

  .pageAccount {
    .triplegroup {
      grid-template-columns: 1fr;
    }
    .group.history {
      table {
        thead,
        tbody {
          td:nth-child(3) {
            display: none;
          }
        }
      }
    }
  }

  #top {
    align-items: self-end;
    .logo {
      .icon {
        width: 1.5rem !important;
      }
      .text {
        font-size: 1.5rem !important;
        margin-bottom: 0.3rem !important;
      }
      .bottom {
        font-size: 1.75rem;
        line-height: 1.75rem;
        margin-top: 0;
      }
      .top {
        display: none;
      }
    }
    #menu {
      .icon-button {
        padding: 0;
      }
    }
  }
  #bottom {
    .leftright {
      .left {
        gap: 0.25rem 1rem;
        display: grid;
        grid-template-rows: 1fr 1fr 1fr;
        grid-template-columns: auto auto auto;
        grid-auto-flow: row;
      }
      .right {
        display: grid;
        grid-template-rows: 1fr 1fr 1fr;
        gap: 0.25rem 1rem;
      }
    }
  }
  #centerContent {
    #top {
      grid-template-columns: 1fr auto;
      .desktopConfig {
        display: none;
      }
      .mobileConfig {
        display: block;
      }
    }
    padding: 1rem;
  }
  #middle {
    #result {
      .stats {
        grid-template-areas:
          "wpm"
          "acc";
        gap: 1rem;
      }
    }
  }
  #result {
    .buttons {
      grid-template-rows: 1fr 1fr 1fr 1fr;
      #nextTestButton {
        grid-column: 1/3;
        width: 100%;
        text-align: center;
      }
    }
  }
  #commandLine,
  #commandLineInput {
    width: 400px !important;
  }
}

@media only screen and (max-width: 400px) {
  #top .logo .bottom {
    font-size: 1.5rem;
    line-height: 1.5rem;
    margin-top: 0;
  }

  #top .config {
    grid-gap: 0.25rem;
    .group .buttons {
      font-size: 0.65rem;
      line-height: 0.65rem;
    }
  }

  #bottom {
    font-size: 0.65rem;
    .leftright {
      grid-template-columns: 1fr 1fr;
      .left {
        grid-template-rows: 1fr 1fr 1fr 1fr;
        grid-template-columns: 1fr 1fr;
        grid-auto-flow: row;
      }
      .right {
        // justify-self: left;
        // grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr 1fr 1fr;
        gap: 0.25rem 1rem;
      }
    }
  }

  #commandLine,
  #commandLineInput {
    width: 300px !important;
  }

  #leaderboardsWrapper #leaderboards .tables .titleAndTable .titleAndButtons {
    grid-template-columns: unset;
  }
}

@media only screen and (max-width: 350px) {
  .keymap {
    display: none !important;
  }
  .pageLogin .side input {
    width: 90vw;
  }
}

@media (hover: none) and (pointer: coarse) {
  #commandLineMobileButton {
    display: block !important;
  }
}

==> monkeytype/src/sass/inputs.scss <==
input,
textarea {
  outline: none;
  border: none;
  border-radius: var(--roundness);
  background: rgba(0, 0, 0, 0.1);
  color: var(--text-color);
  padding: 0.5rem;
  font-size: 1rem;
  font-family: var(--font);
}

input[type="range"] {
  -webkit-appearance: none;
  padding: 0;
  width: 100%;
  height: 1rem;
  border-radius: var(--roundness);
  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    padding: 0;
    border: none;
    width: 2rem;
    height: 1rem;
    border-radius: var(--roundness);
    background-color: var(--main-color);
  }

  &::-moz-range-thumb {
    -webkit-appearance: none;
    padding: 0;
    border: none;
    width: 2rem;
    height: 1rem;
    border-radius: var(--roundness);
    background-color: var(--main-color);
  }
}

input[type="color"] {
  height: 3px; //i dont know why its 3, but safari gods have spoken - 3 makes it work
  opacity: 0;
  padding: 0;
  margin: 0;
  position: absolute;
  pointer-events: none;
}

::-moz-color-swatch {
  border: none;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  margin: 0;
}

input[type="number"] {
  -moz-appearance: textfield;
}

.select2-dropdown {
  background-color: var(--bg-color);
  color: var(--text-color);
  outline: none;
}

.select2-selection {
  background: rgba(0, 0, 0, 0.1);
  height: fit-content;
  height: -moz-fit-content;
  padding: 5px;
  border-radius: var(--roundness);
  color: var(--text-color);
  font: var(--font);
  border: none;
  outline: none;
}

.select2-container--default
  .select2-selection--single
  .select2-selection__rendered {
  color: var(--text-color);
  outline: none;
}

.select2-container--default
  .select2-results__option--highlighted.select2-results__option--selectable {
  background-color: var(--text-color);
  color: var(--bg-color);
}

.select2-container--default .select2-results__option--selected {
  background-color: var(--bg-color);
  color: var(--sub-color);
}

.select2-container--open .select2-dropdown--below {
  border-color: rgba(0, 0, 0, 0.1);
  background: var(--bg-color);
  color: var(--sub-color);
  border-radius: var(--roundness);
}

.select2-container--default .select2-selection--single {
  color: var(--text-color);
  background: rgba(0, 0, 0, 0.1);
  outline: none;
  border: none;
  height: auto;
}

.select2-selection:focus {
  height: fit-content;
  height: -moz-fit-content;
  padding: 5px;
  border-radius: var(--roundness);
  color: var(--text-color);
  font: var(--font);
  border: none;
  outline: none;
}
.select2-selection:active {
  height: fit-content;
  height: -moz-fit-content;
  padding: 5px;
  border-radius: var(--roundness);
  color: var(--text-color);
  font: var(--font);
  border: none;
  outline: none;
}

.select2-container--default
  .select2-selection--single
  .select2-selection__arrow {
  height: 35px;
}

.select2-container--default
  .select2-selection--single
  .select2-selection__arrow
  b {
  border-color: var(--sub-color) transparent transparent transparent;
}

.select2-container--default.select2-container--open
  .select2-selection--single
  .select2-selection__arrow
  b {
  border-color: var(--sub-color) transparent;
}

.select2-container--default .select2-search--dropdown .select2-search__field {
  border-color: rgba(0, 0, 0, 0.1);
  background: var(--bg-color);
  color: var(--text-color);
  border-radius: var(--roundness);
}

==> monkeytype/src/sass/commandline.scss <==
#commandLineWrapper {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.75);
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  display: grid;
  justify-content: center;
  align-items: start;
  padding: 5rem 0;

  #commandInput {
    width: 700px;
    background: var(--bg-color);
    border-radius: var(--roundness);

    input {
      background: var(--bg-color);
      padding: 1rem;
      color: var(--main-color);
      border: none;
      outline: none;
      font-size: 1rem;
      width: 100%;
      border-radius: var(--roundness);
    }

    .shiftEnter {
      padding: 0.5rem 1rem;
      font-size: 0.75rem;
      line-height: 0.75rem;
      color: var(--sub-color);
      text-align: center;
    }
  }

  #commandLine {
    width: 700px;
    background: var(--bg-color);
    border-radius: var(--roundness);

    .searchicon {
      color: var(--sub-color);
      margin: 1px 1rem 0 1rem;
    }

    input {
      background: var(--bg-color);
      padding: 1rem 1rem 1rem 0;
      color: var(--text-color);
      border: none;
      outline: none;
      font-size: 1rem;
      width: 100%;
      border-radius: var(--roundness);
    }

    .separator {
      background: black;
      width: 100%;
      height: 1px;
      margin-bottom: 0.5rem;
    }

    .listTitle {
      color: var(--text-color);
      padding: 0.5rem 1rem;
      font-size: 0.75rem;
      line-height: 0.75rem;
    }

    .suggestions {
      display: block;
      @extend .ffscroll;
      overflow-y: scroll;
      max-height: calc(100vh - 10rem - 3rem);
      display: grid;
      cursor: pointer;
      user-select: none;

      .entry {
        padding: 0.5rem 1rem;
        font-size: 0.75rem;
        line-height: 0.75rem;
        color: var(--sub-color);
        display: grid;
        grid-template-columns: auto 1fr;

        div {
          pointer-events: none;
        }

        .textIcon {
          font-weight: 900;
          /* width: 1.25rem; */
          display: inline-block;
          letter-spacing: -0.1rem;
          margin-right: 0.5rem;
          text-align: center;
          width: 1.25em;
        }

        .fas {
          margin-right: 0.5rem;
        }

        &:last-child {
          border-radius: 0 0 var(--roundness) var(--roundness);
        }

        &.activeMouse {
          color: var(--bg-color);
          background: var(--text-color);
          cursor: pointer;
        }

        &.activeKeyboard {
          color: var(--bg-color);
          background: var(--text-color);
        }

        // &:hover {
        //   color: var(--text-color);
        //   background: var(--sub-color);
        //   cursor: pointer;
        // }
      }
    }
  }
}

==> monkeytype/src/sass/notifications.scss <==
#notificationCenter {
  width: 350px;
  z-index: 99999999;
  display: grid;
  gap: 1rem;
  position: fixed;
  right: 1rem;
  top: 1rem;
  .history {
    display: grid;
    gap: 1rem;
  }
  .notif {
    user-select: none;
    .icon {
      color: var(--bg-color);
      opacity: 0.5;
      padding: 1rem 1rem;
      align-items: center;
      display: grid;
      font-size: 1.25rem;
    }
    .message {
      padding: 1rem 1rem 1rem 0;
      .title {
        color: var(--bg-color);
        font-size: 0.75em;
        opacity: 0.5;
        line-height: 0.75rem;
      }
    }

    position: relative;
    background: var(--sub-color);
    color: var(--bg-color);
    display: grid;
    grid-template-columns: min-content auto min-content;
    border-radius: var(--roundness);
    border-width: 0.25rem;

    &.bad {
      background-color: var(--error-color);
    }

    &.good {
      background-color: var(--main-color);
    }

    &:hover {
      // opacity: .5;
      // box-shadow: 0 0 20px rgba(0,0,0,.25);
      cursor: pointer;
      &::after {
        opacity: 1;
      }
    }
    &::after {
      transition: 0.125s;
      font-family: "Font Awesome 5 Free";
      background: rgba(0, 0, 0, 0.5);
      opacity: 0;
      font-weight: 900;
      content: "\f00d";
      position: absolute;
      width: 100%;
      height: 100%;
      color: var(--bg-color);
      font-size: 2.5rem;
      display: grid;
      /* align-self: center; */
      align-items: center;
      text-align: center;
      border-radius: var(--roundness);
    }
  }
}

==> monkeytype/src/sass/caret.scss <==
#caret {
  height: 1.5rem;
  background: var(--caret-color);
  animation: caretFlashSmooth 1s infinite;
  position: absolute;
  border-radius: var(--roundness);
  // transition: 0.05s;
  transform-origin: top left;
}

#paceCaret {
  height: 1.5rem;
  // background: var(--sub-color);
  background: var(--sub-color);
  opacity: 0.5;
  position: absolute;
  border-radius: var(--roundness);
  // transition: 0.25s;
  transform-origin: top left;
  width: 2px;
}

#caret,
#paceCaret {
  &.off {
    width: 0;
  }

  &.default {
    width: 2px;
  }

  &.carrot {
    background-color: transparent;
    background-image: url("../images/carrot.png");
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    width: 0.25rem;
    &.size2 {
      margin-left: -0.1rem;
    }
    &.size3 {
      margin-left: -0.2rem;
    }
    &.size4 {
      margin-left: -0.3rem;
    }
  }

  &.banana {
    background-color: transparent;
    background-image: url("../images/banana.png");
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    width: 1rem;
    &.size2 {
      margin-left: -0.1rem;
    }
    &.size3 {
      margin-left: -0.5rem;
    }
    &.size4 {
      margin-left: -0.3rem;
    }
  }

  &.block {
    width: 0.7em;
    margin-left: 0.25em;
    border-radius: 0;
    z-index: -1;
  }

  &.outline {
    @extend #caret, .block;
    animation-name: none;
    background: transparent;
    border: 1px solid var(--caret-color);
  }

  &.underline {
    height: 2px;
    width: 0.8em;
    margin-top: 1.3em;
    margin-left: 0.3em;

    &.size125 {
      margin-top: 1.8em;
    }

    &.size15 {
      margin-top: 2.1em;
    }

    &.size2 {
      margin-top: 2.7em;
    }

    &.size3 {
      margin-top: 3.9em;
    }
    &.size4 {
      margin-top: 4.7em;
    }
  }

  &.size125 {
    transform: scale(1.25);
  }

  &.size15 {
    transform: scale(1.45);
  }

  &.size2 {
    transform: scale(1.9);
  }

  &.size3 {
    transform: scale(2.8);
  }

  &.size4 {
    transform: scale(3.7);
  }
}

==> monkeytype/src/sass/test.scss <==
#timerWrapper {
  opacity: 0;
  transition: 0.25s;
  z-index: -1;
  position: relative;
  z-index: 99;
  #timer {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    /*   height: 0.5rem; */
    height: 0.5rem;
    background: black;
    /*   background: #0f0f0f; */
    /*   background: red; */
    // transition: 1s linear;
    z-index: -1;

    &.timerMain {
      background: var(--main-color);
    }

    &.timerSub {
      background: var(--sub-color);
    }

    &.timerText {
      background: var(--text-color);
    }
  }
}

.pageTest {
  position: relative;

  .ssWatermark {
    font-size: 1.25rem;
    color: var(--sub-color);
    line-height: 1rem;
    text-align: right;
  }

  #timerNumber {
    pointer-events: none;
    transition: 0.25s;
    height: 0;
    color: black;
    line-height: 0;
    z-index: -1;
    text-align: center;
    left: 0;
    width: 100%;
    position: relative;
    font-size: 10rem;
    opacity: 0;
    width: 0;
    height: 0;
    margin: 0 auto;
    display: grid;
    justify-content: center;
    bottom: 6rem;
    transition: none;
  }

  #largeLiveWpmAndAcc {
    font-size: 10rem;
    color: black;
    width: 100%;
    left: 0;
    text-align: center;
    z-index: -1;
    height: 0;
    line-height: 0;
    top: 5rem;
    position: relative;
    display: grid;
    grid-auto-flow: column;
    justify-content: center;
    gap: 5rem;

    #liveWpm {
      opacity: 0;
    }

    #liveAcc {
      opacity: 0;
    }

    #liveBurst {
      opacity: 0;
    }
  }

  #largeLiveWpmAndAcc.timerMain,
  #timerNumber.timerMain {
    color: var(--main-color);
  }

  #timer.timerMain {
    background: var(--main-color);
  }

  #largeLiveWpmAndAcc.timerSub,
  #timerNumber.timerSub {
    color: var(--sub-color);
  }

  #timer.timerSub {
    background: var(--sub-color);
  }

  #largeLiveWpmAndAcc.timerText,
  #timerNumber.timerText {
    color: var(--text-color);
  }

  #timer.timerText {
    background: var(--text-color);
  }
}

#words {
  height: fit-content;
  height: -moz-fit-content;
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  align-content: flex-start;
  user-select: none;
  padding-bottom: 1em;

  .newline {
    width: inherit;
  }

  letter {
    border-bottom-style: solid;
    border-bottom-width: 0.05em;
    border-bottom-color: transparent;
    &.dead {
      border-bottom-width: 0.05em;
      border-bottom-color: var(--sub-color);
    }
    &.tabChar,
    &.nlChar {
      margin: 0 0.25rem;
      opacity: 0.2;
    }
  }

  /* a little hack for right-to-left languages */
  &.rightToLeftTest {
    //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
    direction: rtl;
    .word {
      //flex-direction: row-reverse;
      direction: rtl;
    }
  }
  &.withLigatures {
    letter {
      display: inline;
    }
  }
  &.blurred {
    opacity: 0.25;
    filter: blur(4px);
    -webkit-filter: blur(4px);
  }

  &.flipped {
    .word {
      color: var(--text-color);

      & letter.dead {
        border-bottom-color: var(--sub-color) !important;
      }

      & letter.correct {
        color: var(--sub-color);
      }

      & letter.corrected {
        color: var(--sub-color);
        border-bottom: 2px dotted var(--main-color);
      }

      & letter.extraCorrected {
        border-right: 2px dotted var(--main-color);
      }
    }
  }

  &.colorfulMode {
    .word {
      & letter.dead {
        border-bottom-color: var(--main-color) !important;
      }

      & letter.correct {
        color: var(--main-color);
      }

      & letter.corrected {
        color: var(--main-color);
        border-bottom: 2px dotted var(--text-color);
      }

      & letter.extraCorrected {
        border-right: 2px dotted var(--text-color);
      }

      & letter.incorrect {
        color: var(--colorful-error-color);
      }

      & letter.incorrect.extra {
        color: var(--colorful-error-extra-color);
      }
    }
  }

  &.flipped.colorfulMode {
    .word {
      color: var(--main-color);

      & letter.dead {
        border-bottom-color: var(--sub-color) !important;
      }

      & letter.correct {
        color: var(--sub-color);
      }

      & letter.corrected {
        color: var(--sub-color);
        border-bottom: 2px dotted var(--main-color);
      }

      & letter.extraCorrected {
        border-right: 2px dotted var(--main-color);
      }

      & letter.incorrect {
        color: var(--colorful-error-color);
      }

      & letter.incorrect.extra {
        color: var(--colorful-error-extra-color);
      }
    }
  }
}

.word {
  margin: 0.25rem;
  color: var(--sub-color);
  font-variant: no-common-ligatures;
  // display: flex;
  // transition: 0.25s
  /*   margin-bottom: 1px; */
  border-bottom: 2px solid transparent;
  line-height: 1rem;
  letter {
    display: inline-block;
  }

  &.lastbeforenewline::after {
    font-family: "Font Awesome 5 Free";
    font-weight: 600;
    content: "\f107";
    margin-left: 0.5rem;
    opacity: 0.25;
  }

  // transition: .25s;
  .wordInputAfter {
    opacity: 1;
    position: absolute;
    background: var(--sub-color);
    color: var(--bg-color);
    /* background: red; */
    padding: 0.5rem;
    /* left: .5rem; */
    margin-left: -0.5rem;
    // margin-top: -1.5rem;
    border-radius: var(--roundness);
    // box-shadow: 0 0 10px rgba(0,0,0,.25);
    transition: 0.25s;
    text-shadow: none;
    top: -0.5rem;
    z-index: 10;
    cursor: text;
    .speed {
      font-size: 0.75rem;
    }
  }
}

#words.size125 .word {
  line-height: 1.25rem;
  font-size: 1.25rem;
  margin: 0.31rem;
}

#words.size15 .word {
  line-height: 1.5rem;
  font-size: 1.5rem;
  margin: 0.37rem;
}

#words.size2 .word {
  line-height: 2rem;
  font-size: 2rem;
  margin: 0.5rem;
}

#words.size3 .word {
  line-height: 3rem;
  font-size: 3rem;
  margin: 0.75rem;
}

#words.size4 .word {
  line-height: 4rem;
  font-size: 4rem;
  margin: 1rem;
}

#words.nospace {
  .word {
    margin: 0.5rem 0;
  }
}

#words.arrows {
  .word {
    margin: 0.5rem 0;
    letter {
      margin: 0 0.25rem;
    }
  }
}

.word.error {
  /*   margin-bottom: 1px; */
  border-bottom: 2px solid var(--error-color);
  text-shadow: 1px 0px 0px var(--bg-color),
    // 2px 0px 0px var(--bg-color),
    -1px 0px 0px var(--bg-color),
    // -2px 0px 0px var(--bg-color),
    0px 1px 0px var(--bg-color),
    1px 1px 0px var(--bg-color), -1px 1px 0px var(--bg-color);
}

#words.noErrorBorder,
#resultWordsHistory.noErrorBorder {
  .word.error {
    text-shadow: none;
  }
}
// .word letter {
// transition: .1s;
// height: 1rem;
// line-height: 1rem;
/* margin: 0 1px; */
// }

.word letter.correct {
  color: var(--text-color);
}

.word letter.corrected {
  color: var(--text-color);
  border-bottom: 2px dotted var(--main-color);
}

.word letter.extraCorrected {
  border-right: 2px dotted var(--main-color);
}

.word letter.incorrect {
  color: var(--error-color);
  position: relative;
}

.word letter.incorrect hint {
  position: absolute;
  bottom: -1em;
  color: var(--text-color);
  line-height: initial;
  font-size: 0.75em;
  text-shadow: none;
  padding: 1px;
  left: 0;
  opacity: 0.5;
  text-align: center;
  width: 100%;
}

.word letter.incorrect.extra {
  color: var(--error-extra-color);
}

.word letter.missing {
  opacity: 0.5;
}

#words.flipped.colorfulMode .word.error,
#words.colorfulMode .word.error {
  border-bottom: 2px solid var(--colorful-error-color);
}

#wordsInput {
  opacity: 0;
  padding: 0;
  margin: 0;
  border: none;
  outline: none;
  display: block;
  resize: none;
  position: fixed;
  z-index: -1;
  cursor: default;
  pointer-events: none;
}

#capsWarning {
  background: var(--main-color);
  color: var(--bg-color);
  display: table;
  position: absolute;
  left: 50%;
  // top: 66vh;
  transform: translateX(-50%) translateY(-50%);
  padding: 1rem;
  border-radius: var(--roundness);
  /* margin-top: 1rem; */
  transition: 0.25s;
  z-index: 999;
  pointer-events: none;

  i {
    margin-right: 0.5rem;
  }
}

#result {
  display: grid;
  // height: 200px;
  gap: 1rem;
  // grid-template-columns: auto 1fr;
  // justify-content: center;
  align-items: center;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "stats chart"
    "morestats morestats";
  // "wordsHistory wordsHistory"
  // "buttons buttons"
  // "login login"
  // "ssw ssw";

  &:focus {
    outline: none;
  }

  .buttons {
    display: grid;
    grid-auto-flow: column;
    gap: 1rem;
    justify-content: center;
    // grid-area: buttons;
    grid-column: 1/3;
  }

  .ssWatermark {
    // grid-area: ssw;
    grid-column: 1/3;
  }

  #resultWordsHistory,
  #resultReplay {
    // grid-area: wordsHistory;
    color: var(--sub-color);
    // grid-column: 1/3;
    margin-bottom: 1rem;
    .icon-button {
      padding: 0;
      margin-left: 0.5rem;
    }
    .heatmapLegend {
      display: inline-grid;
      grid-template-columns: auto auto auto;
      gap: 1rem;
      font-size: 0.75rem;
      color: var(--sub-color);
      width: min-content;
      .boxes {
        display: flex;
        .box {
          width: 1rem;
          height: 1rem;
        }
        .box:nth-child(1) {
          background: var(--colorful-error-color);
          border-radius: var(--roundness) 0 0 var(--roundness);
        }
        .box:nth-child(2) {
          background: var(--colorful-error-color);
          filter: opacity(0.6);
        }
        .box:nth-child(3) {
          background: var(--sub-color);
        }
        .box:nth-child(4) {
          background: var(--main-color);
          filter: opacity(0.6);
        }
        .box:nth-child(5) {
          background: var(--main-color);
          border-radius: 0 var(--roundness) var(--roundness) 0;
        }
      }
    }
    .title {
      user-select: none;
      margin-bottom: 0.25rem;
    }
    .words {
      display: flex;
      flex-wrap: wrap;
      width: 100%;
      align-content: flex-start;
      user-select: none;
      .word {
        position: relative;
        margin: 0.18rem 0.6rem 0.15rem 0;
        letter.correct {
          color: var(--text-color);
        }
        letter.incorrect {
          color: var(--error-color);
        }
        letter.incorrect.extra {
          color: var(--error-extra-color);
        }
        &.heatmap-0 letter {
          color: var(--colorful-error-color);
        }
        &.heatmap-1 letter {
          color: var(--colorful-error-color);
          filter: opacity(0.6);
        }
        &.heatmap-2 letter {
          color: var(--sub-color);
        }
        &.heatmap-3 letter {
          color: var(--main-color);
          filter: opacity(0.6);
        }
        &.heatmap-4 letter {
          color: var(--main-color);
        }
      }
      &.rightToLeftTest {
        //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
        direction: rtl;
        .word {
          //flex-direction: row-reverse;
          direction: rtl;
        }
      }
      &.withLigatures {
        letter {
          display: inline;
        }
      }
    }
  }

  .chart {
    grid-area: chart;
    width: 100%;

    canvas {
      width: 100% !important;
      height: 100%;
    }

    max-height: 200px;
    height: 200px;

    .title {
      color: var(--sub-color);
      margin-bottom: 1rem;
    }
  }

  .loginTip {
    grid-column: 1/3;
    text-align: center;
    color: var(--sub-color);
    // grid-area: login;
    grid-column: 1/3;
    .link {
      text-decoration: underline;
      display: inline-block;
      cursor: pointer;
    }
  }

  .stats {
    grid-area: stats;
    display: grid;
    // column-gap: 0.5rem;
    gap: 0.5rem;
    justify-content: center;
    align-items: center;
    // grid-template-areas:
    //   "wpm acc"
    //   "wpm key"
    //   "raw time"
    //   "consistency consistency"
    //   "source source"
    //   "leaderboards leaderboards"
    //   "testType infoAndTags";
    // grid-template-areas:
    //   "wpm acc key consistency testType leaderboards source"
    //   "wpm raw time nothing infoAndTags leaderboards source";
    grid-template-areas:
      "wpm"
      "acc";
    margin-bottom: 1rem;

    &.morestats {
      display: grid;
      grid-auto-flow: column;
      grid-template-areas: none;
      align-items: flex-start;
      justify-content: space-between;
      column-gap: 2rem;
      grid-area: morestats;

      // grid-template-areas: "raw consistency testType infoAndTags leaderboards source"
      //   "key time testType infoAndTags leaderboards source";
      .subgroup {
        display: grid;
        gap: 0.5rem;
      }
    }

    .group {
      // margin-bottom: 0.5rem;

      .top {
        color: var(--sub-color);
        font-size: 1rem;
        line-height: 1rem;
        margin-bottom: 0.25rem;
      }

      .bottom {
        color: var(--main-color);
        font-size: 2rem;
        line-height: 2rem;
      }

      &.time {
        .afk,
        .timeToday {
          color: var(--sub-color);
          font-size: 0.75rem;
          line-height: 0.75rem;
          margin-left: 0.2rem;
        }
      }

      &.source {
        #rateQuoteButton,
        #reportQuoteButton {
          padding: 0 0.25rem;
        }
        #rateQuoteButton {
          display: inline-grid;
          gap: 0.25rem;
        }
      }
    }

    // .infoAndTags {
    //   display: grid;
    //   gap: 0.5rem;
    //   align-self: baseline;
    //   // grid-area: infoAndTags;
    //   color: var(--sub-color);

    //   .top {
    //     font-size: 1rem;
    //     line-height: 1rem;
    //   }

    //   .bottom {
    //     font-size: 1rem;
    //     line-height: 1rem;
    //   }
    // }

    .info,
    .tags,
    .source {
      .top {
        font-size: 1rem;
        line-height: 1rem;
      }

      .bottom {
        font-size: 1rem;
        line-height: 1rem;
      }
    }

    .source {
      max-width: 30rem;
    }

    .tags .bottom .fas {
      margin-left: 0.5rem;
    }

    .wpm {
      grid-area: wpm;

      .top {
        font-size: 2rem;
        line-height: 1.5rem;
        display: flex;
        // margin-top: -0.5rem;

        // .crownWrapper {
        //   width: 1.7rem;
        //   overflow: hidden;
        //   height: 1.7rem;
        //   margin-left: 0.5rem;
        //   // margin-top: 0.98rem;
        //   margin-top: -0.5rem;

        .crown {
          height: 1.7rem;
          width: 1.7rem;
          margin-left: 0.5rem;
          margin-top: -0.2rem;
          font-size: 0.7rem;
          line-height: 1.7rem;
          background: var(--main-color);
          color: var(--bg-color);
          border-radius: 0.6rem;
          text-align: center;
          align-self: center;
          width: 1.7rem;
          height: 1.7rem;
        }
        // }
      }

      .bottom {
        font-size: 4rem;
        line-height: 4rem;
      }
    }

    .testType,
    .leaderboards {
      .bottom {
        font-size: 1rem;
        line-height: 1rem;
        .lbChange .fas {
          margin-right: 0.15rem;
        }
      }
    }

    .acc {
      grid-area: acc;

      .top {
        font-size: 2rem;
        line-height: 1.5rem;
      }

      .bottom {
        font-size: 4rem;
        line-height: 4rem;
      }
    }

    .burst {
      grid-area: burst;

      .top {
        font-size: 2rem;
        line-height: 1.5rem;
      }

      .bottom {
        font-size: 4rem;
        line-height: 4rem;
      }
    }

    // .key {
    //   grid-area: key;
    // }

    // .time {
    //   grid-area: time;
    // }

    // .raw {
    //   grid-area: raw;
    // }
  }
}

#restartTestButton,
#showWordHistoryButton,
#saveScreenshotButton,
#restartTestButtonWithSameWordset,
#nextTestButton,
#practiseWordsButton,
#watchReplayButton {
  position: relative;
  border-radius: var(--roundness);
  padding: 1rem 2rem;
  width: min-content;
  width: -moz-min-content;
  color: var(--sub-color);
  transition: 0.25s;
  cursor: pointer;

  &:hover,
  &:focus {
    color: var(--main-color);
    outline: none;
  }

  &:focus {
    background: var(--sub-color);
  }
}

#retrySavingResultButton {
  position: relative;
  border-radius: var(--roundness);
  padding: 1rem 2rem;
  color: var(--error-color);
  transition: 0.25s;
  cursor: pointer;
  width: max-content;
  width: -moz-max-content;
  background: var(--colorful-error-color);
  color: var(--bg-color);
  justify-self: center;
  justify-content: center;
  margin: 0 auto 1rem auto;
  user-select: none;

  &:hover,
  &:focus {
    background: var(--text-color);
    outline: none;
  }

  &:focus {
    background: var(--text-color);
  }
}

#showWordHistoryButton {
  opacity: 1;
}

#replayWords {
  cursor: pointer;
}

#replayStopwatch {
  color: var(--main-color);
  display: inline-block;
  margin: 0;
}

#restartTestButton {
  margin: 0 auto;
  margin-top: 1rem;
}

.pageTest {
  #wordsWrapper {
    position: relative;
  }
  #memoryTimer {
    background: var(--main-color);
    color: var(--bg-color);
    padding: 1rem;
    border-radius: var(--roundness);
    /* width: min-content; */
    text-align: center;
    width: max-content;
    /* justify-self: center; */
    left: 50%;
    position: absolute;
    transform: translateX(-50%);
    top: -6rem;
    user-select: none;
    pointer-events: none;
    opacity: 0;
  }
  .outOfFocusWarning {
    text-align: center;
    height: 0;
    line-height: 150px;
    z-index: 999;
    position: relative;
    user-select: none;
    pointer-events: none;
  }

  #testModesNotice {
    display: grid;
    grid-auto-flow: column;
    gap: 1rem;
    color: var(--sub-color);
    text-align: center;
    margin-bottom: 1.25rem;
    height: 1rem;
    line-height: 1rem;
    transition: 0.125s;
    justify-content: center;
    user-select: none;

    .fas {
      margin-right: 0.5rem;
    }
  }
  #miniTimerAndLiveWpm {
    height: 0;
    margin-left: 0.37rem;
    display: flex;
    font-size: 1rem;
    line-height: 1rem;
    margin-top: -1.5rem;
    position: absolute;
    color: black;

    .time {
      margin-right: 2rem;
    }

    .wpm,
    .acc {
      margin-right: 2rem;
    }

    .time,
    .wpm,
    .acc,
    .burst {
      opacity: 0;
    }

    &.timerMain {
      color: var(--main-color);
    }

    &.timerSub {
      color: var(--sub-color);
    }

    &.timerText {
      color: var(--text-color);
    }

    &.size125 {
      margin-top: -1.75rem;
      font-size: 1.25rem;
      line-height: 1.25rem;
    }
    &.size15 {
      margin-top: -2rem;
      font-size: 1.5rem;
      line-height: 1.5rem;
    }
    &.size2 {
      margin-top: -2.5rem;
      font-size: 2rem;
      line-height: 2rem;
    }
    &.size3 {
      margin-top: -3.5rem;
      font-size: 3rem;
      line-height: 3rem;
    }
    &.size4 {
      margin-top: -4.5rem;
      font-size: 4rem;
      line-height: 4rem;
    }
  }
}

#middle.focus .pageTest {
  #testModesNotice {
    opacity: 0 !important;
  }
}

==> monkeytype/src/sass/leaderboards.scss <==
#leaderboardsWrapper {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.75);
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  display: grid;
  justify-content: center;
  align-items: center;
  padding: 5rem 0;

  #leaderboards {
    width: 85vw;
    // height: calc(95vh - 5rem);
    overflow-y: auto;
    background: var(--bg-color);
    border-radius: var(--roundness);
    padding: 2rem;
    display: grid;
    gap: 2rem 0;
    grid-template-rows: 3rem auto;
    grid-template-areas:
      "title buttons"
      "tables tables";
    grid-template-columns: 1fr 1fr;

    .leaderboardsTop {
      width: 200%;
      min-width: 100%;
      display: flex;
      align-items: center;
      justify-content: space-between;

      .buttonGroup .button {
        padding: 0.4rem 2.18rem;
      }
    }

    .mainTitle {
      font-size: 3rem;
      line-height: 3rem;
      grid-area: title;
    }

    .subTitle {
      color: var(--sub-color);
    }

    .title {
      font-size: 2rem;
      line-height: 2rem;
      margin-bottom: 0.5rem;
    }

    .tables {
      grid-area: tables;
      display: grid;
      gap: 1rem;
      grid-template-columns: 1fr 1fr;
      font-size: 0.8rem;
      width: 100%;

      .sub {
        opacity: 0.5;
      }

      .alignRight {
        text-align: right;
      }

      .titleAndTable {
        display: grid;
        width: 100%;

        .titleAndButtons {
          display: grid;
          grid-template-columns: 1fr auto;
          .buttons {
            display: grid;
            grid-template-columns: auto 1fr 1fr;
            align-items: center;
            // margin-top: .1rem;
            gap: 1rem;
            color: var(--sub-color);
            .button {
              padding-left: 1rem;
              padding-right: 1rem;
            }
          }
        }

        .title {
          grid-area: 1/1;
          margin-bottom: 0;
          line-height: 2.5rem;
        }

        .subtitle {
          grid-area: 1/1;
          align-self: center;
          justify-self: right;
          color: var(--sub-color);
        }
      }

      .leftTableWrapper,
      .rightTableWrapper {
        height: calc(100vh - 22rem);
        @extend .ffscroll;
        overflow-y: scroll;
        overflow-x: auto;
      }

      .leftTableWrapper::-webkit-scrollbar,
      .rightTableWrapper::-webkit-scrollbar {
        height: 5px;
        width: 5px;
      }

      table {
        width: 100%;
        border-spacing: 0;
        border-collapse: collapse;

        tr td:first-child {
          text-align: center;
        }

        tr.me {
          td {
            color: var(--main-color);
            // font-weight: 900;
          }
        }

        td {
          padding: 0.5rem 0.5rem;
        }

        thead {
          color: var(--sub-color);
          font-size: 0.75rem;

          td {
            padding: 0.5rem;
            background: var(--bg-color);
            position: -webkit-sticky;
            position: sticky;
            top: 0;
            z-index: 99;
          }
        }

        tbody {
          color: var(--text-color);

          tr:nth-child(odd) td {
            background: rgba(0, 0, 0, 0.1);
          }
        }

        tfoot {
          td {
            padding: 1rem 0.5rem;
            position: -webkit-sticky;
            position: sticky;
            bottom: -5px;
            background: var(--bg-color);
            color: var(--main-color);
            z-index: 4;
          }
        }

        tr {
          td:first-child {
            padding-left: 1rem;
          }
          td:last-child {
            padding-right: 1rem;
          }
        }
      }
    }

    .buttons {
      .buttonGroup {
        display: grid;
        grid-auto-flow: column;
        gap: 1rem;
        grid-area: 1/2;
      }
    }
  }
}

==> monkeytype/src/sass/keymap.scss <==
.keymap {
  display: grid;
  grid-template-rows: 1fr 1fr 1fr;
  justify-content: center;
  white-space: nowrap;
  // height: 140px;
  gap: 0.25rem;
  margin-top: 1rem;
  user-select: none;

  .row {
    height: 2rem;
    gap: 0.25rem;
  }

  .keymap-key {
    display: flex;
    background-color: transparent;
    color: var(--sub-color);
    border-radius: var(--roundness);
    border: 0.05rem solid;
    border-color: var(--sub-color);
    text-align: center;
    justify-content: center;
    align-items: center;
    width: 2rem;
    height: 2rem;
    position: relative;

    .bump {
      width: 0.75rem;
      height: 0.05rem;
      background: var(--sub-color);
      position: absolute;
      border-radius: var(--roundness);
      // margin-top: 1.5rem;
      bottom: 0.15rem;
    }

    &.active-key {
      color: var(--bg-color);
      background-color: var(--main-color);
      border-color: var(--main-color);

      .bump {
        background: var(--bg-color);
      }
    }

    &#KeySpace {
      &:hover {
        cursor: pointer;
        color: var(--main-color);
      }
    }

    &#KeySpace,
    &#KeySpace2 {
      width: 100%;
    }

    &#KeySpace2 {
      opacity: 0;
    }

    &.flash {
      animation: flashKey 1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
    }
  }

  .hidden-key,
  .hide-key {
    opacity: 0;
  }

  .keymap-split-spacer,
  .keymap-stagger-split-spacer,
  .keymap-matrix-split-spacer {
    display: none;
  }

  .r1 {
    display: grid;
    grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  .r2 {
    display: grid;
    grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1rem;
  }

  .r3 {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  .r4 {
    display: grid;
    grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2.75fr;
  }

  .r5 {
    display: grid;
    grid-template-columns: 3.5fr 6fr 3.5fr;
    font-size: 0.5rem;
    // &.matrixSpace {
    //   // grid-template-columns: 6.75fr 1.9fr 6.75fr;
    //   grid-template-columns: 6.9fr 4.6fr 6.9fr; // wider spacebar
    // }
    // &.splitSpace {
    //   // grid-template-columns: 6.75fr 1.9fr 6.75fr;
    //   grid-template-columns: 4fr 7.5fr 4fr;
    // }
  }
  &.matrix {
    .r1,
    .r2,
    .r3 {
      grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
    }

    .r4 {
      grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
    }

    .r5 {
      grid-template-columns: 3.25fr 5fr 2fr 1fr;
    }

    .r1,
    .r2,
    .r3 {
      :nth-child(13) {
        opacity: 0;
      }

      :nth-child(14) {
        opacity: 0;
      }
    }
  }
  &.split {
    .keymap-split-spacer {
      display: block;
    }
    .keymap-stagger-split-spacer {
      display: block;
    }

    .r1 {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
    }

    .r2 {
      display: grid;
      grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
    }

    .r3 {
      display: grid;
      grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
    }

    .r4 {
      display: grid;
      grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
    }
    .r5 {
      grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr;
    }
    #KeySpace2 {
      opacity: 1;
    }
  }
  &.split_matrix {
    .keymap-split-spacer {
      display: block;
      width: 2rem;
      height: 2rem;
    }
    .keymap-stagger-split-spacer {
      display: none;
    }
    .keymap-matrix-split-spacer {
      display: block;
      width: 2rem;
      height: 2rem;
    }
    .r1,
    .r2,
    .r3 {
      grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
    }

    .r4 {
      grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
    }

    .r5 {
      grid-template-columns: 3.225fr 3fr 1fr 3fr 2fr;
    }
    #KeySpace2 {
      opacity: 1;
    }

    .r1 {
      :nth-child(12) {
        opacity: 0;
      }
    }

    .r1,
    .r2,
    .r3 {
      :nth-child(13) {
        opacity: 0;
      }

      :nth-child(14) {
        opacity: 0;
      }
    }
  }
  &.alice {
    .keymap-split-spacer {
      display: block;
    }
    .r4 .keymap-split-spacer {
      display: none;
    }
    .keymap-stagger-split-spacer {
      display: block;
    }

    .r1 {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
      .keymap-key:nth-child(2) {
        //1
        margin-left: 45%;
      }
      .keymap-key:nth-child(3) {
        //2
        margin-top: -2px;
        margin-left: 45%;
      }
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(6),
      .keymap-key:nth-child(7) {
        //3456
        transform: rotate(10deg);
        margin-left: 45%;
      }
      .keymap-key:nth-child(4) {
        //3
        margin-top: 3px;
      }
      .keymap-key:nth-child(5) {
        //4
        margin-top: 10px;
      }
      .keymap-key:nth-child(6) {
        //5
        margin-top: 17px;
      }
      .keymap-key:nth-child(7) {
        //6
        margin-top: 24px;
      }
      .keymap-key:nth-child(9),
      .keymap-key:nth-child(10),
      .keymap-key:nth-child(11),
      .keymap-key:nth-child(12) {
        //7890
        transform: rotate(-10deg);
        margin-left: -48%;
      }
      .keymap-key:nth-child(12) {
        //7
        margin-top: -1px;
      }
      .keymap-key:nth-child(11) {
        //8
        margin-top: 6px;
      }
      .keymap-key:nth-child(10) {
        //9
        margin-top: 13px;
      }
      .keymap-key:nth-child(9) {
        //10
        margin-top: 20px;
      }
      .keymap-key:nth-child(13),
      .keymap-key:nth-child(14) {
        //-=
        margin-left: -40%;
      }
    }

    .r2 {
      display: grid;
      grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
      .keymap-key:nth-child(2) {
        //Q
        margin-left: 20%;
      }
      .keymap-key:nth-child(3),
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(6) {
        //WERT
        transform: rotate(10deg);
        margin-left: 45%;
      }
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(10) {
        //EI
        margin-top: 8px;
      }
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(9) {
        //RU
        margin-top: 15px;
      }
      .keymap-key:nth-child(6),
      .keymap-key:nth-child(8) {
        //TY
        margin-top: 22px;
      }

      .keymap-key:nth-child(8),
      .keymap-key:nth-child(9),
      .keymap-key:nth-child(10),
      .keymap-key:nth-child(11) {
        //YUIO
        transform: rotate(-10deg);
        margin-left: -12%;
      }
    }

    .r3 {
      display: grid;
      grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
      .keymap-key:nth-child(2) {
        //A
        margin-left: -5px;
      }
      .keymap-key:nth-child(3),
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(6) {
        //SDFG
        margin-left: -1px;
        transform: rotate(10deg);
      }
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(10) {
        //DK
        margin-top: 8px;
      }
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(9) {
        //FJ
        margin-top: 15px;
      }
      .keymap-key:nth-child(6),
      .keymap-key:nth-child(8) {
        //GH
        margin-top: 22px;
      }

      .keymap-key:nth-child(8),
      .keymap-key:nth-child(9),
      .keymap-key:nth-child(10),
      .keymap-key:nth-child(11) {
        //HJKL
        transform: rotate(-10deg);
        margin-left: -25%;
      }
    }

    .r4 {
      display: grid;
      grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
      .keymap-key:nth-child(2) {
        margin-left: -18px;
      }
      .keymap-key:nth-child(3) {
        //Z
        margin-left: -15px;
      }
      .keymap-key:nth-child(4),
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(6),
      .keymap-key:nth-child(7) {
        //XCVB
        margin-left: -11px;
        transform: rotate(10deg);
        margin-top: 2px;
      }
      .keymap-key:nth-child(12) {
        //,
        margin-top: 4px;
        margin-left: -5px;
      }
      .keymap-key:nth-child(5),
      .keymap-key:nth-child(11) {
        //CM
        margin-top: 10px;
      }
      .keymap-key:nth-child(6),
      .keymap-key:nth-child(10) {
        //VN
        margin-top: 18px;
      }
      .keymap-key:nth-child(7) {
        //B
        margin-top: 24px;
      }

      .keymap-key:nth-child(10),
      .keymap-key:nth-child(11),
      .keymap-key:nth-child(12) {
        //NM,
        transform: rotate(-10deg);
        margin-left: -25%;
      }
    }
    .r5 {
      grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr;
    }
    #KeySpace2 {
      opacity: 1;
    }

    // div#KeyE.keymap-key,
    // div#KeyD.keymap-key {
    //   margin-top: 6px;
    // }
    // div#KeyC.keymap-key {
    //   margin-top: 8px;
    // }
    // div#KeyR.keymap-key,
    // div#KeyF.keymap-key {
    //   margin-top: 12px;
    // }
    // div#KeyV.keymap-key {
    //   margin-top: 14px;
    // }
    // div#KeyT.keymap-key,
    // div#KeyG.keymap-key {
    //   margin-top: 18px;
    // }
    // div#KeyB.keymap-key {
    //   margin-top: 20px;
    // }
    // div#KeyY.keymap-key,
    // div#KeyU.keymap-key,
    // div#KeyI.keymap-key,
    // div#KeyO.keymap-key {
    //   transform: rotate(-10deg);
    //   margin-left: -25%;
    // }
    // div#KeyH.keymap-key,
    // div#KeyJ.keymap-key,
    // div#KeyK.keymap-key,
    // div#KeyL.keymap-key {
    //   transform: rotate(-10deg);
    //   margin-left: -35%;
    // }
    // div#KeyN.keymap-key,
    // div#KeyM.keymap-key,
    // div#KeyComma.keymap-key {
    //   transform: rotate(-10deg);
    //   margin-left: -16%;
    // }
    // div#KeyP.keymap-key,
    // div#KeyLeftBracket.keymap-key,
    // div#KeyRightBracket.keymap-key {
    //   margin-left: 5%;
    // }
    // div#KeySemicolon.keymap-key,
    // div#KeyQuote.keymap-key {
    //   margin-left: -25%;
    // }
    // div#KeyPeriod.keymap-key,
    // div#KeySlash.keymap-key {
    //   margin-left: -3px;
    // }
    // div#KeyO.keymap-key,
    // div#KeyComma.keymap-key {
    //   margin-top: 3px;
    // }
    // div#KeyL.keymap-key {
    //   margin-top: 1px;
    // }
    // div#KeyI.keymap-key,
    // div#KeyM.keymap-key {
    //   margin-top: 9px;
    // }
    // div#KeyK.keymap-key {
    //   margin-top: 7px;
    // }
    // div#KeyU.keymap-key,
    // div#KeyN.keymap-key {
    //   margin-top: 15px;
    // }
    // div#KeyJ.keymap-key {
    //   margin-top: 13px;
    // }
    // div#KeyY.keymap-key {
    //   margin-top: 21px;
    // }
    // div#KeyH.keymap-key {
    //   margin-top: 19px;
    // }
    div#KeySpace.keymap-key {
      transform: rotate(10deg);
      margin-left: -5%;
      margin-top: 21%;
    }
    div#KeySpace2.keymap-key {
      transform: rotate(-10deg);
      margin-left: -33%;
      margin-top: 20%;
    }
    div#KeyBackslash.keymap-key {
      visibility: hidden;
    }

    div.extraKey {
      margin-top: 25px;
      transform: rotate(-10deg) !important;
      margin-left: -7px !important;
      display: flex;
      background-color: transparent;
      color: var(--sub-color);
      border-radius: var(--roundness);
      border: 0.05rem solid;
      border-color: var(--sub-color);
      text-align: center;
      justify-content: center;
      align-items: center;
      width: 2rem;
      height: 2rem;
      position: relative;
    }
    // div#KeySpace.keymap-key:after {
    //     content: 'Alice';
    //     text-indent: 0;
    //     font-weight: 600!important;
    //     margin: auto;
    //     font-size: 0.9rem;
    //     color: var(--bg-color)
    // }
  }
}

==> monkeytype/src/sass/nav.scss <==
#menu {
  font-size: 1rem;
  line-height: 1rem;
  color: var(--sub-color);
  display: grid;
  grid-auto-flow: column;
  gap: 0.5rem;
  // margin-bottom: -0.4rem;
  width: fit-content;
  width: -moz-fit-content;

  .icon-button {
    //   .icon {
    //     display: grid;
    //     align-items: center;
    //     justify-items: center;
    //     text-align: center;
    //     width: 1.25rem;
    //     height: 1.25rem;
    //   }
    text-decoration: none;

    .text {
      font-size: 0.65rem;
      line-height: 0.65rem;
      align-self: center;
      margin-left: 0.25rem;
    }

    //   &:hover {
    //     cursor: pointer;
    //     color: var(--main-color);
    //   }
  }

  .separator {
    width: 2px;
    height: 1rem;
    background-color: var(--sub-color);
  }
}

#top.focus #menu .icon-button.discord::after {
  background: transparent;
}

#top.focus #menu {
  color: transparent !important;
}

#top.focus #menu .icon-button {
  color: transparent !important;
}

#top {
  grid-template-areas: "logo menu config";
  line-height: 2.3rem;
  font-size: 2.3rem;
  /* text-align: center; */
  // transition: 0.25s;
  padding: 0 5px;
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: auto 1fr auto;
  z-index: 2;
  align-items: center;
  gap: 0.5rem;
  user-select: none;

  .logo {
    // margin-bottom: 0.6rem;
    cursor: pointer;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem;

    .icon {
      width: 2.5rem;
      display: grid;
      align-items: center;
      background-color: transparent;
      // margin-bottom: 0.15rem;
      svg path {
        transition: 0.25s;
        fill: var(--main-color);
      }
    }
    .text {
      .top {
        position: absolute;
        left: 0.25rem;
        top: -0.1rem;
        font-size: 0.65rem;
        line-height: 0.65rem;
        color: var(--sub-color);
        transition: 0.25s;
      }
      position: relative;
      font-size: 2rem;
      margin-bottom: 0.4rem;
      font-family: "Lexend Deca";
      transition: 0.25s;
    }
    white-space: nowrap;
    user-select: none;

    .bottom {
      margin-left: -0.15rem;
      color: var(--main-color);
      transition: 0.25s;
      cursor: pointer;
    }
  }

  .config {
    grid-area: config;
    transition: 0.125s;
    .mobileConfig {
      display: none;
      .icon-button {
        display: grid;
        grid-auto-flow: column;
        align-content: center;
        transition: 0.25s;
        margin-right: -1rem;
        padding: 0.5rem 1rem;
        font-size: 2rem;
        border-radius: var(--roundness);
        cursor: pointer;
        color: var(--sub-color);
        &:hover {
          color: var(--text-color);
        }
      }
    }

    .desktopConfig {
      justify-self: right;
      display: grid;
      // grid-auto-flow: row;
      grid-template-rows: 0.7rem 0.7rem 0.7rem;
      grid-gap: 0.2rem;
      // width: min-content;
      // width: -moz-min-content;
      // transition: 0.25s;
      /*   margin-bottom: 0.1rem; */
      justify-items: self-end;

      .group {
        // transition: 0.25s;

        .title {
          color: var(--sub-color);
          font-size: 0.5rem;
          line-height: 0.5rem;
          margin-bottom: 0.15rem;
        }

        .buttons {
          font-size: 0.7rem;
          line-height: 0.7rem;
          display: flex;
        }
        &.disabled {
          pointer-events: none;
          opacity: 0.25;
        }
      }

      .punctuationMode {
        margin-bottom: -0.1rem;
      }

      .numbersMode {
        margin-bottom: -0.1rem;
      }
    }
  }

  .result {
    display: grid;
    grid-auto-flow: column;
    grid-gap: 1rem;
    width: min-content;
    width: -moz-min-content;
    transition: 0.25s;
    grid-column: 3/4;
    grid-row: 1/2;

    .group {
      .title {
        font-size: 0.65rem;
        line-height: 0.65rem;
        color: var(--sub-color);
      }

      .val {
        font-size: 1.7rem;
        line-height: 1.7rem;
        color: var(--main-color);
        transition: 0.25s;
      }
    }
  }

  //top focus
  &.focus {
    color: var(--sub-color) !important;

    .result {
      opacity: 0 !important;
    }

    .icon svg path {
      fill: var(--sub-color) !important;
    }

    .logo .text {
      color: var(--sub-color) !important;
      // opacity: 0 !important;
    }

    .logo .top {
      opacity: 0 !important;
    }

    .config {
      opacity: 0 !important;
    }
  }
}

==> monkeytype/src/sass/scroll.scss <==
/* width */
::-webkit-scrollbar {
  width: 7px;
}

/* Track */
::-webkit-scrollbar-track {
  background: transparent;
}

/* Handle */
::-webkit-scrollbar-thumb {
  background: var(--sub-color);
  transition: 0.25s;
  border-radius: 2px !important;
}

/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
  background: var(--main-color);
}

::-webkit-scrollbar-corner {
  background: var(--sub-color);
}

==> monkeytype/src/sass/settings.scss <==
.pageSettings {
  display: grid;
  // grid-template-columns: 1fr 1fr;
  gap: 2rem;

  .tip {
    color: var(--sub-color);
  }

  .sectionGroupTitle {
    font-size: 2rem;
    color: var(--sub-color);
    line-height: 2rem;
    cursor: pointer;
    transition: 0.25s;

    &:hover {
      color: var(--text-color);
    }

    .fas {
      margin-left: 0.5rem;

      &.rotate {
        transform: rotate(-90deg);
      }
    }
  }

  .sectionSpacer {
    height: 1.5rem;
  }

  .settingsGroup {
    display: grid;
    gap: 2rem;
    &.quickNav .links {
      display: grid;
      grid-auto-flow: column;
      text-align: center;
      a {
        text-decoration: none;
        width: 100%;
        cursor: pointer;
        // opacity: 0.5;
        &:hover {
          opacity: 1;
        }
      }
    }
  }

  .section {
    display: grid;
    // gap: .5rem;
    grid-template-areas:
      "title title"
      "text buttons";
    grid-template-columns: 2fr 1fr;
    column-gap: 2rem;
    align-items: center;

    .button.danger {
      box-shadow: 0px 0px 0px 2px var(--error-color);
      color: var(--text-color);
      &:hover {
        background: var(--text-color);
        color: var(--bg-color);
      }
    }

    .inputAndButton {
      display: grid;
      grid-template-columns: 8fr 1fr;
      gap: 0.5rem;
      margin-bottom: 0.5rem;

      .button {
        height: auto;

        .fas {
          margin-right: 0rem;
          vertical-align: sub;
        }
      }
    }

    &.themes .tabContainer [tabcontent="custom"] {
      label.button:first-child {
        color: var(--text-color);
      }
      label.button {
        color: var(--bg-color);
      }
    }

    &.customBackgroundFilter {
      .groups {
        grid-area: buttons;
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 2rem;
        margin-top: 2rem;
        .group {
          display: grid;
          grid-template-columns: 1fr auto 2fr;
          gap: 1rem;
          .title,
          .value {
            color: var(--text-color);
          }
        }
      }
      .saveContainer {
        grid-column: -1/-3;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 1rem;
      }
      .fas {
        margin-right: 0rem;
      }
    }

    &.customTheme {
      grid-template-columns: 1fr 1fr 1fr 1fr;
      justify-items: stretch;
      gap: 0.5rem 2rem;

      & p {
        grid-area: unset;
        grid-column: 1 / span 4;
      }

      & .spacer {
        grid-column: 3 / 5;
      }
    }

    h1 {
      font-size: 1rem;
      line-height: 1rem;
      color: var(--sub-color);
      margin: 0;
      grid-area: title;
      font-weight: 300;
    }

    p {
      grid-area: text;
      color: var(--sub-color);
      margin: 0;
    }

    & > .text {
      align-self: normal;
      color: var(--text-color);
      grid-area: text;
    }

    .buttons {
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: 1fr;
      gap: 0.5rem;
      grid-area: buttons;
      &.vertical {
        grid-auto-flow: unset;
      }
    }

    &.discordIntegration {
      .info {
        grid-area: buttons;
        text-align: center;
        color: var(--main-color);
      }

      #unlinkDiscordButton {
        margin-top: 0.5rem;
        font-size: 0.75rem;
        color: var(--sub-color);
        &:hover {
          color: var(--text-color);
        }
      }

      .howto {
        margin-top: 1rem;
        color: var(--text-color);
      }
    }

    &.tags {
      .tagsListAndButton {
        grid-area: buttons;
      }

      .tag {
        grid-template-columns: 6fr 1fr 1fr 1fr;
        margin-bottom: 0.5rem;
      }

      .addTagButton {
        margin-top: 0.5rem;
        color: var(--text-color);
        cursor: pointer;
        transition: 0.25s;
        padding: 0.2rem 0.5rem;
        border-radius: var(--roundness);

        background: rgba(0, 0, 0, 0.1);
        text-align: center;
        -webkit-user-select: none;
        display: grid;
        align-content: center;
        height: min-content;
        height: -moz-min-content;

        &.active {
          background: var(--main-color);
          color: var(--bg-color);
        }

        &:hover,
        &:focus {
          color: var(--bg-color);
          background: var(--text-color);
          outline: none;
        }
      }
    }

    &.presets {
      .presetsListAndButton {
        grid-area: buttons;
      }

      .preset {
        grid-template-columns: 7fr 1fr 1fr;
        margin-bottom: 0.5rem;
      }

      .addPresetButton {
        margin-top: 0.5rem;
        color: var(--text-color);
        cursor: pointer;
        transition: 0.25s;
        padding: 0.2rem 0.5rem;
        border-radius: var(--roundness);

        background: rgba(0, 0, 0, 0.1);
        text-align: center;
        -webkit-user-select: none;
        display: grid;
        align-content: center;
        height: min-content;
        height: -moz-min-content;

        &.active {
          background: var(--main-color);
          color: var(--bg-color);
        }

        &:hover,
        &:focus {
          color: var(--bg-color);
          background: var(--text-color);
          outline: none;
        }
      }
    }

    &.fontSize .buttons {
      grid-template-columns: 1fr 1fr 1fr 1fr;
    }

    &.themes {
      .tabContainer {
        position: relative;
        grid-area: buttons;

        .tabContent {
          overflow: revert;
          height: auto;

          &.customTheme {
            margin-top: 0.5rem;
            .colorText {
              color: var(--text-color);
            }
          }

          .text {
            align-self: center;
          }
        }
      }

      .theme.button {
        display: grid;
        grid-template-columns: auto 1fr auto;
        .text {
          color: inherit;
        }
        .activeIndicator {
          overflow: hidden;
          width: 1.25rem;
          transition: 0.25s;
          opacity: 0;
          color: inherit;
          .far {
            margin: 0;
          }
          &.active {
            width: 1.25rem;
            opacity: 1;
          }
        }
        .favButton {
          overflow: hidden;
          width: 1.25rem;
          transition: 0.25s;
          opacity: 0;
          .far,
          .fas {
            margin: 0;
            pointer-events: none;
          }
          &:hover {
            cursor: pointer;
          }
          &.active {
            width: 1.25rem;
            opacity: 1;
          }
        }
        &:hover {
          .favButton {
            width: 1.25rem;
            opacity: 1;
          }
        }
        &.active {
          .activeIndicator {
            opacity: 1;
          }
        }
      }
    }

    &.themes {
      grid-template-columns: 2fr 1fr;
      grid-template-areas:
        "title tabs"
        "text text"
        "buttons buttons";
      column-gap: 2rem;
      // row-gap: 0.5rem;

      .tabs {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        gap: 0.5rem;
        grid-area: tabs;
      }

      .buttons {
        margin-left: 0;
        grid-auto-flow: dense;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr 1fr;
        gap: 0.5rem;
        margin-top: 0.5rem;
      }
    }

    &.fullWidth {
      grid-template-columns: 2fr 1fr;
      grid-template-areas:
        "title tabs"
        "text text"
        "buttons buttons";
      column-gap: 2rem;
      // row-gap: 0.5rem;

      .buttons {
        margin-left: 0;
        grid-auto-flow: dense;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr 1fr;
        gap: 0.5rem;
        margin-top: 1rem;
      }
    }

    &.randomTheme .buttons {
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
    }
  }
}

.buttons div.theme:hover {
  transform: scale(1.1);
}

==> monkeytype/src/sass/animations.scss <==
@keyframes loader {
  0% {
    width: 0;
    left: 0;
  }

  50% {
    width: 100%;
    left: 0;
  }

  100% {
    width: 0;
    left: 100%;
  }
}

@keyframes caretFlashSmooth {
  0%,
  100% {
    opacity: 0;
  }

  50% {
    opacity: 1;
  }
}

@keyframes caretFlashHard {
  0%,
  50% {
    opacity: 1;
  }

  51%,
  100% {
    opacity: 0;
  }
}

@keyframes flashKey {
  from {
    color: var(--bg-color);
    background-color: var(--main-color);
    border-color: var(--main-color);
  }

  to {
    color: var(--sub-color);
    background-color: var(--bg-color);
    border-color: var(--sub-color);
  }
}

@keyframes shake {
  0% {
    transform: translate(4px, 0) rotate(0deg);
  }
  50% {
    transform: translate(-4px, 0) rotate(0deg);
  }
  100% {
    transform: translate(4px, 0) rotate(0deg);
  }
}

@keyframes flashHighlight {
  0% {
    background-color: var(--bg-color);
  }
  10% {
    background-color: var(--main-color);
  }
  40% {
    background-color: var(--main-color);
  }
  100% {
    background-color: var(--bg-color);
  }
}

==> monkeytype/src/sass/footer.scss <==
#bottom {
  position: relative;
  text-align: center;
  line-height: 1rem;
  font-size: 0.75rem;
  color: var(--sub-color);
  // transition: 0.25s;
  padding: 0 5px;

  // margin-bottom: 2rem;
  .keyTips {
    transition: 0.25s;
    margin-bottom: 2rem;
  }

  #supportMeButton {
    transition: 0.25s;
    &:hover {
      color: var(--text-color);
      cursor: pointer;
    }
  }

  #commandLineMobileButton {
    display: none;
    top: -4rem;
    left: 0;
    position: absolute;
    font-size: 1rem;
    width: 3rem;
    height: 3rem;
    text-align: center;
    line-height: 3rem;
    background: var(--main-color);
    border-radius: 99rem;
    z-index: 99;
    cursor: pointer;
    color: var(--bg-color);
    transition: 0.25s;
  }

  .leftright {
    display: grid;
    grid-template-columns: auto auto;
    gap: 1rem;
    a {
      text-decoration: none;
    }
    .left {
      text-align: left;
      display: grid;
      grid-auto-flow: column;
      width: fit-content;
      gap: 1rem;
      width: -moz-fit-content;
    }
    .right {
      text-align: right;
      display: grid;
      grid-auto-flow: column;
      width: fit-content;
      width: -moz-fit-content;
      justify-self: right;
      gap: 1rem;
      // align-items: center;
    }
    .left a,
    .right a {
      display: grid;
      grid-auto-flow: column;
      gap: 0.25rem;
      align-items: baseline;
      width: max-content;
      width: -moz-available;
      &:hover {
        color: var(--text-color);
        cursor: pointer;
      }
    }
  }

  .version {
    opacity: 0;
  }
}

#bottom.focus {
  .keyTips {
    opacity: 0 !important;
  }
  a {
    opacity: 0 !important;
  }
  #commandLineMobileButton {
    opacity: 0 !important;
    pointer-events: none !important;
  }
}

==> ./monkeytype/src/js/theme-colors.js <==
// export let bg = "#323437";
// export let main = "#e2b714";
// export let caret = "#e2b714";
// export let sub = "#646669";
// export let text = "#d1d0c5";
// export let error = "#ca4754";
// export let errorExtra = "#7e2a33";
// export let colorfulError = "#ca4754";
// export let colorfulErrorExtra = "#7e2a33";

let colors = {
  bg: "#323437",
  main: "#e2b714",
  caret: "#e2b714",
  sub: "#646669",
  text: "#d1d0c5",
  error: "#ca4754",
  errorExtra: "#7e2a33",
  colorfulError: "#ca4754",
  colorfulErrorExtra: "#7e2a33",
};

export async function get(color) {
  let ret;

  if (color === undefined) {
    ret = colors;
  } else {
    ret = colors[color];
  }

  return ret;

  // return check();

  // async function run() {
  //   return new Promise(function (resolve, reject) {
  //     window.setTimeout(() => {
  //       update();
  //       if (color === undefined) {
  //         ret = colors;
  //       } else {
  //         ret = colors[color];
  //       }
  //       resolve(check());
  //     }, 250);
  //   });
  // }
  // async function check() {
  //   if (color === undefined) {
  //     if (ret.bg === "") {
  //       return await run();
  //     } else {
  //       return ret;
  //     }
  //   } else {
  //     if (ret === "") {
  //       return await run();
  //     } else {
  //       return ret;
  //     }
  //   }
  // }
}

export function reset() {
  colors = {
    bg: "",
    main: "",
    caret: "",
    sub: "",
    text: "",
    error: "",
    errorExtra: "",
    colorfulError: "",
    colorfulErrorExtra: "",
  };
}

export function update() {
  let st = getComputedStyle(document.body);
  colors.bg = st.getPropertyValue("--bg-color").replace(" ", "");
  colors.main = st.getPropertyValue("--main-color").replace(" ", "");
  colors.caret = st.getPropertyValue("--caret-color").replace(" ", "");
  colors.sub = st.getPropertyValue("--sub-color").replace(" ", "");
  colors.text = st.getPropertyValue("--text-color").replace(" ", "");
  colors.error = st.getPropertyValue("--error-color").replace(" ", "");
  colors.errorExtra = st
    .getPropertyValue("--error-extra-color")
    .replace(" ", "");
  colors.colorfulError = st
    .getPropertyValue("--colorful-error-color")
    .replace(" ", "");
  colors.colorfulErrorExtra = st
    .getPropertyValue("--colorful-error-extra-color")
    .replace(" ", "");
}

==> ./monkeytype/src/js/simple-popups.js <==
import * as Loader from "./loader";
import * as Notifications from "./notifications";
import * as AccountController from "./account-controller";
import * as DB from "./db";
import * as Settings from "./settings";
import axiosInstance from "./axios-instance";
import * as UpdateConfig from "./config";

export let list = {};
class SimplePopup {
  constructor(
    id,
    type,
    title,
    inputs = [],
    text = "",
    buttonText = "Confirm",
    execFn,
    beforeShowFn
  ) {
    this.parameters = [];
    this.id = id;
    this.type = type;
    this.execFn = execFn;
    this.title = title;
    this.inputs = inputs;
    this.text = text;
    this.wrapper = $("#simplePopupWrapper");
    this.element = $("#simplePopup");
    this.buttonText = buttonText;
    this.beforeShowFn = beforeShowFn;
  }
  reset() {
    this.element.html(`
    <div class="title"></div>
    <div class="inputs"></div>
    <div class="text"></div>
    <div class="button"></div>`);
  }

  init() {
    let el = this.element;
    el.find("input").val("");
    // if (el.attr("popupId") !== this.id) {
    this.reset();
    el.attr("popupId", this.id);
    el.find(".title").text(this.title);
    el.find(".text").text(this.text);

    this.initInputs();

    if (!this.buttonText) {
      el.find(".button").remove();
    } else {
      el.find(".button").text(this.buttonText);
    }

    // }
  }

  initInputs() {
    let el = this.element;
    if (this.inputs.length > 0) {
      if (this.type === "number") {
        this.inputs.forEach((input) => {
          el.find(".inputs").append(`
        <input type="number" min="1" val="${input.initVal}" placeholder="${
            input.placeholder
          }" class="${input.hidden ? "hidden" : ""}" ${
            input.hidden ? "" : "required"
          } autocomplete="off">
        `);
        });
      } else if (this.type === "text") {
        this.inputs.forEach((input) => {
          if (input.type) {
            el.find(".inputs").append(`
            <input type="${input.type}" val="${input.initVal}" placeholder="${
              input.placeholder
            }" class="${input.hidden ? "hidden" : ""}" ${
              input.hidden ? "" : "required"
            } autocomplete="off">
            `);
          } else {
            el.find(".inputs").append(`
            <input type="text" val="${input.initVal}" placeholder="${
              input.placeholder
            }" class="${input.hidden ? "hidden" : ""}" ${
              input.hidden ? "" : "required"
            } autocomplete="off">
            `);
          }
        });
      }
      el.find(".inputs").removeClass("hidden");
    } else {
      el.find(".inputs").addClass("hidden");
    }
  }

  exec() {
    let vals = [];
    $.each($("#simplePopup input"), (index, el) => {
      vals.push($(el).val());
    });
    this.execFn(...vals);
    this.hide();
  }

  show(parameters) {
    this.parameters = parameters;
    this.beforeShowFn();
    this.init();
    this.wrapper
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate({ opacity: 1 }, 125, () => {
        $($("#simplePopup").find("input")[0]).focus();
      });
  }

  hide() {
    this.wrapper
      .stop(true, true)
      .css("opacity", 1)
      .removeClass("hidden")
      .animate({ opacity: 0 }, 125, () => {
        this.wrapper.addClass("hidden");
      });
  }
}

export function hide() {
  $("#simplePopupWrapper")
    .stop(true, true)
    .css("opacity", 1)
    .removeClass("hidden")
    .animate({ opacity: 0 }, 125, () => {
      $("#simplePopupWrapper").addClass("hidden");
    });
}

$("#simplePopupWrapper").mousedown((e) => {
  if ($(e.target).attr("id") === "simplePopupWrapper") {
    $("#simplePopupWrapper")
      .stop(true, true)
      .css("opacity", 1)
      .removeClass("hidden")
      .animate({ opacity: 0 }, 125, () => {
        $("#simplePopupWrapper").addClass("hidden");
      });
  }
});

$(document).on("click", "#simplePopupWrapper .button", (e) => {
  let id = $("#simplePopup").attr("popupId");
  list[id].exec();
});

$(document).on("keyup", "#simplePopupWrapper input", (e) => {
  if (e.key === "Enter") {
    e.preventDefault();
    let id = $("#simplePopup").attr("popupId");
    list[id].exec();
  }
});

list.updateEmail = new SimplePopup(
  "updateEmail",
  "text",
  "Update Email",
  [
    {
      placeholder: "Password",
      type: "password",
      initVal: "",
    },
    {
      placeholder: "New email",
      initVal: "",
    },
    {
      placeholder: "Confirm new email",
      initVal: "",
    },
  ],
  "",
  "Update",
  async (password, email, emailConfirm) => {
    try {
      const user = firebase.auth().currentUser;
      if (email !== emailConfirm) {
        Notifications.add("Emails don't match", 0);
        return;
      }
      if (user.providerData[0].providerId === "password") {
        const credential = firebase.auth.EmailAuthProvider.credential(
          user.email,
          password
        );
        await user.reauthenticateWithCredential(credential);
      }
      Loader.show();
      let response;
      try {
        response = await axiosInstance.post("/user/updateEmail", {
          uid: user.uid,
          previousEmail: user.email,
          newEmail: email,
        });
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to update email: " + msg, -1);
        return;
      }
      Loader.hide();
      if (response.status !== 200) {
        Notifications.add(response.data.message);
        return;
      } else {
        Notifications.add("Email updated", 1);
      }
    } catch (e) {
      if (e.code == "auth/wrong-password") {
        Notifications.add("Incorrect password", -1);
      } else {
        Notifications.add("Something went wrong: " + e, -1);
      }
    }
  },
  () => {
    const user = firebase.auth().currentUser;
    if (!user.providerData.find((p) => p.providerId === "password")) {
      eval(`this.inputs = []`);
      eval(`this.buttonText = undefined`);
      eval(`this.text = "Password authentication is not enabled";`);
    }
  }
);

list.updateName = new SimplePopup(
  "updateName",
  "text",
  "Update Name",
  [
    {
      placeholder: "Password",
      type: "password",
      initVal: "",
    },
    {
      placeholder: "New name",
      type: "text",
      initVal: "",
    },
  ],
  "",
  "Update",
  async (pass, newName) => {
    try {
      const user = firebase.auth().currentUser;
      if (user.providerData[0].providerId === "password") {
        const credential = firebase.auth.EmailAuthProvider.credential(
          user.email,
          pass
        );
        await user.reauthenticateWithCredential(credential);
      } else if (user.providerData[0].providerId === "google.com") {
        await user.reauthenticateWithPopup(AccountController.gmailProvider);
      }
      Loader.show();

      let response;
      try {
        response = await axiosInstance.post("/user/checkName", {
          name: newName,
        });
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to check name: " + msg, -1);
        return;
      }
      Loader.hide();
      if (response.status !== 200) {
        Notifications.add(response.data.message);
        return;
      }
      try {
        response = await axiosInstance.post("/user/updateName", {
          name: newName,
        });
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to update name: " + msg, -1);
        return;
      }
      Loader.hide();
      if (response.status !== 200) {
        Notifications.add(response.data.message);
        return;
      } else {
        Notifications.add("Name updated", 1);
        DB.getSnapshot().name = newName;
        $("#menu .icon-button.account .text").text(newName);
      }
    } catch (e) {
      Loader.hide();
      if (e.code == "auth/wrong-password") {
        Notifications.add("Incorrect password", -1);
      } else {
        Notifications.add("Something went wrong: " + e, -1);
      }
    }
  },
  () => {
    const user = firebase.auth().currentUser;
    if (user.providerData[0].providerId === "google.com") {
      eval(`this.inputs[0].hidden = true`);
      eval(`this.buttonText = "Reauthenticate to update"`);
    }
  }
);

list.updatePassword = new SimplePopup(
  "updatePassword",
  "text",
  "Update Password",
  [
    {
      placeholder: "Password",
      type: "password",
      initVal: "",
    },
    {
      placeholder: "New password",
      type: "password",
      initVal: "",
    },
    {
      placeholder: "Confirm new password",
      type: "password",
      initVal: "",
    },
  ],
  "",
  "Update",
  async (previousPass, newPass, newPassConfirm) => {
    try {
      const user = firebase.auth().currentUser;
      const credential = firebase.auth.EmailAuthProvider.credential(
        user.email,
        previousPass
      );
      if (newPass !== newPassConfirm) {
        Notifications.add("New passwords don't match", 0);
        return;
      }
      Loader.show();
      await user.reauthenticateWithCredential(credential);
      await user.updatePassword(newPass);
      Loader.hide();
      Notifications.add("Password updated", 1);
    } catch (e) {
      Loader.hide();
      if (e.code == "auth/wrong-password") {
        Notifications.add("Incorrect password", -1);
      } else {
        Notifications.add("Something went wrong: " + e, -1);
      }
    }
  },
  () => {
    const user = firebase.auth().currentUser;
    if (!user.providerData.find((p) => p.providerId === "password")) {
      eval(`this.inputs = []`);
      eval(`this.buttonText = undefined`);
      eval(`this.text = "Password authentication is not enabled";`);
    }
  }
);

list.addPasswordAuth = new SimplePopup(
  "addPasswordAuth",
  "text",
  "Add Password Authentication",
  [
    {
      placeholder: "email",
      type: "email",
      initVal: "",
    },
    {
      placeholder: "confirm email",
      type: "email",
      initVal: "",
    },
    {
      placeholder: "new password",
      type: "password",
      initVal: "",
    },
    {
      placeholder: "confirm new password",
      type: "password",
      initVal: "",
    },
  ],
  "",
  "Add",
  async (email, emailConfirm, pass, passConfirm) => {
    if (email !== emailConfirm) {
      Notifications.add("Emails don't match", 0);
      return;
    }

    if (pass !== passConfirm) {
      Notifications.add("Passwords don't match", 0);
      return;
    }

    AccountController.addPasswordAuth(email, pass);
  },
  () => {}
);

list.deleteAccount = new SimplePopup(
  "deleteAccount",
  "text",
  "Delete Account",
  [
    {
      placeholder: "Password",
      type: "password",
      initVal: "",
    },
  ],
  "This is the last time you can change your mind. After pressing the button everything is gone.",
  "Delete",
  async (password) => {
    //
    try {
      const user = firebase.auth().currentUser;
      if (user.providerData[0].providerId === "password") {
        const credential = firebase.auth.EmailAuthProvider.credential(
          user.email,
          password
        );
        await user.reauthenticateWithCredential(credential);
      } else if (user.providerData[0].providerId === "google.com") {
        await user.reauthenticateWithPopup(AccountController.gmailProvider);
      }
      Loader.show();

      Notifications.add("Deleting stats...", 0);
      let response;
      try {
        response = await axiosInstance.post("/user/delete");
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to delete user stats: " + msg, -1);
        return;
      }
      if (response.status !== 200) {
        throw response.data.message;
      }

      Notifications.add("Deleting results...", 0);
      try {
        response = await axiosInstance.post("/results/deleteAll");
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to delete user results: " + msg, -1);
        return;
      }
      if (response.status !== 200) {
        throw response.data.message;
      }

      Notifications.add("Deleting login information...", 0);
      await firebase.auth().currentUser.delete();

      Notifications.add("Goodbye", 1, 5);

      setTimeout(() => {
        location.reload();
      }, 3000);
    } catch (e) {
      Loader.hide();
      if (e.code == "auth/wrong-password") {
        Notifications.add("Incorrect password", -1);
      } else {
        Notifications.add("Something went wrong: " + e, -1);
      }
    }
  },
  () => {
    const user = firebase.auth().currentUser;
    if (user.providerData[0].providerId === "google.com") {
      eval(`this.inputs = []`);
      eval(`this.buttonText = "Reauthenticate to delete"`);
    }
  }
);

list.clearTagPb = new SimplePopup(
  "clearTagPb",
  "text",
  "Clear Tag PB",
  [],
  `Are you sure you want to clear this tags PB?`,
  "Clear",
  () => {
    let tagid = eval("this.parameters[0]");
    Loader.show();
    axiosInstance
      .post("/user/tags/clearPb", {
        tagid: tagid,
      })
      .then((res) => {
        Loader.hide();
        if (res.data.resultCode === 1) {
          let tag = DB.getSnapshot().tags.filter((t) => t.id === tagid)[0];
          tag.pb = 0;
          $(
            `.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton`
          ).attr("aria-label", "No PB found");
          Notifications.add("Tag PB cleared.", 0);
        } else {
          Notifications.add("Something went wrong: " + res.data.message, -1);
        }
      })
      .catch((e) => {
        Loader.hide();
        if (e.code == "auth/wrong-password") {
          Notifications.add("Incorrect password", -1);
        } else {
          Notifications.add("Something went wrong: " + e, -1);
        }
      });
    // console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`);
  },
  () => {
    eval(
      "this.text = `Are you sure you want to clear PB for tag ${eval('this.parameters[1]')}?`"
    );
  }
);

list.applyCustomFont = new SimplePopup(
  "applyCustomFont",
  "text",
  "Custom font",
  [{ placeholder: "Font name", initVal: "" }],
  "Make sure you have the font installed on your computer before applying.",
  "Apply",
  (fontName) => {
    if (fontName === "") return;
    Settings.groups.fontFamily?.setValue(fontName.replace(/\s/g, "_"));
  },
  () => {}
);

list.resetPersonalBests = new SimplePopup(
  "resetPersonalBests",
  "text",
  "Reset Personal Bests",
  [
    {
      placeholder: "Password",
      type: "password",
      initVal: "",
    },
  ],
  "",
  "Reset",
  async (password) => {
    try {
      const user = firebase.auth().currentUser;
      if (user.providerData[0].providerId === "password") {
        const credential = firebase.auth.EmailAuthProvider.credential(
          user.email,
          password
        );
        await user.reauthenticateWithCredential(credential);
      } else if (user.providerData[0].providerId === "google.com") {
        await user.reauthenticateWithPopup(AccountController.gmailProvider);
      }
      Loader.show();

      let response;
      try {
        response = await axiosInstance.post("/user/clearPb");
      } catch (e) {
        Loader.hide();
        let msg = e?.response?.data?.message ?? e.message;
        Notifications.add("Failed to reset personal bests: " + msg, -1);
        return;
      }
      Loader.hide();
      if (response.status !== 200) {
        Notifications.add(response.data.message);
      } else {
        Notifications.add("Personal bests have been reset", 1);
        DB.getSnapshot().personalBests = {};
      }
    } catch (e) {
      Loader.hide();
      Notifications.add(e, -1);
    }
  },
  () => {
    const user = firebase.auth().currentUser;
    if (user.providerData[0].providerId === "google.com") {
      eval(`this.inputs = []`);
      eval(`this.buttonText = "Reauthenticate to reset"`);
    }
  }
);

list.resetSettings = new SimplePopup(
  "resetSettings",
  "text",
  "Reset Settings",
  [],
  "Are you sure you want to reset all your settings?",
  "Reset",
  () => {
    UpdateConfig.reset();
    // setTimeout(() => {
    //   location.reload();
    // }, 1000);
  },
  () => {}
);

list.unlinkDiscord = new SimplePopup(
  "unlinkDiscord",
  "text",
  "Unlink Discord",
  [],
  "Are you sure you want to unlink your Discord account?",
  "Unlink",
  async () => {
    Loader.show();
    let response;
    try {
      response = await axiosInstance.post("/user/discord/unlink", {});
    } catch (e) {
      Loader.hide();
      let msg = e?.response?.data?.message ?? e.message;
      Notifications.add("Failed to unlink Discord: " + msg, -1);
      return;
    }
    Loader.hide();
    if (response.status !== 200) {
      Notifications.add(response.data.message);
    } else {
      Notifications.add("Accounts unlinked", 1);
      DB.getSnapshot().discordId = undefined;
      Settings.updateDiscordSection();
    }
  },
  () => {}
);

==> ./monkeytype/src/js/settings/settings-group.js <==
import Config from "./config";

export default class SettingsGroup {
  constructor(
    configName,
    toggleFunction,
    setCallback = null,
    updateCallback = null
  ) {
    this.configName = configName;
    this.configValue = Config[configName];
    this.onOff = typeof this.configValue === "boolean";
    this.toggleFunction = toggleFunction;
    this.setCallback = setCallback;
    this.updateCallback = updateCallback;

    this.updateButton();

    $(document).on(
      "click",
      `.pageSettings .section.${this.configName} .button`,
      (e) => {
        let target = $(e.currentTarget);
        if (target.hasClass("disabled") || target.hasClass("no-auto-handle"))
          return;
        if (this.onOff) {
          if (target.hasClass("on")) {
            this.setValue(true);
          } else {
            this.setValue(false);
          }
          this.updateButton();
          if (this.setCallback !== null) this.setCallback();
        } else {
          const value = target.attr(configName);
          const params = target.attr("params");
          if (!value && !params) return;
          this.setValue(value, params);
        }
      }
    );
  }

  setValue(value, params = undefined) {
    if (params === undefined) {
      this.toggleFunction(value);
    } else {
      this.toggleFunction(value, ...params);
    }
    this.updateButton();
    if (this.setCallback !== null) this.setCallback();
  }

  updateButton() {
    this.configValue = Config[this.configName];
    $(`.pageSettings .section.${this.configName} .button`).removeClass(
      "active"
    );
    if (this.onOff) {
      const onOffString = this.configValue ? "on" : "off";
      $(
        `.pageSettings .section.${this.configName} .buttons .button.${onOffString}`
      ).addClass("active");
    } else {
      $(
        `.pageSettings .section.${this.configName} .button[${this.configName}='${this.configValue}']`
      ).addClass("active");
    }
    if (this.updateCallback !== null) this.updateCallback();
  }
}

==> ./monkeytype/src/js/settings/language-picker.js <==
import * as Misc from "./misc";
import Config, * as UpdateConfig from "./config";

export async function setActiveGroup(groupName, clicked = false) {
  let currentGroup;
  if (groupName === undefined) {
    currentGroup = await Misc.findCurrentGroup(Config.language);
  } else {
    let groups = await Misc.getLanguageGroups();
    groups.forEach((g) => {
      if (g.name === groupName) {
        currentGroup = g;
      }
    });
  }
  $(`.pageSettings .section.languageGroups .button`).removeClass("active");
  $(
    `.pageSettings .section.languageGroups .button[group='${currentGroup.name}']`
  ).addClass("active");

  let langEl = $(".pageSettings .section.language .buttons").empty();
  currentGroup.languages.forEach((language) => {
    langEl.append(
      `<div class="language button" language='${language}'>${language.replace(
        /_/g,
        " "
      )}</div>`
    );
  });

  if (clicked) {
    $($(`.pageSettings .section.language .buttons .button`)[0]).addClass(
      "active"
    );
    UpdateConfig.setLanguage(currentGroup.languages[0]);
  } else {
    $(
      `.pageSettings .section.language .buttons .button[language=${Config.language}]`
    ).addClass("active");
  }
}

==> ./monkeytype/src/js/settings/theme-picker.js <==
import Config, * as UpdateConfig from "./config";
import * as ThemeController from "./theme-controller";
import * as Misc from "./misc";
import * as Notifications from "./notifications";
import * as CommandlineLists from "./commandline-lists";
import * as ThemeColors from "./theme-colors";
import * as ChartController from "./chart-controller";

export function updateActiveButton() {
  let activeThemeName = Config.theme;
  if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) {
    activeThemeName = ThemeController.randomTheme;
  }
  $(`.pageSettings .section.themes .theme`).removeClass("active");
  $(`.pageSettings .section.themes .theme[theme=${activeThemeName}]`).addClass(
    "active"
  );
}

function updateColors(colorPicker, color, onlyStyle, noThemeUpdate = false) {
  if (onlyStyle) {
    let colorid = colorPicker.find("input[type=color]").attr("id");
    if (!noThemeUpdate)
      document.documentElement.style.setProperty(colorid, color);
    let pickerButton = colorPicker.find("label");
    pickerButton.val(color);
    pickerButton.attr("value", color);
    if (pickerButton.attr("for") !== "--bg-color")
      pickerButton.css("background-color", color);
    colorPicker.find("input[type=text]").val(color);
    colorPicker.find("input[type=color]").attr("value", color);
    return;
  }
  let colorREGEX = [
    {
      rule: /\b[0-9]{1,3},\s?[0-9]{1,3},\s?[0-9]{1,3}\s*\b/,
      start: "rgb(",
      end: ")",
    },
    {
      rule: /\b[A-Z, a-z, 0-9]{6}\b/,
      start: "#",
      end: "",
    },
    {
      rule: /\b[0-9]{1,3},\s?[0-9]{1,3}%,\s?[0-9]{1,3}%?\s*\b/,
      start: "hsl(",
      end: ")",
    },
  ];

  color = color.replace("°", "");

  for (let regex of colorREGEX) {
    if (color.match(regex.rule)) {
      color = regex.start + color + regex.end;
      break;
    }
  }

  $(".colorConverter").css("color", color);
  color = Misc.convertRGBtoHEX($(".colorConverter").css("color"));
  if (!color) {
    return;
  }

  let colorid = colorPicker.find("input[type=color]").attr("id");

  if (!noThemeUpdate)
    document.documentElement.style.setProperty(colorid, color);

  let pickerButton = colorPicker.find("label");

  pickerButton.val(color);
  pickerButton.attr("value", color);
  if (pickerButton.attr("for") !== "--bg-color")
    pickerButton.css("background-color", color);
  colorPicker.find("input[type=text]").val(color);
  colorPicker.find("input[type=color]").attr("value", color);
}

export function refreshButtons() {
  let favThemesEl = $(
    ".pageSettings .section.themes .favThemes.buttons"
  ).empty();
  let themesEl = $(".pageSettings .section.themes .allThemes.buttons").empty();

  let activeThemeName = Config.theme;
  if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) {
    activeThemeName = ThemeController.randomTheme;
  }

  Misc.getSortedThemesList().then((themes) => {
    //first show favourites
    if (Config.favThemes.length > 0) {
      favThemesEl.css({ paddingBottom: "1rem" });
      themes.forEach((theme) => {
        if (Config.favThemes.includes(theme.name)) {
          let activeTheme = activeThemeName === theme.name ? "active" : "";
          favThemesEl.append(
            `<div class="theme button ${activeTheme}" theme='${
              theme.name
            }' style="color:${theme.mainColor};background:${theme.bgColor}">
          <div class="activeIndicator"><i class="fas fa-circle"></i></div>
          <div class="text">${theme.name.replace(/_/g, " ")}</div>
          <div class="favButton active"><i class="fas fa-star"></i></div></div>`
          );
        }
      });
    } else {
      favThemesEl.css({ paddingBottom: "0" });
    }
    //then the rest
    themes.forEach((theme) => {
      if (!Config.favThemes.includes(theme.name)) {
        let activeTheme = activeThemeName === theme.name ? "active" : "";
        themesEl.append(
          `<div class="theme button ${activeTheme}" theme='${
            theme.name
          }' style="color:${theme.mainColor};background:${theme.bgColor}">
          <div class="activeIndicator"><i class="fas fa-circle"></i></div>
          <div class="text">${theme.name.replace(/_/g, " ")}</div>
          <div class="favButton"><i class="far fa-star"></i></div></div>`
        );
      }
    });
    updateActiveButton();
  });
}

export function setCustomInputs(noThemeUpdate) {
  $(
    ".pageSettings .section.themes .tabContainer .customTheme .colorPicker"
  ).each((n, index) => {
    let currentColor =
      Config.customThemeColors[
        ThemeController.colorVars.indexOf(
          $(index).find("input[type=color]").attr("id")
        )
      ];

    //todo check if needed
    // $(index).find("input[type=color]").val(currentColor);
    // $(index).find("input[type=color]").attr("value", currentColor);
    // $(index).find("input[type=text]").val(currentColor);
    updateColors($(index), currentColor, false, noThemeUpdate);
  });
}

function toggleFavourite(themename) {
  if (Config.favThemes.includes(themename)) {
    //already favourite, remove
    UpdateConfig.setFavThemes(
      Config.favThemes.filter((t) => {
        if (t !== themename) {
          return t;
        }
      })
    );
  } else {
    //add to favourites
    let newlist = Config.favThemes;
    newlist.push(themename);
    UpdateConfig.setFavThemes(newlist);
  }
  UpdateConfig.saveToLocalStorage();
  refreshButtons();
  // showFavouriteThemesAtTheTop();
  CommandlineLists.updateThemeCommands();
}

export function updateActiveTab() {
  $(".pageSettings .section.themes .tabs .button").removeClass("active");
  if (!Config.customTheme) {
    $(".pageSettings .section.themes .tabs .button[tab='preset']").addClass(
      "active"
    );

    // UI.swapElements(
    //   $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'),
    //   $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'),
    //   250
    // );
  } else {
    $(".pageSettings .section.themes .tabs .button[tab='custom']").addClass(
      "active"
    );

    // UI.swapElements(
    //   $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'),
    //   $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'),
    //   250
    // );
  }
}

$(".pageSettings .section.themes .tabs .button").click((e) => {
  $(".pageSettings .section.themes .tabs .button").removeClass("active");
  var $target = $(e.currentTarget);
  $target.addClass("active");
  setCustomInputs();
  if ($target.attr("tab") == "preset") {
    UpdateConfig.setCustomTheme(false);
    // ThemeController.set(Config.theme);
    // applyCustomThemeColors();
    // UI.swapElements(
    //   $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'),
    //   $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'),
    //   250
    // );
  } else {
    UpdateConfig.setCustomTheme(true);
    // ThemeController.set("custom");
    // applyCustomThemeColors();
    // UI.swapElements(
    //   $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'),
    //   $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'),
    //   250
    // );
  }
});

$(document).on(
  "click",
  ".pageSettings .section.themes .theme .favButton",
  (e) => {
    let theme = $(e.currentTarget).parents(".theme.button").attr("theme");
    toggleFavourite(theme);
  }
);

$(document).on("click", ".pageSettings .section.themes .theme.button", (e) => {
  let theme = $(e.currentTarget).attr("theme");
  if (!$(e.target).hasClass("favButton")) {
    UpdateConfig.setTheme(theme);
    // ThemePicker.refreshButtons();
    updateActiveButton();
  }
});

$(
  ".pageSettings .section.themes .tabContainer .customTheme input[type=color]"
).on("input", (e) => {
  // UpdateConfig.setCustomTheme(true, true);
  let $colorVar = $(e.currentTarget).attr("id");
  let $pickedColor = $(e.currentTarget).val();

  //todo check if needed
  //   document.documentElement.style.setProperty($colorVar, $pickedColor);
  //   $(".colorPicker #" + $colorVar).attr("value", $pickedColor);
  //   $(".colorPicker #" + $colorVar).val($pickedColor);
  //   $(".colorPicker #" + $colorVar + "-txt").val($pickedColor);
  // });

  // $(
  //   ".pageSettings .section.themes .tabContainer .customTheme input[type=text]"
  // ).on("input", (e) => {
  //   // UpdateConfig.setCustomTheme(true, true);
  //   let $colorVar = $(e.currentTarget).attr("id").replace("-txt", "");
  //   let $pickedColor = $(e.currentTarget).val();

  //   document.documentElement.style.setProperty($colorVar, $pickedColor);
  //   $(".colorPicker #" + $colorVar).attr("value", $pickedColor);
  //   $(".colorPicker #" + $colorVar).val($pickedColor);
  //   $(".colorPicker #" + $colorVar + "-txt").val($pickedColor);
  updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor, true);
});

$(
  ".pageSettings .section.themes .tabContainer .customTheme input[type=color]"
).on("change", (e) => {
  // UpdateConfig.setCustomTheme(true, true);
  let $colorVar = $(e.currentTarget).attr("id");
  let $pickedColor = $(e.currentTarget).val();

  //todo check if needed
  //   document.documentElement.style.setProperty($colorVar, $pickedColor);
  //   $(".colorPicker #" + $colorVar).attr("value", $pickedColor);
  //   $(".colorPicker #" + $colorVar).val($pickedColor);
  //   $(".colorPicker #" + $colorVar + "-txt").val($pickedColor);
  // });

  // $(
  //   ".pageSettings .section.themes .tabContainer .customTheme input[type=text]"
  // ).on("input", (e) => {
  //   // UpdateConfig.setCustomTheme(true, true);
  //   let $colorVar = $(e.currentTarget).attr("id").replace("-txt", "");
  //   let $pickedColor = $(e.currentTarget).val();

  //   document.documentElement.style.setProperty($colorVar, $pickedColor);
  //   $(".colorPicker #" + $colorVar).attr("value", $pickedColor);
  //   $(".colorPicker #" + $colorVar).val($pickedColor);
  //   $(".colorPicker #" + $colorVar + "-txt").val($pickedColor);
  updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor);
});

$(".pageSettings .section.themes .tabContainer .customTheme input[type=text]")
  .on("blur", (e) => {
    let $colorVar = $(e.currentTarget).attr("id");
    let $pickedColor = $(e.currentTarget).val();

    updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor);
  })
  .on("keypress", function (e) {
    if (e.which === 13) {
      $(this).attr("disabled", "disabled");
      let $colorVar = $(e.currentTarget).attr("id");
      let $pickedColor = $(e.currentTarget).val();

      updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor);
      $(this).removeAttr("disabled");
    }
  });

$(".pageSettings .saveCustomThemeButton").click((e) => {
  let save = [];
  $.each(
    $(".pageSettings .section.customTheme [type='color']"),
    (index, element) => {
      save.push($(element).attr("value"));
    }
  );
  UpdateConfig.setCustomThemeColors(save);
  ThemeController.set("custom");
  Notifications.add("Custom theme colors saved", 1);
});

$(".pageSettings #loadCustomColorsFromPreset").click((e) => {
  // previewTheme(Config.theme);
  $("#currentTheme").attr("href", `themes/${Config.theme}.css`);

  ThemeController.colorVars.forEach((e) => {
    document.documentElement.style.setProperty(e, "");
  });

  setTimeout(async () => {
    ChartController.updateAllChartColors();

    let themecolors = await ThemeColors.get();

    ThemeController.colorVars.forEach((colorName) => {
      let color;
      if (colorName === "--bg-color") {
        color = themecolors.bg;
      } else if (colorName === "--main-color") {
        color = themecolors.main;
      } else if (colorName === "--sub-color") {
        color = themecolors.sub;
      } else if (colorName === "--caret-color") {
        color = themecolors.caret;
      } else if (colorName === "--text-color") {
        color = themecolors.text;
      } else if (colorName === "--error-color") {
        color = themecolors.error;
      } else if (colorName === "--error-extra-color") {
        color = themecolors.errorExtra;
      } else if (colorName === "--colorful-error-color") {
        color = themecolors.colorfulError;
      } else if (colorName === "--colorful-error-extra-color") {
        color = themecolors.colorfulErrorExtra;
      }

      updateColors($(".colorPicker #" + colorName).parent(), color);
    });
  }, 250);
});

==> ./monkeytype/src/js/challenge-controller.js <==
import * as Misc from "./misc";
import * as Notifications from "./notifications";
import * as ManualRestart from "./manual-restart-tracker";
import * as CustomText from "./custom-text";
import * as TestLogic from "./test-logic";
import * as Funbox from "./funbox";
import Config, * as UpdateConfig from "./config";
import * as UI from "./ui";
import * as TestUI from "./test-ui";

export let active = null;
let challengeLoading = false;

export function clearActive() {
  if (active && !challengeLoading && !TestUI.testRestarting) {
    Notifications.add("Challenge cleared", 0);
    active = null;
  }
}

export function verify(result) {
  try {
    if (active) {
      let afk = (result.afkDuration / result.testDuration) * 100;

      if (afk > 10) {
        Notifications.add(`Challenge failed: AFK time is greater than 10%`, 0);
        return null;
      }

      if (!active.requirements) {
        Notifications.add(`${active.display} challenge passed!`, 1);
        return active.name;
      } else {
        let requirementsMet = true;
        let failReasons = [];
        for (let requirementType in active.requirements) {
          if (requirementsMet == false) return;
          let requirementValue = active.requirements[requirementType];
          if (requirementType == "wpm") {
            let wpmMode = Object.keys(requirementValue)[0];
            if (wpmMode == "exact") {
              if (Math.round(result.wpm) != requirementValue.exact) {
                requirementsMet = false;
                failReasons.push(`WPM not ${requirementValue.exact}`);
              }
            } else if (wpmMode == "min") {
              if (result.wpm < requirementValue.min) {
                requirementsMet = false;
                failReasons.push(`WPM below ${requirementValue.min}`);
              }
            }
          } else if (requirementType == "acc") {
            let accMode = Object.keys(requirementValue)[0];
            if (accMode == "exact") {
              if (result.acc != requirementValue.exact) {
                requirementsMet = false;
                failReasons.push(`Accuracy not ${requirementValue.exact}`);
              }
            } else if (accMode == "min") {
              if (result.acc < requirementValue.min) {
                requirementsMet = false;
                failReasons.push(`Accuracy below ${requirementValue.min}`);
              }
            }
          } else if (requirementType == "afk") {
            let afkMode = Object.keys(requirementValue)[0];
            if (afkMode == "max") {
              if (Math.round(afk) > requirementValue.max) {
                requirementsMet = false;
                failReasons.push(
                  `AFK percentage above ${requirementValue.max}`
                );
              }
            }
          } else if (requirementType == "time") {
            let timeMode = Object.keys(requirementValue)[0];
            if (timeMode == "min") {
              if (Math.round(result.testDuration) < requirementValue.min) {
                requirementsMet = false;
                failReasons.push(`Test time below ${requirementValue.min}`);
              }
            }
          } else if (requirementType == "funbox") {
            let funboxMode = requirementValue;
            if (funboxMode != result.funbox) {
              requirementsMet = false;
              failReasons.push(`${funboxMode} funbox not active`);
            }
          } else if (requirementType == "raw") {
            let rawMode = Object.keys(requirementValue)[0];
            if (rawMode == "exact") {
              if (Math.round(result.rawWpm) != requirementValue.exact) {
                requirementsMet = false;
                failReasons.push(`Raw WPM not ${requirementValue.exact}`);
              }
            }
          } else if (requirementType == "con") {
            let conMode = Object.keys(requirementValue)[0];
            if (conMode == "exact") {
              if (Math.round(result.consistency) != requirementValue.exact) {
                requirementsMet = false;
                failReasons.push(`Consistency not ${requirementValue.exact}`);
              }
            }
          } else if (requirementType == "config") {
            for (let configKey in requirementValue) {
              let configValue = requirementValue[configKey];
              if (Config[configKey] != configValue) {
                requirementsMet = false;
                failReasons.push(`${configKey} not set to ${configValue}`);
              }
            }
          }
        }
        if (requirementsMet) {
          if (active.autoRole) {
            Notifications.add(
              "You will receive a role shortly. Please don't post a screenshot in challenge submissions.",
              1,
              5
            );
          }
          Notifications.add(`${active.display} challenge passed!`, 1);
          return active.name;
        } else {
          Notifications.add(
            `${active.display} challenge failed: ${failReasons.join(", ")}`,
            0
          );
          return null;
        }
      }
    } else {
      return null;
    }
  } catch (e) {
    console.error(e);
    Notifications.add(
      `Something went wrong when verifying challenge: ${e.message}`,
      0
    );
    return null;
  }
}

export async function setup(challengeName) {
  challengeLoading = true;
  if (UI.getActivePage() !== "pageTest") {
    UI.changePage("", true);
  }

  let list = await Misc.getChallengeList();
  let challenge = list.filter((c) => c.name === challengeName)[0];
  let notitext;
  try {
    if (challenge === undefined) {
      Notifications.add("Challenge not found", 0);
      ManualRestart.set();
      TestLogic.restart(false, true);
      setTimeout(() => {
        $("#top .config").removeClass("hidden");
        $(".page.pageTest").removeClass("hidden");
      }, 250);
      return;
    }
    if (challenge.type === "customTime") {
      UpdateConfig.setTimeConfig(challenge.parameters[0], true);
      UpdateConfig.setMode("time", true);
      UpdateConfig.setDifficulty("normal", true);
      if (challenge.name === "englishMaster") {
        UpdateConfig.setLanguage("english_10k", true);
        UpdateConfig.setNumbers(true, true);
        UpdateConfig.setPunctuation(true, true);
      }
    } else if (challenge.type === "customWords") {
      UpdateConfig.setWordCount(challenge.parameters[0], true);
      UpdateConfig.setMode("words", true);
      UpdateConfig.setDifficulty("normal", true);
    } else if (challenge.type === "customText") {
      CustomText.setText(challenge.parameters[0].split(" "));
      CustomText.setIsWordRandom(challenge.parameters[1]);
      CustomText.setWord(parseInt(challenge.parameters[2]));
      UpdateConfig.setMode("custom", true);
      UpdateConfig.setDifficulty("normal", true);
    } else if (challenge.type === "script") {
      let scriptdata = await fetch("/challenges/" + challenge.parameters[0]);
      scriptdata = await scriptdata.text();
      let text = scriptdata.trim();
      text = text.replace(/[\n\rt ]/gm, " ");
      text = text.replace(/ +/gm, " ");
      CustomText.setText(text.split(" "));
      CustomText.setIsWordRandom(false);
      UpdateConfig.setMode("custom", true);
      UpdateConfig.setDifficulty("normal", true);
      if (challenge.parameters[1] != null) {
        UpdateConfig.setTheme(challenge.parameters[1]);
      }
      if (challenge.parameters[2] != null) {
        Funbox.activate(challenge.parameters[2]);
      }
    } else if (challenge.type === "accuracy") {
      UpdateConfig.setTimeConfig(0, true);
      UpdateConfig.setMode("time", true);
      UpdateConfig.setDifficulty("master", true);
    } else if (challenge.type === "funbox") {
      UpdateConfig.setFunbox(challenge.parameters[0], true);
      UpdateConfig.setDifficulty("normal", true);
      if (challenge.parameters[1] === "words") {
        UpdateConfig.setWordCount(challenge.parameters[2], true);
      } else if (challenge.parameters[1] === "time") {
        UpdateConfig.setTimeConfig(challenge.parameters[2], true);
      }
      UpdateConfig.setMode(challenge.parameters[1], true);
      if (challenge.parameters[3] !== undefined) {
        UpdateConfig.setDifficulty(challenge.parameters[3], true);
      }
    } else if (challenge.type === "special") {
      if (challenge.name === "semimak") {
        // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout?
        UpdateConfig.setMode("time", true);
        UpdateConfig.setTimeConfig(120, true);
        UpdateConfig.setLanguage("english_10k", true);
        UpdateConfig.setPunctuation(true, true);
        UpdateConfig.setStopOnError("word", true);
        UpdateConfig.setLayout("semimak", true);
        UpdateConfig.setKeymapLayout("overrideSync", true);
        UpdateConfig.setKeymapMode("static", true);
      }
    }
    ManualRestart.set();
    TestLogic.restart(false, true);
    notitext = challenge.message;
    $("#top .config").removeClass("hidden");
    $(".page.pageTest").removeClass("hidden");

    if (notitext === undefined) {
      Notifications.add(`Challenge '${challenge.display}' loaded.`, 0);
    } else {
      Notifications.add("Challenge loaded. " + notitext, 0);
    }
    active = challenge;
    challengeLoading = false;
  } catch (e) {
    Notifications.add("Something went wrong: " + e, -1);
  }
}

==> ./monkeytype/src/js/ui.js <==
import Config, * as UpdateConfig from "./config";
import * as Notifications from "./notifications";
import * as Caret from "./caret";
import * as TestLogic from "./test-logic";
import * as CustomText from "./custom-text";
import * as CommandlineLists from "./commandline-lists";
import * as Commandline from "./commandline";
import * as TestUI from "./test-ui";
import * as TestConfig from "./test-config";
import * as SignOutButton from "./sign-out-button";
import * as TestStats from "./test-stats";
import * as ManualRestart from "./manual-restart-tracker";
import * as Settings from "./settings";
import * as Account from "./account";
import * as Leaderboards from "./leaderboards";
import * as Funbox from "./funbox";
import * as About from "./about-page";

export let pageTransition = true;
let activePage = "pageLoading";

export function getActivePage() {
  return activePage;
}

export function setActivePage(active) {
  activePage = active;
}

export function setPageTransition(val) {
  pageTransition = val;
}

export function updateKeytips() {
  if (Config.swapEscAndTab) {
    $(".pageSettings .tip").html(`
    tip: You can also change all these settings quickly using the
    command line (
    <key>tab</key>
    )`);
    $("#bottom .keyTips").html(`
    <key>esc</key> - restart test<br>
      <key>tab</key> - command line`);
  } else {
    $(".pageSettings .tip").html(`
    tip: You can also change all these settings quickly using the
    command line (
    <key>esc</key>
    )`);
    $("#bottom .keyTips").html(`
    <key>tab</key> - restart test<br>
      <key>esc</key> or <key>ctrl/cmd</key>+<key>shift</key>+<key>p</key> - command line`);
  }
}

export function swapElements(
  el1,
  el2,
  totalDuration,
  callback = function () {
    return;
  },
  middleCallback = function () {
    return;
  }
) {
  if (
    (el1.hasClass("hidden") && !el2.hasClass("hidden")) ||
    (!el1.hasClass("hidden") && el2.hasClass("hidden"))
  ) {
    //one of them is hidden and the other is visible
    if (el1.hasClass("hidden")) {
      callback();
      return false;
    }
    $(el1)
      .removeClass("hidden")
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        totalDuration / 2,
        () => {
          middleCallback();
          $(el1).addClass("hidden");
          $(el2)
            .removeClass("hidden")
            .css("opacity", 0)
            .animate(
              {
                opacity: 1,
              },
              totalDuration / 2,
              () => {
                callback();
              }
            );
        }
      );
  } else if (el1.hasClass("hidden") && el2.hasClass("hidden")) {
    //both are hidden, only fade in the second
    $(el2)
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: 1,
        },
        totalDuration,
        () => {
          callback();
        }
      );
  } else {
    callback();
  }
}

export function changePage(page, norestart = false) {
  if (pageTransition) {
    console.log(`change page ${page} stopped`);
    return;
  }

  if (page == undefined) {
    //use window loacation
    let pages = {
      "/": "test",
      "/login": "login",
      "/settings": "settings",
      "/about": "about",
      "/account": "account",
    };
    let path = pages[window.location.pathname];
    if (!path) {
      path = "test";
    }
    page = path;
  }

  console.log(`change page ${page}`);
  let activePageElement = $(".page.active");
  let check = activePage + "";
  setTimeout(() => {
    if (check === "pageAccount" && page !== "account") {
      Account.reset();
    } else if (check === "pageSettings" && page !== "settings") {
      Settings.reset();
    } else if (check === "pageAbout" && page !== "about") {
      About.reset();
    }
  }, 250);

  activePage = undefined;
  $(".page").removeClass("active");
  $("#wordsInput").focusout();
  if (page == "test" || page == "") {
    setPageTransition(true);
    swapElements(
      activePageElement,
      $(".page.pageTest"),
      250,
      () => {
        setPageTransition(false);
        TestUI.focusWords();
        $(".page.pageTest").addClass("active");
        activePage = "pageTest";
        history.pushState("/", null, "/");
      },
      () => {
        TestConfig.show();
      }
    );
    SignOutButton.hide();
    // restartCount = 0;
    // incompleteTestSeconds = 0;
    TestStats.resetIncomplete();
    ManualRestart.set();
    if (!norestart) TestLogic.restart();
    Funbox.activate(Config.funbox);
  } else if (page == "about") {
    setPageTransition(true);
    TestLogic.restart();
    swapElements(activePageElement, $(".page.pageAbout"), 250, () => {
      setPageTransition(false);
      history.pushState("about", null, "about");
      $(".page.pageAbout").addClass("active");
      activePage = "pageAbout";
    });
    About.fill();
    Funbox.activate("none");
    TestConfig.hide();
    SignOutButton.hide();
  } else if (page == "settings") {
    setPageTransition(true);
    TestLogic.restart();
    swapElements(activePageElement, $(".page.pageSettings"), 250, () => {
      setPageTransition(false);
      history.pushState("settings", null, "settings");
      $(".page.pageSettings").addClass("active");
      activePage = "pageSettings";
    });
    Funbox.activate("none");
    Settings.fillSettingsPage().then(() => {
      Settings.update();
    });
    // Settings.update();
    TestConfig.hide();
    SignOutButton.hide();
  } else if (page == "account") {
    if (!firebase.auth().currentUser) {
      console.log(
        `current user is ${firebase.auth().currentUser}, going back to login`
      );
      changePage("login");
    } else {
      setPageTransition(true);
      TestLogic.restart();
      swapElements(activePageElement, $(".page.pageAccount"), 250, () => {
        setPageTransition(false);
        history.pushState("account", null, "account");
        $(".page.pageAccount").addClass("active");
        activePage = "pageAccount";
      });
      Funbox.activate("none");
      Account.update();
      TestConfig.hide();
    }
  } else if (page == "login") {
    if (firebase.auth().currentUser != null) {
      changePage("account");
    } else {
      setPageTransition(true);
      TestLogic.restart();
      swapElements(activePageElement, $(".page.pageLogin"), 250, () => {
        setPageTransition(false);
        history.pushState("login", null, "login");
        $(".page.pageLogin").addClass("active");
        activePage = "pageLogin";
      });
      Funbox.activate("none");
      TestConfig.hide();
      SignOutButton.hide();
    }
  }
}

//checking if the project is the development site
/*
if (firebase.app().options.projectId === "monkey-type-dev-67af4") {
  $("#top .logo .bottom").text("monkey-dev");
  $("head title").text("Monkey Dev");
  $("body").append(
    `<div class="devIndicator tr">DEV</div><div class="devIndicator bl">DEV</div>`
  );
}
*/

if (window.location.hostname === "localhost") {
  window.onerror = function (error) {
    Notifications.add(error, -1);
  };
  $("#top .logo .top").text("localhost");
  $("head title").text($("head title").text() + " (localhost)");
  //firebase.functions().useFunctionsEmulator("http://localhost:5001");
  $("body").append(
    `<div class="devIndicator tl">local</div><div class="devIndicator br">local</div>`
  );
  $(".pageSettings .discordIntegration .buttons a").attr(
    "href",
    "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fverify&response_type=token&scope=identify"
  );
}

//stop space scrolling
window.addEventListener("keydown", function (e) {
  if (e.keyCode == 32 && e.target == document.body) {
    e.preventDefault();
  }
});

$(document).on("click", "#bottom .leftright .right .current-theme", (e) => {
  if (e.shiftKey) {
    UpdateConfig.toggleCustomTheme();
  } else {
    // if (Config.customTheme) {
    //   toggleCustomTheme();
    // }
    CommandlineLists.pushCurrent(CommandlineLists.themeCommands);
    Commandline.show();
  }
});

$(document.body).on("click", ".pageAbout .aboutEnableAds", () => {
  CommandlineLists.pushCurrent(CommandlineLists.commandsEnableAds);
  Commandline.show();
});

window.addEventListener("beforeunload", (event) => {
  // Cancel the event as stated by the standard.
  if (
    (Config.mode === "words" && Config.words < 1000) ||
    (Config.mode === "time" && Config.time < 3600) ||
    Config.mode === "quote" ||
    (Config.mode === "custom" &&
      CustomText.isWordRandom &&
      CustomText.word < 1000) ||
    (Config.mode === "custom" &&
      CustomText.isTimeRandom &&
      CustomText.time < 1000) ||
    (Config.mode === "custom" &&
      !CustomText.isWordRandom &&
      CustomText.text.length < 1000)
  ) {
    //ignore
  } else {
    if (TestLogic.active) {
      event.preventDefault();
      // Chrome requires returnValue to be set.
      event.returnValue = "";
    }
  }
});

$(window).resize(() => {
  Caret.updatePosition();
});

$(document).on("click", "#top .logo", (e) => {
  changePage("test");
});

$(document).on("click", "#top #menu .icon-button", (e) => {
  if ($(e.currentTarget).hasClass("leaderboards")) {
    Leaderboards.show();
  } else {
    const href = $(e.currentTarget).attr("href");
    ManualRestart.set();
    changePage(href.replace("/", ""));
  }
  return false;
});

==> ./monkeytype/src/js/axios-instance.js <==
import axios from "axios";

let apiPath = "";

let baseURL;
if (window.location.hostname === "localhost") {
  baseURL = "http://localhost:5005" + apiPath;
} else {
  baseURL = "https://api.monkeytype.com" + apiPath;
}

const axiosInstance = axios.create({
  baseURL: baseURL,
  timeout: 10000,
});

// Request interceptor for API calls
axiosInstance.interceptors.request.use(
  async (config) => {
    let idToken;
    if (firebase.auth().currentUser != null) {
      idToken = await firebase.auth().currentUser.getIdToken();
    } else {
      idToken = null;
    }
    if (idToken) {
      config.headers = {
        Authorization: `Bearer ${idToken}`,
        Accept: "application/json",
        "Content-Type": "application/json",
      };
    } else {
      config.headers = {
        Accept: "application/json",
        "Content-Type": "application/json",
      };
    }
    return config;
  },
  (error) => {
    Promise.reject(error);
  }
);

axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    // whatever you want to do with the error
    // console.log('interctepted');
    // if(error.response.data.message){
    //   Notifications.add(`${error.response.data.message}`);
    // }else{
    //   Notifications.add(`${error.response.status} ${error.response.statusText}`);
    // }
    // return error.response;
    throw error;
  }
);

export default axiosInstance;

==> ./monkeytype/src/js/commandline.js <==
import * as Leaderboards from "./leaderboards";
import * as ThemeController from "./theme-controller";
import Config, * as UpdateConfig from "./config";
import * as Focus from "./focus";
import * as CommandlineLists from "./commandline-lists";
import * as TestUI from "./test-ui";
import * as PractiseWords from "./practise-words";
import * as SimplePopups from "./simple-popups";
import * as CustomWordAmountPopup from "./custom-word-amount-popup";
import * as CustomTestDurationPopup from "./custom-test-duration-popup";
import * as CustomTextPopup from "./custom-text-popup";
import * as QuoteSearchPopupWrapper from "./quote-search-popup";

let commandLineMouseMode = false;

function showInput(command, placeholder, defaultValue = "") {
  $("#commandLineWrapper").removeClass("hidden");
  $("#commandLine").addClass("hidden");
  $("#commandInput").removeClass("hidden");
  $("#commandInput input").attr("placeholder", placeholder);
  $("#commandInput input").val(defaultValue);
  $("#commandInput input").focus();
  $("#commandInput input").attr("command", "");
  $("#commandInput input").attr("command", command);
  if (defaultValue != "") {
    $("#commandInput input").select();
  }
}

export function isSingleListCommandLineActive() {
  return $("#commandLine").hasClass("allCommands");
}

function showFound() {
  $("#commandLine .suggestions").empty();
  let commandsHTML = "";
  let list = CommandlineLists.current[CommandlineLists.current.length - 1];
  $.each(list.list, (index, obj) => {
    if (obj.found && (obj.available !== undefined ? obj.available() : true)) {
      let icon = obj.icon ?? "fa-chevron-right";
      let faIcon = /^fa-/g.test(icon);
      if (!faIcon) {
        icon = `<div class="textIcon">${icon}</div>`;
      } else {
        icon = `<i class="fas fa-fw ${icon}"></i>`;
      }
      if (list.configKey) {
        if (
          (obj.configValueMode &&
            obj.configValueMode === "include" &&
            Config[list.configKey].includes(obj.configValue)) ||
          Config[list.configKey] === obj.configValue
        ) {
          icon = `<i class="fas fa-fw fa-check"></i>`;
        } else {
          icon = `<i class="fas fa-fw"></i>`;
        }
      }
      let iconHTML = `<div class="icon">${icon}</div>`;
      if (obj.noIcon && !isSingleListCommandLineActive()) {
        iconHTML = "";
      }
      commandsHTML += `<div class="entry" command="${obj.id}">${iconHTML}<div>${obj.display}</div></div>`;
    }
  });
  $("#commandLine .suggestions").html(commandsHTML);
  if ($("#commandLine .suggestions .entry").length == 0) {
    $("#commandLine .separator").css({ height: 0, margin: 0 });
  } else {
    $("#commandLine .separator").css({
      height: "1px",
      "margin-bottom": ".5rem",
    });
  }
  let entries = $("#commandLine .suggestions .entry");
  if (entries.length > 0) {
    $(entries[0]).addClass("activeKeyboard");
    try {
      $.each(list.list, (index, obj) => {
        if (obj.found) {
          if (
            (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") &&
            !ThemeController.randomTheme
          )
            ThemeController.clearPreview();
          if (!/font/gi.test(obj.id))
            UpdateConfig.previewFontFamily(Config.fontFamily);
          obj.hover();
          return false;
        }
      });
    } catch (e) {}
  }
  $("#commandLine .listTitle").remove();
}

function updateSuggested() {
  let inputVal = $("#commandLine input")
    .val()
    .toLowerCase()
    .split(" ")
    .filter((s, i) => s || i == 0); //remove empty entries after first
  let list = CommandlineLists.current[CommandlineLists.current.length - 1];
  if (
    inputVal[0] === "" &&
    Config.singleListCommandLine === "on" &&
    CommandlineLists.current.length === 1
  ) {
    $.each(list.list, (index, obj) => {
      obj.found = false;
    });
    showFound();
    return;
  }
  //ignore the preceeding ">"s in the command line input
  if (inputVal[0] && inputVal[0][0] == ">")
    inputVal[0] = inputVal[0].replace(/^>+/, "");
  if (inputVal[0] == "" && inputVal.length == 1) {
    $.each(list.list, (index, obj) => {
      if (obj.visible !== false) obj.found = true;
    });
  } else {
    $.each(list.list, (index, obj) => {
      let foundcount = 0;
      $.each(inputVal, (index2, obj2) => {
        if (obj2 == "") return;
        let escaped = obj2.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
        let re = new RegExp("\\b" + escaped, "g");
        let res = obj.display.toLowerCase().match(re);
        let res2 =
          obj.alias !== undefined ? obj.alias.toLowerCase().match(re) : null;
        if (
          (res != null && res.length > 0) ||
          (res2 != null && res2.length > 0)
        ) {
          foundcount++;
        } else {
          foundcount--;
        }
      });
      if (foundcount > inputVal.length - 1) {
        obj.found = true;
      } else {
        obj.found = false;
      }
    });
  }
  showFound();
}

export let show = () => {
  if (!$(".page.pageLoading").hasClass("hidden")) return;
  Focus.set(false);
  $("#commandLine").removeClass("hidden");
  $("#commandInput").addClass("hidden");
  if ($("#commandLineWrapper").hasClass("hidden")) {
    $("#commandLineWrapper")
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate(
        {
          opacity: 1,
        },
        100
      );
  }
  $("#commandLine input").val("");
  updateSuggested();
  $("#commandLine input").focus();
};

function hide() {
  UpdateConfig.previewFontFamily(Config.fontFamily);
  // applyCustomThemeColors();
  if (!ThemeController.randomTheme) {
    ThemeController.clearPreview();
  }
  $("#commandLineWrapper")
    .stop(true, true)
    .css("opacity", 1)
    .animate(
      {
        opacity: 0,
      },
      100,
      () => {
        $("#commandLineWrapper").addClass("hidden");
        $("#commandLine").removeClass("allCommands");
        TestUI.focusWords();
      }
    );
  TestUI.focusWords();
}

function trigger(command) {
  let subgroup = false;
  let input = false;
  let list = CommandlineLists.current[CommandlineLists.current.length - 1];
  let sticky = false;
  $.each(list.list, (i, obj) => {
    if (obj.id == command) {
      if (obj.input) {
        input = true;
        let escaped = obj.display.split("</i>")[1] ?? obj.display;
        showInput(obj.id, escaped, obj.defaultValue);
      } else if (obj.subgroup) {
        subgroup = true;
        if (obj.beforeSubgroup) {
          obj.beforeSubgroup();
        }
        CommandlineLists.current.push(obj.subgroup);
        show();
      } else {
        obj.exec();
        if (obj.sticky === true) {
          sticky = true;
        }
      }
    }
  });
  if (!subgroup && !input && !sticky) {
    try {
      firebase.analytics().logEvent("usedCommandLine", {
        command: command,
      });
    } catch (e) {
      console.log("Analytics unavailable");
    }
    hide();
  }
}

function addChildCommands(
  unifiedCommands,
  commandItem,
  parentCommandDisplay = "",
  parentCommand = ""
) {
  let commandItemDisplay = commandItem.display.replace(/\s?\.\.\.$/g, "");
  let icon = `<i class="fas fa-fw"></i>`;
  if (
    commandItem.configValue !== undefined &&
    Config[parentCommand.configKey] === commandItem.configValue
  ) {
    icon = `<i class="fas fa-fw fa-check"></i>`;
  }
  if (commandItem.noIcon) {
    icon = "";
  }

  if (parentCommandDisplay)
    commandItemDisplay =
      parentCommandDisplay + " > " + icon + commandItemDisplay;
  if (commandItem.subgroup) {
    if (commandItem.beforeSubgroup) commandItem.beforeSubgroup();
    try {
      commandItem.subgroup.list.forEach((cmd) => {
        commandItem.configKey = commandItem.subgroup.configKey;
        addChildCommands(unifiedCommands, cmd, commandItemDisplay, commandItem);
      });
      // commandItem.exec();
      // const currentCommandsIndex = CommandlineLists.current.length - 1;
      // CommandlineLists.current[currentCommandsIndex].list.forEach((cmd) => {
      //   if (cmd.alias === undefined) cmd.alias = commandItem.alias;
      //   addChildCommands(unifiedCommands, cmd, commandItemDisplay);
      // });
      // CommandlineLists.current.pop();
    } catch (e) {}
  } else {
    let tempCommandItem = { ...commandItem };
    tempCommandItem.icon = parentCommand.icon;
    if (parentCommandDisplay) tempCommandItem.display = commandItemDisplay;
    unifiedCommands.push(tempCommandItem);
  }
}

function generateSingleListOfCommands() {
  const allCommands = [];
  const oldShowCommandLine = show;
  show = () => {};
  CommandlineLists.defaultCommands.list.forEach((c) =>
    addChildCommands(allCommands, c)
  );
  show = oldShowCommandLine;
  return {
    title: "All Commands",
    list: allCommands,
  };
}

function useSingleListCommandLine(sshow = true) {
  let allCommands = generateSingleListOfCommands();
  // if (Config.singleListCommandLine == "manual") {
  // CommandlineLists.pushCurrent(allCommands);
  // } else if (Config.singleListCommandLine == "on") {
  CommandlineLists.setCurrent([allCommands]);
  // }
  if (Config.singleListCommandLine != "off")
    $("#commandLine").addClass("allCommands");
  if (sshow) show();
}

function restoreOldCommandLine(sshow = true) {
  if (isSingleListCommandLineActive()) {
    $("#commandLine").removeClass("allCommands");
    CommandlineLists.setCurrent(
      CommandlineLists.current.filter((l) => l.title != "All Commands")
    );
    if (CommandlineLists.current.length < 1)
      CommandlineLists.setCurrent([CommandlineLists.defaultCommands]);
  }
  if (sshow) show();
}

$("#commandLine input").keyup((e) => {
  commandLineMouseMode = false;
  $("#commandLineWrapper #commandLine .suggestions .entry").removeClass(
    "activeMouse"
  );
  if (
    e.key === "ArrowUp" ||
    e.key === "ArrowDown" ||
    e.key === "Enter" ||
    e.key === "Tab" ||
    e.code == "AltLeft" ||
    (e.key.length > 1 && e.key !== "Backspace" && e.key !== "Delete")
  )
    return;
  updateSuggested();
});

$(document).ready((e) => {
  $(document).keydown((event) => {
    // opens command line if escape, ctrl/cmd + shift + p, or tab is pressed if the setting swapEscAndTab is enabled
    if (
      event.key === "Escape" ||
      (event.key &&
        event.key.toLowerCase() === "p" &&
        (event.metaKey || event.ctrlKey) &&
        event.shiftKey) ||
      (event.key === "Tab" && Config.swapEscAndTab)
    ) {
      event.preventDefault();
      if (!$("#leaderboardsWrapper").hasClass("hidden")) {
        //maybe add more condition for closing other dialogs in the future as well
        event.preventDefault();
        Leaderboards.hide();
      } else if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        PractiseWords.hide();
      } else if (!$("#simplePopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        SimplePopups.hide();
      } else if (!$("#customWordAmountPopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        CustomWordAmountPopup.hide();
      } else if (!$("#customTestDurationPopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        CustomTestDurationPopup.hide();
      } else if (!$("#customTextPopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        CustomTextPopup.hide();
      } else if (!$("#quoteSearchPopupWrapper").hasClass("hidden")) {
        event.preventDefault();
        QuoteSearchPopupWrapper.hide();
      } else if (!$("#commandLineWrapper").hasClass("hidden")) {
        if (CommandlineLists.current.length > 1) {
          CommandlineLists.current.pop();
          $("#commandLine").removeClass("allCommands");
          show();
        } else {
          hide();
        }
        UpdateConfig.setFontFamily(Config.fontFamily, true);
      } else if (event.key === "Tab" || !Config.swapEscAndTab) {
        if (Config.singleListCommandLine == "on") {
          useSingleListCommandLine(false);
        } else {
          CommandlineLists.setCurrent([CommandlineLists.defaultCommands]);
        }
        show();
      }
    }
  });
});

$("#commandInput input").keydown((e) => {
  if (e.key === "Enter") {
    //enter
    e.preventDefault();
    let command = $("#commandInput input").attr("command");
    let value = $("#commandInput input").val();
    let list = CommandlineLists.current[CommandlineLists.current.length - 1];
    $.each(list.list, (i, obj) => {
      if (obj.id == command) {
        obj.exec(value);
        if (obj.subgroup !== null && obj.subgroup !== undefined) {
          //TODO: what is this for?
          // subgroup = obj.subgroup;
        }
      }
    });
    try {
      firebase.analytics().logEvent("usedCommandLine", {
        command: command,
      });
    } catch (e) {
      console.log("Analytics unavailable");
    }
    hide();
  }
  return;
});

$(document).on("mousemove", () => {
  if (!commandLineMouseMode) commandLineMouseMode = true;
});

$(document).on(
  "mouseenter",
  "#commandLineWrapper #commandLine .suggestions .entry",
  (e) => {
    if (!commandLineMouseMode) return;
    $(e.target).addClass("activeMouse");
  }
);

$(document).on(
  "mouseleave",
  "#commandLineWrapper #commandLine .suggestions .entry",
  (e) => {
    if (!commandLineMouseMode) return;
    $(e.target).removeClass("activeMouse");
  }
);

$("#commandLineWrapper #commandLine .suggestions").on("mouseover", (e) => {
  if (!commandLineMouseMode) return;
  // console.log("clearing keyboard active");
  $("#commandLineWrapper #commandLine .suggestions .entry").removeClass(
    "activeKeyboard"
  );
  let hoverId = $(e.target).attr("command");
  try {
    let list = CommandlineLists.current[CommandlineLists.current.length - 1];
    $.each(list.list, (index, obj) => {
      if (obj.id == hoverId) {
        if (
          (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") &&
          !ThemeController.randomTheme
        )
          ThemeController.clearPreview();
        if (!/font/gi.test(obj.id))
          UpdateConfig.previewFontFamily(Config.fontFamily);
        obj.hover();
      }
    });
  } catch (e) {}
});

$(document).on(
  "click",
  "#commandLineWrapper #commandLine .suggestions .entry",
  (e) => {
    $(".suggestions .entry").removeClass("activeKeyboard");
    trigger($(e.currentTarget).attr("command"));
  }
);

$("#commandLineWrapper").click((e) => {
  if ($(e.target).attr("id") === "commandLineWrapper") {
    hide();
    UpdateConfig.setFontFamily(Config.fontFamily, true);
    // if (Config.customTheme === true) {
    //   applyCustomThemeColors();
    // } else {
    //   setTheme(Config.theme, true);
    // }
  }
});

//might come back to it later
// function shiftCommand(){
//   let activeEntries = $("#commandLineWrapper #commandLine .suggestions .entry.activeKeyboard, #commandLineWrapper #commandLine .suggestions .entry.activeMouse");
//   activeEntries.each((index, activeEntry) => {
//     let commandId = activeEntry.getAttribute('command');
//     let foundCommand = null;
//     CommandlineLists.defaultCommands.list.forEach(command => {
//       if(foundCommand === null && command.id === commandId){
//         foundCommand = command;
//       }
//     })
//     if(foundCommand.shift){
//       $(activeEntry).find('div').text(foundCommand.shift.display);
//     }
//   })
// }

// let shiftedCommands = false;
// $(document).keydown((e) => {
//   if (e.key === "Shift") {
//     if(shiftedCommands === false){
//       shiftedCommands = true;
//       shiftCommand();
//     }

//   }
// });

// $(document).keyup((e) => {
//   if (e.key === "Shift") {
//     shiftedCommands = false;
//   }
// });

$(document).keydown((e) => {
  // if (isPreviewingTheme) {
  // console.log("applying theme");
  // applyCustomThemeColors();
  // previewTheme(Config.theme, false);
  // }
  if (!$("#commandLineWrapper").hasClass("hidden")) {
    $("#commandLine input").focus();
    if (e.key == ">" && Config.singleListCommandLine == "manual") {
      if (!isSingleListCommandLineActive()) {
        useSingleListCommandLine(false);
        return;
      } else if ($("#commandLine input").val() == ">") {
        //so that it will ignore succeeding ">" when input is already ">"
        e.preventDefault();
        return;
      }
    }

    if (e.key === "Backspace" || e.key === "Delete") {
      setTimeout(() => {
        let inputVal = $("#commandLine input").val();
        if (
          Config.singleListCommandLine == "manual" &&
          isSingleListCommandLineActive() &&
          inputVal[0] !== ">"
        ) {
          restoreOldCommandLine(false);
        }
      }, 1);
    }

    if (e.key === "Enter") {
      //enter
      e.preventDefault();
      let command = $(".suggestions .entry.activeKeyboard").attr("command");
      trigger(command);
      return;
    }
    if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Tab") {
      e.preventDefault();
      $("#commandLineWrapper #commandLine .suggestions .entry").unbind(
        "mouseenter mouseleave"
      );
      let entries = $(".suggestions .entry");
      let activenum = -1;
      let hoverId;
      $.each(entries, (index, obj) => {
        if ($(obj).hasClass("activeKeyboard")) activenum = index;
      });
      if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
        entries.removeClass("activeKeyboard");
        if (activenum == 0) {
          $(entries[entries.length - 1]).addClass("activeKeyboard");
          hoverId = $(entries[entries.length - 1]).attr("command");
        } else {
          $(entries[--activenum]).addClass("activeKeyboard");
          hoverId = $(entries[activenum]).attr("command");
        }
      }
      if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
        entries.removeClass("activeKeyboard");
        if (activenum + 1 == entries.length) {
          $(entries[0]).addClass("activeKeyboard");
          hoverId = $(entries[0]).attr("command");
        } else {
          $(entries[++activenum]).addClass("activeKeyboard");
          hoverId = $(entries[activenum]).attr("command");
        }
      }
      try {
        let scroll =
          Math.abs(
            $(".suggestions").offset().top -
              $(".entry.activeKeyboard").offset().top -
              $(".suggestions").scrollTop()
          ) -
          $(".suggestions").outerHeight() / 2 +
          $($(".entry")[0]).outerHeight();
        $(".suggestions").scrollTop(scroll);
      } catch (e) {
        console.log("could not scroll suggestions: " + e.message);
      }
      // console.log(`scrolling to ${scroll}`);
      try {
        let list =
          CommandlineLists.current[CommandlineLists.current.length - 1];
        $.each(list.list, (index, obj) => {
          if (obj.id == hoverId) {
            if (
              (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") &&
              !ThemeController.randomTheme
            )
              ThemeController.clearPreview();
            if (!/font/gi.test(obj.id))
              UpdateConfig.previewFontFamily(Config.fontFamily);
            obj.hover();
          }
        });
      } catch (e) {}

      return false;
    }
  }
});

$(document).on("click", "#commandLineMobileButton", () => {
  CommandlineLists.setCurrent([CommandlineLists.defaultCommands]);
  show();
});

==> ./monkeytype/src/js/test/poetry.js <==
const bannedChars = ["—", "_", " "];
const maxWords = 100;
const apiURL = "https://poetrydb.org/random";

export class Poem {
  constructor(title, author, words) {
    this.title = title;
    this.author = author;
    this.words = words;

    this.cleanUpText();
  }

  cleanUpText() {
    var count = 0;
    var scrubbedWords = [];
    for (var i = 0; i < this.words.length; i++) {
      let scrubbed = "";
      for (var j = 0; j < this.words[i].length; j++) {
        if (!bannedChars.includes(this.words[i][j]))
          scrubbed += this.words[i][j];
      }

      if (scrubbed == "") continue;

      scrubbedWords.push(scrubbed);
      count++;

      if (count == maxWords) break;
    }

    this.words = scrubbedWords;
  }
}

export async function getPoem() {
  return new Promise((res, rej) => {
    console.log("Getting poem");
    var poemReq = new XMLHttpRequest();
    poemReq.onload = () => {
      if (poemReq.readyState == 4) {
        if (poemReq.status == 200) {
          let poemObj = JSON.parse(poemReq.responseText)[0];
          let words = [];
          poemObj.lines.forEach((line) => {
            line.split(" ").forEach((word) => {
              words.push(word);
            });
          });

          let poem = new Poem(poemObj.title, poemObj.author, words);
          res(poem);
        } else {
          rej(poemReq.status);
        }
      }
    };
    poemReq.open("GET", apiURL);
    poemReq.send();
  });
}

==> ./monkeytype/src/js/test/test-timer.js <==
//most of the code is thanks to
//https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript

import Config, * as UpdateConfig from "./config";
import * as CustomText from "./custom-text";
import * as TimerProgress from "./timer-progress";
import * as LiveWpm from "./live-wpm";
import * as TestStats from "./test-stats";
import * as Monkey from "./monkey";
import * as Misc from "./misc";
import * as Notifications from "./notifications";
import * as TestLogic from "./test-logic";
import * as Caret from "./caret";

export let slowTimer = false;
let slowTimerCount = 0;
export let time = 0;
let timer = null;
const interval = 1000;
let expected = 0;

function setSlowTimer() {
  if (slowTimer) return;
  slowTimer = true;
  console.error("Slow timer, disabling animations");
  // Notifications.add("Slow timer detected", -1, 5);
}

function clearSlowTimer() {
  slowTimer = false;
  slowTimerCount = 0;
}

let timerDebug = false;
export function enableTimerDebug() {
  timerDebug = true;
}

export function clear() {
  time = 0;
  clearTimeout(timer);
}

function premid() {
  if (timerDebug) console.time("premid");
  document.querySelector("#premidSecondsLeft").innerHTML = Config.time - time;
  if (timerDebug) console.timeEnd("premid");
}

function updateTimer() {
  if (timerDebug) console.time("timer progress update");
  if (
    Config.mode === "time" ||
    (Config.mode === "custom" && CustomText.isTimeRandom)
  ) {
    TimerProgress.update(time);
  }
  if (timerDebug) console.timeEnd("timer progress update");
}

function calculateWpmRaw() {
  if (timerDebug) console.time("calculate wpm and raw");
  let wpmAndRaw = TestLogic.calculateWpmAndRaw();
  if (timerDebug) console.timeEnd("calculate wpm and raw");
  if (timerDebug) console.time("update live wpm");
  LiveWpm.update(wpmAndRaw.wpm, wpmAndRaw.raw);
  if (timerDebug) console.timeEnd("update live wpm");
  if (timerDebug) console.time("push to history");
  TestStats.pushToWpmHistory(wpmAndRaw.wpm);
  TestStats.pushToRawHistory(wpmAndRaw.raw);
  if (timerDebug) console.timeEnd("push to history");
  return wpmAndRaw;
}

function monkey(wpmAndRaw) {
  if (timerDebug) console.time("update monkey");
  Monkey.updateFastOpacity(wpmAndRaw.wpm);
  if (timerDebug) console.timeEnd("update monkey");
}

function calculateAcc() {
  if (timerDebug) console.time("calculate acc");
  let acc = Misc.roundTo2(TestStats.calculateAccuracy());
  if (timerDebug) console.timeEnd("calculate acc");
  return acc;
}

function layoutfluid() {
  if (timerDebug) console.time("layoutfluid");
  if (Config.funbox === "layoutfluid" && Config.mode === "time") {
    const layouts = Config.customLayoutfluid
      ? Config.customLayoutfluid.split("#")
      : ["qwerty", "dvorak", "colemak"];
    // console.log(Config.customLayoutfluid);
    // console.log(layouts);
    const numLayouts = layouts.length;
    let index = 0;
    index = Math.floor(time / (Config.time / numLayouts));

    if (
      time == Math.floor(Config.time / numLayouts) - 3 ||
      time == (Config.time / numLayouts) * 2 - 3
    ) {
      Notifications.add("3", 0, 1);
    }
    if (
      time == Math.floor(Config.time / numLayouts) - 2 ||
      time == Math.floor(Config.time / numLayouts) * 2 - 2
    ) {
      Notifications.add("2", 0, 1);
    }
    if (
      time == Math.floor(Config.time / numLayouts) - 1 ||
      time == Math.floor(Config.time / numLayouts) * 2 - 1
    ) {
      Notifications.add("1", 0, 1);
    }

    if (Config.layout !== layouts[index] && layouts[index] !== undefined) {
      Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0);
      UpdateConfig.setLayout(layouts[index], true);
      UpdateConfig.setKeymapLayout(layouts[index], true);
    }
  }
  if (timerDebug) console.timeEnd("layoutfluid");
}

function checkIfFailed(wpmAndRaw, acc) {
  if (timerDebug) console.time("fail conditions");
  TestStats.pushKeypressesToHistory();
  if (
    Config.minWpm === "custom" &&
    wpmAndRaw.wpm < parseInt(Config.minWpmCustomSpeed) &&
    TestLogic.words.currentIndex > 3
  ) {
    clearTimeout(timer);
    TestLogic.fail("min wpm");
    clearSlowTimer();
    return;
  }
  if (
    Config.minAcc === "custom" &&
    acc < parseInt(Config.minAccCustom) &&
    TestLogic.words.currentIndex > 3
  ) {
    clearTimeout(timer);
    TestLogic.fail("min accuracy");
    clearSlowTimer();
    return;
  }
  if (timerDebug) console.timeEnd("fail conditions");
}

function checkIfTimeIsUp() {
  if (timerDebug) console.time("times up check");
  if (
    Config.mode == "time" ||
    (Config.mode === "custom" && CustomText.isTimeRandom)
  ) {
    if (
      (time >= Config.time && Config.time !== 0 && Config.mode === "time") ||
      (time >= CustomText.time &&
        CustomText.time !== 0 &&
        Config.mode === "custom")
    ) {
      //times up
      clearTimeout(timer);
      Caret.hide();
      TestLogic.input.pushHistory();
      TestLogic.corrected.pushHistory();
      TestLogic.finish();
      clearSlowTimer();
      return;
    }
  }
  if (timerDebug) console.timeEnd("times up check");
}

// ---------------------------------------

let timerStats = [];

export function getTimerStats() {
  return timerStats;
}

async function timerStep() {
  if (timerDebug) console.time("timer step -----------------------------");
  time++;
  premid();
  updateTimer();
  let wpmAndRaw = calculateWpmRaw();
  let acc = calculateAcc();
  monkey(wpmAndRaw);
  layoutfluid();
  checkIfFailed(wpmAndRaw, acc);
  checkIfTimeIsUp();
  if (timerDebug) console.timeEnd("timer step -----------------------------");
}

export async function start() {
  clearSlowTimer();
  timerStats = [];
  expected = TestStats.start + interval;
  (function loop() {
    const delay = expected - performance.now();
    timerStats.push({
      dateNow: Date.now(),
      now: performance.now(),
      expected: expected,
      nextDelay: delay,
    });
    if (
      (Config.mode === "time" && Config.time < 130 && Config.time > 0) ||
      (Config.mode === "words" && Config.words < 250 && Config.words > 0)
    ) {
      if (delay < interval / 2) {
        //slow timer
        setSlowTimer();
      }
      if (delay < interval / 10) {
        slowTimerCount++;
        if (slowTimerCount > 5) {
          //slow timer
          Notifications.add(
            "Stopping the test due to bad performance. This would cause test calculations to be incorrect. If this happens a lot, please report this.",
            -1
          );
          TestLogic.fail("slow timer");
        }
      }
    }
    timer = setTimeout(function () {
      // time++;

      if (!TestLogic.active) {
        clearTimeout(timer);
        clearSlowTimer();
        return;
      }

      timerStep();

      expected += interval;
      loop();
    }, delay);
  })();
}

==> ./monkeytype/src/js/test/caps-warning.js <==
import Config from "./config";

function show() {
  if ($("#capsWarning").hasClass("hidden")) {
    $("#capsWarning").removeClass("hidden");
  }
}

function hide() {
  if (!$("#capsWarning").hasClass("hidden")) {
    $("#capsWarning").addClass("hidden");
  }
}

$(document).keydown(function (event) {
  try {
    if (
      Config.capsLockWarning &&
      event.originalEvent.getModifierState("CapsLock")
    ) {
      show();
    } else {
      hide();
    }
  } catch {}
});

$(document).keyup(function (event) {
  try {
    if (
      Config.capsLockWarning &&
      event.originalEvent.getModifierState("CapsLock")
    ) {
      show();
    } else {
      hide();
    }
  } catch {}
});

==> ./monkeytype/src/js/test/shift-tracker.js <==
import Config from "./config";
import Layouts from "./layouts";

export let leftState = false;
export let rightState = false;

let keymapStrings = {
  left: null,
  right: null,
  keymap: null,
};

function buildKeymapStrings() {
  if (keymapStrings.keymap === Config.keymapLayout) return;

  let layout = Layouts[Config.keymapLayout]?.keys;

  if (!layout) {
    keymapStrings = {
      left: null,
      right: null,
      keymap: Config.keymapLayout,
    };
  } else {
    keymapStrings.left = (
      layout.slice(0, 7).join(" ") +
      " " +
      layout.slice(13, 19).join(" ") +
      " " +
      layout.slice(26, 31).join(" ") +
      " " +
      layout.slice(38, 43).join(" ")
    ).replace(/ /g, "");
    keymapStrings.right = (
      layout.slice(6, 13).join(" ") +
      " " +
      layout.slice(18, 26).join(" ") +
      " " +
      layout.slice(31, 38).join(" ") +
      " " +
      layout.slice(42, 48).join(" ")
    ).replace(/ /g, "");
    keymapStrings.keymap = Config.keymapLayout;
  }
}

$(document).keydown((e) => {
  if (e.code === "ShiftLeft") {
    leftState = true;
    rightState = false;
  } else if (e.code === "ShiftRight") {
    leftState = false;
    rightState = true;
  }
});

$(document).keyup((e) => {
  if (e.code === "ShiftLeft" || e.code === "ShiftRight") {
    leftState = false;
    rightState = false;
  }
});

export function reset() {
  leftState = false;
  rightState = false;
}

let leftSideKeys = [
  "KeyQ",
  "KeyW",
  "KeyE",
  "KeyR",
  "KeyT",

  "KeyA",
  "KeyS",
  "KeyD",
  "KeyF",
  "KeyG",

  "KeyZ",
  "KeyX",
  "KeyC",
  "KeyV",

  "Backquote",
  "Digit1",
  "Digit2",
  "Digit3",
  "Digit4",
  "Digit5",
];

let rightSideKeys = [
  "KeyU",
  "KeyI",
  "KeyO",
  "KeyP",

  "KeyH",
  "KeyJ",
  "KeyK",
  "KeyL",

  "KeyN",
  "KeyM",

  "Digit7",
  "Digit8",
  "Digit9",
  "Digit0",

  "Backslash",
  "BracketLeft",
  "BracketRight",
  "Semicolon",
  "Quote",
  "Comma",
  "Period",
  "Slash",
];

export function isUsingOppositeShift(event) {
  if (!leftState && !rightState) return null;

  if (Config.oppositeShiftMode === "on") {
    if (
      !rightSideKeys.includes(event.code) &&
      !leftSideKeys.includes(event.code)
    )
      return null;

    if (
      (leftState && rightSideKeys.includes(event.code)) ||
      (rightState && leftSideKeys.includes(event.code))
    ) {
      return true;
    } else {
      return false;
    }
  } else if (Config.oppositeShiftMode === "keymap") {
    buildKeymapStrings();

    if (!keymapStrings.left || !keymapStrings.right) return null;

    if (
      (leftState && keymapStrings.right.includes(event.key)) ||
      (rightState && keymapStrings.left.includes(event.key))
    ) {
      return true;
    } else {
      return false;
    }
  }
}

==> ./monkeytype/src/js/test/keymap.js <==
import Config, * as UpdateConfig from "./config";
import * as ThemeColors from "./theme-colors";
import layouts from "./layouts";
import * as CommandlineLists from "./commandline-lists";
import * as Commandline from "./commandline";
import * as TestTimer from "./test-timer";

export function highlightKey(currentKey) {
  if (Config.mode === "zen") return;
  try {
    if ($(".active-key") != undefined) {
      $(".active-key").removeClass("active-key");
    }

    let highlightKey;
    switch (currentKey) {
      case "\\":
      case "|":
        highlightKey = "#KeyBackslash";
        break;
      case "}":
      case "]":
        highlightKey = "#KeyRightBracket";
        break;
      case "{":
      case "[":
        highlightKey = "#KeyLeftBracket";
        break;
      case '"':
      case "'":
        highlightKey = "#KeyQuote";
        break;
      case ":":
      case ";":
        highlightKey = "#KeySemicolon";
        break;
      case "<":
      case ",":
        highlightKey = "#KeyComma";
        break;
      case ">":
      case ".":
        highlightKey = "#KeyPeriod";
        break;
      case "?":
      case "/":
        highlightKey = "#KeySlash";
        break;
      case "":
        highlightKey = "#KeySpace";
        break;
      default:
        highlightKey = `#Key${currentKey}`;
    }

    $(highlightKey).addClass("active-key");
    if (highlightKey === "#KeySpace") {
      $("#KeySpace2").addClass("active-key");
    }
  } catch (e) {
    console.log("could not update highlighted keymap key: " + e.message);
  }
}

export async function flashKey(key, correct) {
  if (key == undefined) return;
  switch (key) {
    case "\\":
    case "|":
      key = "#KeyBackslash";
      break;
    case "}":
    case "]":
      key = "#KeyRightBracket";
      break;
    case "{":
    case "[":
      key = "#KeyLeftBracket";
      break;
    case '"':
    case "'":
      key = "#KeyQuote";
      break;
    case ":":
    case ";":
      key = "#KeySemicolon";
      break;
    case "<":
    case ",":
      key = "#KeyComma";
      break;
    case ">":
    case ".":
      key = "#KeyPeriod";
      break;
    case "?":
    case "/":
      key = "#KeySlash";
      break;
    case "" || "Space":
      key = "#KeySpace";
      break;
    default:
      key = `#Key${key.toUpperCase()}`;
  }

  if (key == "#KeySpace") {
    key = ".key-split-space";
  }

  let themecolors = await ThemeColors.get();

  try {
    if (correct || Config.blindMode) {
      $(key)
        .stop(true, true)
        .css({
          color: themecolors.bg,
          backgroundColor: themecolors.main,
          borderColor: themecolors.main,
        })
        .animate(
          {
            color: themecolors.sub,
            backgroundColor: "transparent",
            borderColor: themecolors.sub,
          },
          TestTimer.slowTimer ? 0 : 500,
          "easeOutExpo"
        );
    } else {
      $(key)
        .stop(true, true)
        .css({
          color: themecolors.bg,
          backgroundColor: themecolors.error,
          borderColor: themecolors.error,
        })
        .animate(
          {
            color: themecolors.sub,
            backgroundColor: "transparent",
            borderColor: themecolors.sub,
          },
          TestTimer.slowTimer ? 0 : 500,
          "easeOutExpo"
        );
    }
  } catch (e) {}
}

export function hide() {
  $(".keymap").addClass("hidden");
}

export function show() {
  $(".keymap").removeClass("hidden");
}

export function refreshKeys(layout) {
  try {
    let lts = layouts[layout]; //layout to show
    let layoutString = layout;
    if (Config.keymapLayout === "overrideSync") {
      if (Config.layout === "default") {
        lts = layouts["qwerty"];
        layoutString = "default";
      } else {
        lts = layouts[Config.layout];
        layoutString = Config.layout;
      }
    }

    if (lts.keymapShowTopRow) {
      $(".keymap .r1").removeClass("hidden");
    } else {
      $(".keymap .r1").addClass("hidden");
    }

    if (Config.keymapStyle === "alice") {
      $(".keymap .extraKey").removeClass("hidden");
    } else {
      $(".keymap .extraKey").addClass("hidden");
    }

    $($(".keymap .r5 .keymap-key .letter")[0]).text(
      layoutString.replace(/_/g, " ")
    );

    if (lts.iso) {
      $(".keymap .r4 .keymap-key.first").removeClass("hidden-key");
    } else {
      $(".keymap .r4 .keymap-key.first").addClass("hidden-key");
    }

    var toReplace = lts.keys.slice(1, 48);
    var count = 0;

    // let repeatB = false;
    $(".keymap .keymap-key .letter")
      .map(function () {
        if (count < toReplace.length) {
          var key = toReplace[count].charAt(0);
          this.innerHTML = key;

          switch (key) {
            case "\\":
            case "|":
              this.parentElement.id = "KeyBackslash";
              break;
            case "}":
            case "]":
              this.parentElement.id = "KeyRightBracket";
              break;
            case "{":
            case "[":
              this.parentElement.id = "KeyLeftBracket";
              break;
            case '"':
            case "'":
              this.parentElement.id = "KeyQuote";
              break;
            case ":":
            case ";":
              this.parentElement.id = "KeySemicolon";
              break;
            case "<":
            case ",":
              this.parentElement.id = "KeyComma";
              break;
            case ">":
            case ".":
              this.parentElement.id = "KeyPeriod";
              break;
            case "?":
            case "/":
              this.parentElement.id = "KeySlash";
              break;
            case "":
              this.parentElement.id = "KeySpace";
              break;
            default:
              this.parentElement.id = `Key${key.toUpperCase()}`;
          }
        }

        // if (count == 41 && !repeatB) {
        //   repeatB = true;
        // }else{
        //   repeatB = false;
        //   count++;
        // }

        count++;

        // }
      })
      .get();
  } catch (e) {
    console.log(
      "something went wrong when changing layout, resettings: " + e.message
    );
    UpdateConfig.setKeymapLayout("qwerty", true);
  }
}

$(document).on("click", ".keymap .r5 #KeySpace", (e) => {
  CommandlineLists.setCurrent([CommandlineLists.commandsKeymapLayouts]);
  Commandline.show();
});

==> ./monkeytype/src/js/test/wordset.js <==
import Config from "./config";

let currentWordset = null;
let currentWordGenerator = null;

class Wordset {
  constructor(words) {
    this.words = words;
    this.length = this.words.length;
  }

  randomWord() {
    return this.words[Math.floor(Math.random() * this.length)];
  }
}

const prefixSize = 2;

class CharDistribution {
  constructor() {
    this.chars = {};
    this.count = 0;
  }

  addChar(char) {
    this.count++;
    if (char in this.chars) {
      this.chars[char]++;
    } else {
      this.chars[char] = 1;
    }
  }

  randomChar() {
    const randomIndex = Math.floor(Math.random() * this.count);
    let runningCount = 0;
    for (const [char, charCount] of Object.entries(this.chars)) {
      runningCount += charCount;
      if (runningCount > randomIndex) {
        return char;
      }
    }
  }
}

class WordGenerator extends Wordset {
  constructor(words) {
    super(words);
    // Can generate an unbounded number of words in theory.
    this.length = Infinity;

    this.ngrams = {};
    for (let word of words) {
      // Mark the end of each word with a space.
      word += " ";
      let prefix = "";
      for (const c of word) {
        // Add `c` to the distribution of chars that can come after `prefix`.
        if (!(prefix in this.ngrams)) {
          this.ngrams[prefix] = new CharDistribution();
        }
        this.ngrams[prefix].addChar(c);
        prefix = (prefix + c).substr(-prefixSize);
      }
    }
  }

  randomWord() {
    let word = "";
    for (;;) {
      const prefix = word.substr(-prefixSize);
      let charDistribution = this.ngrams[prefix];
      if (!charDistribution) {
        // This shouldn't happen if this.ngrams is complete. If it does
        // somehow, start generating a new word.
        word = "";
        continue;
      }
      // Pick a random char from the distribution that comes after `prefix`.
      const nextChar = charDistribution.randomChar();
      if (nextChar == " ") {
        // A space marks the end of the word, so stop generating and return.
        break;
      }
      word += nextChar;
    }
    return word;
  }
}

export function withWords(words) {
  if (Config.funbox == "pseudolang") {
    if (currentWordGenerator == null || words !== currentWordGenerator.words) {
      currentWordGenerator = new WordGenerator(words);
    }
    return currentWordGenerator;
  } else {
    if (currentWordset == null || words !== currentWordset.words) {
      currentWordset = new Wordset(words);
    }
    return currentWordset;
  }
}

==> ./monkeytype/src/js/test/live-acc.js <==
import Config from "./config";
import * as TestLogic from "./test-logic";

export function update(acc) {
  let number = Math.floor(acc);
  if (Config.blindMode) {
    number = 100;
  }
  document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = number + "%";
  document.querySelector("#liveAcc").innerHTML = number + "%";
}

export function show() {
  if (!Config.showLiveAcc) return;
  if (!TestLogic.active) return;
  if (Config.timerStyle === "mini") {
    // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity);
    if (!$("#miniTimerAndLiveWpm .acc").hasClass("hidden")) return;
    $("#miniTimerAndLiveWpm .acc")
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: Config.timerOpacity,
        },
        125
      );
  } else {
    // $("#liveWpm").css("opacity", Config.timerOpacity);
    if (!$("#liveAcc").hasClass("hidden")) return;
    $("#liveAcc").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: Config.timerOpacity,
      },
      125
    );
  }
}

export function hide() {
  // $("#liveWpm").css("opacity", 0);
  // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0);
  $("#liveAcc").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#liveAcc").addClass("hidden");
    }
  );
  $("#miniTimerAndLiveWpm .acc").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#miniTimerAndLiveWpm .acc").addClass("hidden");
    }
  );
}

==> ./monkeytype/src/js/test/weak-spot.js <==
import * as TestStats from "./test-stats";

// Changes how quickly it 'learns' scores - very roughly the score for a char
// is based on last perCharCount occurrences. Make it smaller to adjust faster.
const perCharCount = 50;

// Choose the highest scoring word from this many random words. Higher values
// will choose words with more weak letters on average.
const wordSamples = 20;

// Score penatly (in milliseconds) for getting a letter wrong.
const incorrectPenalty = 5000;

let scores = {};

class Score {
  constructor() {
    this.average = 0.0;
    this.count = 0;
  }

  update(score) {
    if (this.count < perCharCount) {
      this.count++;
    }
    const adjustRate = 1.0 / this.count;
    // Keep an exponential moving average of the score over time.
    this.average = score * adjustRate + this.average * (1 - adjustRate);
  }
}

export function updateScore(char, isCorrect) {
  const timings = TestStats.keypressTimings.spacing.array;
  if (timings.length == 0) {
    return;
  }
  let score = timings[timings.length - 1];
  if (!isCorrect) {
    score += incorrectPenalty;
  }
  if (!(char in scores)) {
    scores[char] = new Score();
  }
  scores[char].update(score);
}

function score(word) {
  let total = 0.0;
  let numChars = 0;
  for (const c of word) {
    if (c in scores) {
      total += scores[c].average;
      numChars++;
    }
  }
  return numChars == 0 ? 0.0 : total / numChars;
}

export function getWord(wordset) {
  let highScore;
  let randomWord;
  for (let i = 0; i < wordSamples; i++) {
    let newWord = wordset.randomWord();
    let newScore = score(newWord);
    if (i == 0 || newScore > highScore) {
      randomWord = newWord;
      highScore = newScore;
    }
  }
  return randomWord;
}

==> ./monkeytype/src/js/test/tts.js <==
import Config from "./config";
import * as Misc from "./misc";

let voice;

export async function setLanguage(lang = Config.language) {
  if (!voice) return;
  let language = await Misc.getLanguage(lang);
  let bcp = language.bcp47 ? language.bcp47 : "en-US";
  voice.lang = bcp;
}

export async function init() {
  voice = new SpeechSynthesisUtterance();
  setLanguage();
}

export function clear() {
  voice = undefined;
}

export function speak(text) {
  if (!voice) init();
  voice.text = text;
  window.speechSynthesis.cancel();
  window.speechSynthesis.speak(voice);
}

==> ./monkeytype/src/js/test/test-logic.js <==
import * as TestUI from "./test-ui";
import * as ManualRestart from "./manual-restart-tracker";
import Config, * as UpdateConfig from "./config";
import * as Misc from "./misc";
import * as Notifications from "./notifications";
import * as CustomText from "./custom-text";
import * as TestStats from "./test-stats";
import * as PractiseWords from "./practise-words";
import * as ShiftTracker from "./shift-tracker";
import * as Focus from "./focus";
import * as Funbox from "./funbox";
import * as Keymap from "./keymap";
import * as ThemeController from "./theme-controller";
import * as PaceCaret from "./pace-caret";
import * as Caret from "./caret";
import * as LiveWpm from "./live-wpm";
import * as LiveAcc from "./live-acc";
import * as LiveBurst from "./live-burst";
import * as TimerProgress from "./timer-progress";
import * as UI from "./ui";
import * as QuoteSearchPopup from "./quote-search-popup";
import * as QuoteSubmitPopup from "./quote-submit-popup";
import * as PbCrown from "./pb-crown";
import * as TestTimer from "./test-timer";
import * as OutOfFocus from "./out-of-focus";
import * as AccountButton from "./account-button";
import * as DB from "./db";
import * as Replay from "./replay.js";
import axiosInstance from "./axios-instance";
import * as MonkeyPower from "./monkey-power";
import * as Poetry from "./poetry.js";
import * as Wikipedia from "./wikipedia.js";
import * as TodayTracker from "./today-tracker";
import * as WeakSpot from "./weak-spot";
import * as Wordset from "./wordset";
import * as ChallengeContoller from "./challenge-controller";
import * as RateQuotePopup from "./rate-quote-popup";
import * as BritishEnglish from "./british-english";
import * as LazyMode from "./lazy-mode";
import * as Result from "./result";

const objecthash = require("object-hash");

export let glarsesMode = false;

let failReason = "";

export function toggleGlarses() {
  glarsesMode = true;
  console.log(
    "Glarses Mode On - test result will be hidden. You can check the stats in the console (here)"
  );
  console.log("To disable Glarses Mode refresh the page.");
}

export let notSignedInLastResult = null;

export function clearNotSignedInResult() {
  notSignedInLastResult = null;
}

export function setNotSignedInUid(uid) {
  notSignedInLastResult.uid = uid;
  delete notSignedInLastResult.hash;
  notSignedInLastResult.hash = objecthash(notSignedInLastResult);
}

class Words {
  constructor() {
    this.list = [];
    this.length = 0;
    this.currentIndex = 0;
  }
  get(i, raw = false) {
    if (i === undefined) {
      return this.list;
    } else {
      if (raw) {
        return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase();
      } else {
        return this.list[i];
      }
    }
  }
  getCurrent() {
    return this.list[this.currentIndex];
  }
  getLast() {
    return this.list[this.list.length - 1];
  }
  push(word) {
    this.list.push(word);
    this.length = this.list.length;
  }
  reset() {
    this.list = [];
    this.currentIndex = 0;
    this.length = this.list.length;
  }
  resetCurrentIndex() {
    this.currentIndex = 0;
  }
  decreaseCurrentIndex() {
    this.currentIndex--;
  }
  increaseCurrentIndex() {
    this.currentIndex++;
  }
  clean() {
    for (let s of this.list) {
      if (/ +/.test(s)) {
        let id = this.list.indexOf(s);
        let tempList = s.split(" ");
        this.list.splice(id, 1);
        for (let i = 0; i < tempList.length; i++) {
          this.list.splice(id + i, 0, tempList[i]);
        }
      }
    }
  }
}

class Input {
  constructor() {
    this.current = "";
    this.history = [];
    this.length = 0;
  }

  reset() {
    this.current = "";
    this.history = [];
    this.length = 0;
  }

  resetHistory() {
    this.history = [];
    this.length = 0;
  }

  setCurrent(val) {
    this.current = val;
    this.length = this.current.length;
  }

  appendCurrent(val) {
    this.current += val;
    this.length = this.current.length;
  }

  resetCurrent() {
    this.current = "";
  }

  getCurrent() {
    return this.current;
  }

  pushHistory() {
    this.history.push(this.current);
    this.historyLength = this.history.length;
    this.resetCurrent();
  }

  popHistory() {
    return this.history.pop();
  }

  getHistory(i) {
    if (i === undefined) {
      return this.history;
    } else {
      return this.history[i];
    }
  }

  getHistoryLast() {
    return this.history[this.history.length - 1];
  }
}

class Corrected {
  constructor() {
    this.current = "";
    this.history = [];
  }
  setCurrent(val) {
    this.current = val;
  }

  appendCurrent(val) {
    this.current += val;
  }

  resetCurrent() {
    this.current = "";
  }

  resetHistory() {
    this.history = [];
  }

  reset() {
    this.resetCurrent();
    this.resetHistory();
  }

  getHistory(i) {
    return this.history[i];
  }

  popHistory() {
    return this.history.pop();
  }

  pushHistory() {
    this.history.push(this.current);
    this.current = "";
  }
}

export let active = false;
export let words = new Words();
export let input = new Input();
export let corrected = new Corrected();
export let currentWordIndex = 0;
export let isRepeated = false;
export let isPaceRepeat = false;
export let lastTestWpm = 0;
export let hasTab = false;
export let randomQuote = null;
export let bailout = false;

export function setActive(tf) {
  active = tf;
  if (!tf) MonkeyPower.reset();
}

export function setRepeated(tf) {
  isRepeated = tf;
}

export function setPaceRepeat(tf) {
  isPaceRepeat = tf;
}

export function setHasTab(tf) {
  hasTab = tf;
}

export function setBailout(tf) {
  bailout = tf;
}

export function setRandomQuote(rq) {
  randomQuote = rq;
}

let spanishSentenceTracker = "";
export function punctuateWord(previousWord, currentWord, index, maxindex) {
  let word = currentWord;

  let currentLanguage = Config.language.split("_")[0];

  let lastChar = Misc.getLastChar(previousWord);

  if (Config.funbox === "58008") {
    if (currentWord.length > 3) {
      if (Math.random() < 0.75) {
        let special = ["/", "*", "-", "+"][Math.floor(Math.random() * 4)];
        word = Misc.setCharAt(word, Math.floor(word.length / 2), special);
      }
    }
  } else {
    if (
      (index == 0 || lastChar == "." || lastChar == "?" || lastChar == "!") &&
      currentLanguage != "code"
    ) {
      //always capitalise the first word or if there was a dot unless using a code alphabet

      word = Misc.capitalizeFirstLetter(word);

      if (currentLanguage == "spanish" || currentLanguage == "catalan") {
        let rand = Math.random();
        if (rand > 0.9) {
          word = "¿" + word;
          spanishSentenceTracker = "?";
        } else if (rand > 0.8) {
          word = "¡" + word;
          spanishSentenceTracker = "!";
        }
      }
    } else if (
      (Math.random() < 0.1 &&
        lastChar != "." &&
        lastChar != "," &&
        index != maxindex - 2) ||
      index == maxindex - 1
    ) {
      if (currentLanguage == "spanish" || currentLanguage == "catalan") {
        if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") {
          word += spanishSentenceTracker;
          spanishSentenceTracker = "";
        }
      } else {
        let rand = Math.random();
        if (rand <= 0.8) {
          word += ".";
        } else if (rand > 0.8 && rand < 0.9) {
          if (currentLanguage == "french") {
            word = "?";
          } else if (
            currentLanguage == "arabic" ||
            currentLanguage == "persian" ||
            currentLanguage == "urdu"
          ) {
            word += "؟";
          } else if (currentLanguage == "greek") {
            word += ";";
          } else {
            word += "?";
          }
        } else {
          if (currentLanguage == "french") {
            word = "!";
          } else {
            word += "!";
          }
        }
      }
    } else if (
      Math.random() < 0.01 &&
      lastChar != "," &&
      lastChar != "." &&
      currentLanguage !== "russian"
    ) {
      word = `"${word}"`;
    } else if (
      Math.random() < 0.011 &&
      lastChar != "," &&
      lastChar != "." &&
      currentLanguage !== "russian" &&
      currentLanguage !== "ukrainian"
    ) {
      word = `'${word}'`;
    } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") {
      if (currentLanguage == "code") {
        let r = Math.random();
        if (r < 0.25) {
          word = `(${word})`;
        } else if (r < 0.5) {
          word = `{${word}}`;
        } else if (r < 0.75) {
          word = `[${word}]`;
        } else {
          word = `<${word}>`;
        }
      } else {
        word = `(${word})`;
      }
    } else if (
      Math.random() < 0.013 &&
      lastChar != "," &&
      lastChar != "." &&
      lastChar != ";" &&
      lastChar != "؛" &&
      lastChar != ":"
    ) {
      if (currentLanguage == "french") {
        word = ":";
      } else if (currentLanguage == "greek") {
        word = "·";
      } else {
        word += ":";
      }
    } else if (
      Math.random() < 0.014 &&
      lastChar != "," &&
      lastChar != "." &&
      previousWord != "-"
    ) {
      word = "-";
    } else if (
      Math.random() < 0.015 &&
      lastChar != "," &&
      lastChar != "." &&
      lastChar != ";" &&
      lastChar != "؛" &&
      lastChar != ":"
    ) {
      if (currentLanguage == "french") {
        word = ";";
      } else if (currentLanguage == "greek") {
        word = "·";
      } else if (currentLanguage == "arabic") {
        word += "؛";
      } else {
        word += ";";
      }
    } else if (Math.random() < 0.2 && lastChar != ",") {
      if (
        currentLanguage == "arabic" ||
        currentLanguage == "urdu" ||
        currentLanguage == "persian"
      ) {
        word += "،";
      } else {
        word += ",";
      }
    } else if (Math.random() < 0.25 && currentLanguage == "code") {
      let specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"];

      word = specials[Math.floor(Math.random() * 10)];
    }
  }
  return word;
}

export function startTest() {
  if (UI.pageTransition) {
    return false;
  }
  if (!Config.dbConfigLoaded) {
    UpdateConfig.setChangedBeforeDb(true);
  }
  try {
    if (firebase.auth().currentUser != null) {
      firebase.analytics().logEvent("testStarted");
    } else {
      firebase.analytics().logEvent("testStartedNoLogin");
    }
  } catch (e) {
    console.log("Analytics unavailable");
  }
  setActive(true);
  Replay.startReplayRecording();
  Replay.replayGetWordsList(words.list);
  TestStats.resetKeypressTimings();
  TimerProgress.restart();
  TimerProgress.show();
  $("#liveWpm").text("0");
  LiveWpm.show();
  LiveAcc.show();
  LiveBurst.show();
  TimerProgress.update(TestTimer.time);
  TestTimer.clear();

  if (Config.funbox === "memory") {
    Funbox.resetMemoryTimer();
    $("#wordsWrapper").addClass("hidden");
  }

  try {
    if (Config.paceCaret !== "off" || (Config.repeatedPace && isPaceRepeat))
      PaceCaret.start();
  } catch (e) {}
  //use a recursive self-adjusting timer to avoid time drift
  TestStats.setStart(performance.now());
  TestTimer.start();
  return true;
}

export function restart(
  withSameWordset = false,
  nosave = false,
  event,
  practiseMissed = false
) {
  if (TestUI.testRestarting || TestUI.resultCalculating) {
    try {
      event.preventDefault();
    } catch {}
    return;
  }
  if (UI.getActivePage() == "pageTest" && !TestUI.resultVisible) {
    if (!ManualRestart.get()) {
      if (hasTab) {
        try {
          if (!event.shiftKey) return;
        } catch {}
      }
      try {
        if (Config.mode !== "zen") event.preventDefault();
      } catch {}
      if (
        !Misc.canQuickRestart(
          Config.mode,
          Config.words,
          Config.time,
          CustomText
        )
      ) {
        let message = "Use your mouse to confirm.";
        if (Config.quickTab)
          message = "Press shift + tab or use your mouse to confirm.";
        Notifications.add("Quick restart disabled. " + message, 0, 3);
        return;
      }
      // }else{
      //   return;
      // }
    }
  }
  if (active) {
    TestStats.pushKeypressesToHistory();
    let testSeconds = TestStats.calculateTestSeconds(performance.now());
    let afkseconds = TestStats.calculateAfkSeconds(testSeconds);
    // incompleteTestSeconds += ;
    let tt = testSeconds - afkseconds;
    if (tt < 0) tt = 0;
    console.log(
      `increasing incomplete time by ${tt}s (${testSeconds}s - ${afkseconds}s afk)`
    );
    TestStats.incrementIncompleteSeconds(tt);
    TestStats.incrementRestartCount();
    if (tt > 600) {
      Notifications.add(
        `Your time typing just increased by ${Misc.roundTo2(
          tt / 60
        )} minutes. If you think this is incorrect please contact Miodec and dont refresh the website.`,
        -1
      );
    }
    // restartCount++;
  }

  if (Config.mode == "zen") {
    $("#words").empty();
  }

  if (
    PractiseWords.before.mode !== null &&
    !withSameWordset &&
    !practiseMissed
  ) {
    Notifications.add("Reverting to previous settings.", 0);
    UpdateConfig.setPunctuation(PractiseWords.before.punctuation);
    UpdateConfig.setNumbers(PractiseWords.before.numbers);
    UpdateConfig.setMode(PractiseWords.before.mode);
    PractiseWords.resetBefore();
  }

  let repeatWithPace = false;
  if (TestUI.resultVisible && Config.repeatedPace && withSameWordset) {
    repeatWithPace = true;
  }

  ManualRestart.reset();
  TestTimer.clear();
  TestStats.restart();
  corrected.reset();
  ShiftTracker.reset();
  Caret.hide();
  setActive(false);
  Replay.stopReplayRecording();
  LiveWpm.hide();
  LiveAcc.hide();
  LiveBurst.hide();
  TimerProgress.hide();
  Replay.pauseReplay();
  setBailout(false);
  PaceCaret.reset();
  $("#showWordHistoryButton").removeClass("loaded");
  $("#restartTestButton").blur();
  Funbox.resetMemoryTimer();
  RateQuotePopup.clearQuoteStats();
  if (UI.getActivePage() == "pageTest" && window.scrollY > 0)
    window.scrollTo({ top: 0, behavior: "smooth" });
  $("#wordsInput").val(" ");

  TestUI.reset();

  $("#timerNumber").css("opacity", 0);
  let el = null;
  if (TestUI.resultVisible) {
    //results are being displayed
    el = $("#result");
  } else {
    //words are being displayed
    el = $("#typingTest");
  }
  if (TestUI.resultVisible) {
    if (
      Config.randomTheme !== "off" &&
      !UI.pageTransition &&
      !Config.customTheme
    ) {
      ThemeController.randomizeTheme();
    }
  }
  TestUI.setResultVisible(false);
  UI.setPageTransition(true);
  TestUI.setTestRestarting(true);
  el.stop(true, true).animate(
    {
      opacity: 0,
    },
    125,
    async () => {
      if (UI.getActivePage() == "pageTest") Focus.set(false);
      TestUI.focusWords();
      $("#monkey .fast").stop(true, true).css("opacity", 0);
      $("#monkey").stop(true, true).css({ animationDuration: "0s" });
      $("#typingTest").css("opacity", 0).removeClass("hidden");
      $("#wordsInput").val(" ");
      let shouldQuoteRepeat = false;
      if (
        Config.mode === "quote" &&
        Config.repeatQuotes === "typing" &&
        failReason !== ""
      ) {
        shouldQuoteRepeat = true;
      }
      if (Config.funbox === "arrows") {
        UpdateConfig.setPunctuation(false, true);
        UpdateConfig.setNumbers(false, true);
      } else if (Config.funbox === "58008") {
        UpdateConfig.setNumbers(false, true);
      } else if (Config.funbox === "specials") {
        UpdateConfig.setPunctuation(false, true);
        UpdateConfig.setNumbers(false, true);
      } else if (Config.funbox === "ascii") {
        UpdateConfig.setPunctuation(false, true);
        UpdateConfig.setNumbers(false, true);
      }
      if (!withSameWordset && !shouldQuoteRepeat) {
        setRepeated(false);
        setPaceRepeat(repeatWithPace);
        setHasTab(false);
        await init();
        PaceCaret.init(nosave);
      } else {
        setRepeated(true);
        setPaceRepeat(repeatWithPace);
        setActive(false);
        Replay.stopReplayRecording();
        words.resetCurrentIndex();
        input.reset();
        if (Config.funbox === "plus_one" || Config.funbox === "plus_two") {
          Notifications.add(
            "Sorry, this funbox won't work with repeated tests.",
            0
          );
          await Funbox.activate("none");
        } else {
          await Funbox.activate();
        }
        TestUI.showWords();
        PaceCaret.init();
      }
      failReason = "";
      if (Config.mode === "quote") {
        setRepeated(false);
      }
      if (Config.keymapMode !== "off") {
        Keymap.show();
      } else {
        Keymap.hide();
      }
      document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = "0";
      document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = "100%";
      document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = "0";
      document.querySelector("#liveWpm").innerHTML = "0";
      document.querySelector("#liveAcc").innerHTML = "100%";
      document.querySelector("#liveBurst").innerHTML = "0";

      if (Config.funbox === "memory") {
        Funbox.startMemoryTimer();
        if (Config.keymapMode === "next") {
          UpdateConfig.setKeymapMode("react");
        }
      }

      let mode2 = Misc.getMode2();
      let fbtext = "";
      if (Config.funbox !== "none") {
        fbtext = " " + Config.funbox;
      }
      $(".pageTest #premidTestMode").text(
        `${Config.mode} ${mode2} ${Config.language.replace(/_/g, " ")}${fbtext}`
      );
      $(".pageTest #premidSecondsLeft").text(Config.time);

      if (Config.funbox === "layoutfluid") {
        UpdateConfig.setLayout(
          Config.customLayoutfluid
            ? Config.customLayoutfluid.split("#")[0]
            : "qwerty",
          true
        );
        UpdateConfig.setKeymapLayout(
          Config.customLayoutfluid
            ? Config.customLayoutfluid.split("#")[0]
            : "qwerty",
          true
        );
        Keymap.highlightKey(
          words
            .getCurrent()
            .substring(input.current.length, input.current.length + 1)
            .toString()
            .toUpperCase()
        );
      }

      $("#result").addClass("hidden");
      $("#testModesNotice").removeClass("hidden").css({
        opacity: 1,
      });
      // resetPaceCaret();
      $("#typingTest")
        .css("opacity", 0)
        .removeClass("hidden")
        .stop(true, true)
        .animate(
          {
            opacity: 1,
          },
          125,
          () => {
            TestUI.setTestRestarting(false);
            // resetPaceCaret();
            PbCrown.hide();
            TestTimer.clear();
            if ($("#commandLineWrapper").hasClass("hidden"))
              TestUI.focusWords();
            // ChartController.result.update();
            TestUI.updateModesNotice();
            UI.setPageTransition(false);
            // console.log(TestStats.incompleteSeconds);
            // console.log(TestStats.restartCount);
          }
        );
    }
  );
}

async function getNextWord(wordset, language, wordsBound) {
  let randomWord = wordset.randomWord();
  const previousWord = words.get(words.length - 1, true);
  const previousWord2 = words.get(words.length - 2, true);
  if (Config.mode === "quote") {
    randomWord = randomQuote.textSplit[words.length];
  } else if (
    Config.mode == "custom" &&
    !CustomText.isWordRandom &&
    !CustomText.isTimeRandom
  ) {
    randomWord = CustomText.text[words.length];
  } else if (
    Config.mode == "custom" &&
    (CustomText.isWordRandom || CustomText.isTimeRandom) &&
    (wordset.length < 3 || PractiseWords.before.mode !== null)
  ) {
    randomWord = wordset.randomWord();
  } else {
    let regenarationCount = 0; //infinite loop emergency stop button
    while (
      regenarationCount < 100 &&
      (previousWord == randomWord ||
        previousWord2 == randomWord ||
        (!Config.punctuation && randomWord == "I"))
    ) {
      regenarationCount++;
      randomWord = wordset.randomWord();
    }
  }

  if (randomWord === undefined) {
    randomWord = wordset.randomWord();
  }

  if (Config.lazyMode === true && !language.noLazyMode) {
    randomWord = LazyMode.replaceAccents(randomWord, language.accents);
  }

  randomWord = randomWord.replace(/ +/gm, " ");
  randomWord = randomWord.replace(/^ | $/gm, "");

  if (Config.funbox === "rAnDoMcAsE") {
    let randomcaseword = "";
    for (let i = 0; i < randomWord.length; i++) {
      if (i % 2 != 0) {
        randomcaseword += randomWord[i].toUpperCase();
      } else {
        randomcaseword += randomWord[i];
      }
    }
    randomWord = randomcaseword;
  } else if (Config.funbox === "capitals") {
    randomWord = Misc.capitalizeFirstLetter(randomWord);
  } else if (Config.funbox === "gibberish") {
    randomWord = Misc.getGibberish();
  } else if (Config.funbox === "arrows") {
    randomWord = Misc.getArrows();
  } else if (Config.funbox === "58008") {
    randomWord = Misc.getNumbers(7);
  } else if (Config.funbox === "specials") {
    randomWord = Misc.getSpecials();
  } else if (Config.funbox === "ascii") {
    randomWord = Misc.getASCII();
  } else if (Config.funbox === "weakspot") {
    randomWord = WeakSpot.getWord(wordset);
  }

  if (Config.punctuation) {
    randomWord = punctuateWord(
      words.get(words.length - 1),
      randomWord,
      words.length,
      wordsBound
    );
  }
  if (Config.numbers) {
    if (Math.random() < 0.1) {
      randomWord = Misc.getNumbers(4);
    }
  }

  if (Config.britishEnglish && /english/.test(Config.language)) {
    randomWord = await BritishEnglish.replace(randomWord);
  }

  return randomWord;
}

export async function init() {
  setActive(false);
  Replay.stopReplayRecording();
  words.reset();
  TestUI.setCurrentWordElementIndex(0);
  // accuracy = {
  //   correct: 0,
  //   incorrect: 0,
  // };

  input.resetHistory();
  input.resetCurrent();

  let language = await Misc.getLanguage(Config.language);
  if (language && language.name !== Config.language) {
    UpdateConfig.setLanguage("english");
  }

  if (!language) {
    UpdateConfig.setLanguage("english");
    language = await Misc.getLanguage(Config.language);
  }

  if (Config.lazyMode === true && language.noLazyMode) {
    Notifications.add("This language does not support lazy mode.", 0);
    UpdateConfig.setLazyMode(false);
  }

  let wordsBound = 100;
  if (Config.showAllLines) {
    if (Config.mode === "quote") {
      wordsBound = 100;
    } else if (Config.mode === "custom") {
      if (CustomText.isWordRandom) {
        wordsBound = CustomText.word;
      } else if (CustomText.isTimeRandom) {
        wordsBound = 100;
      } else {
        wordsBound = CustomText.text.length;
      }
    } else if (Config.mode != "time") {
      wordsBound = Config.words;
    }
  } else {
    if (Config.mode === "words" && Config.words < wordsBound) {
      wordsBound = Config.words;
    }
    if (
      Config.mode == "custom" &&
      CustomText.isWordRandom &&
      CustomText.word < wordsBound
    ) {
      wordsBound = CustomText.word;
    }
    if (Config.mode == "custom" && CustomText.isTimeRandom) {
      wordsBound = 100;
    }
    if (
      Config.mode == "custom" &&
      !CustomText.isWordRandom &&
      !CustomText.isTimeRandom &&
      CustomText.text.length < wordsBound
    ) {
      wordsBound = CustomText.text.length;
    }
  }

  if (
    (Config.mode === "custom" &&
      CustomText.isWordRandom &&
      CustomText.word == 0) ||
    (Config.mode === "custom" &&
      CustomText.isTimeRandom &&
      CustomText.time == 0)
  ) {
    wordsBound = 100;
  }

  if (Config.mode === "words" && Config.words === 0) {
    wordsBound = 100;
  }
  if (Config.funbox === "plus_one") {
    wordsBound = 2;
    if (Config.mode === "words" && Config.words < wordsBound) {
      wordsBound = Config.words;
    }
  }
  if (Config.funbox === "plus_two") {
    wordsBound = 3;
    if (Config.mode === "words" && Config.words < wordsBound) {
      wordsBound = Config.words;
    }
  }

  if (
    Config.mode == "time" ||
    Config.mode == "words" ||
    Config.mode == "custom"
  ) {
    let wordList = language.words;
    if (Config.mode == "custom") {
      wordList = CustomText.text;
    }
    const wordset = Wordset.withWords(wordList);

    if (
      (Config.funbox == "wikipedia" || Config.funbox == "poetry") &&
      Config.mode != "custom"
    ) {
      let wordCount = 0;

      // If mode is words, get as many sections as you need until the wordCount is fullfilled
      while (
        (Config.mode == "words" && Config.words >= wordCount) ||
        (Config.mode === "time" && wordCount < 100)
      ) {
        let section =
          Config.funbox == "wikipedia"
            ? await Wikipedia.getSection()
            : await Poetry.getPoem();
        for (let word of section.words) {
          if (wordCount >= Config.words && Config.mode == "words") {
            wordCount++;
            break;
          }
          wordCount++;
          words.push(word);
        }
      }
    } else {
      for (let i = 0; i < wordsBound; i++) {
        let randomWord = await getNextWord(wordset, language, wordsBound);

        if (/t/g.test(randomWord)) {
          setHasTab(true);
        }

        if (/ +/.test(randomWord)) {
          let randomList = randomWord.split(" ");
          let id = 0;
          while (id < randomList.length) {
            words.push(randomList[id]);
            id++;

            if (
              words.length == wordsBound &&
              Config.mode == "custom" &&
              CustomText.isWordRandom
            ) {
              break;
            }
          }
          if (
            Config.mode == "custom" &&
            !CustomText.isWordRandom &&
            !CustomText.isTimeRandom
          ) {
            //
          } else {
            i = words.length - 1;
          }
        } else {
          words.push(randomWord);
        }
      }
    }
  } else if (Config.mode == "quote") {
    // setLanguage(Config.language.replace(/_\d*k$/g, ""), true);

    let quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, ""));

    if (quotes.length === 0) {
      TestUI.setTestRestarting(false);
      Notifications.add(
        `No ${Config.language.replace(/_\d*k$/g, "")} quotes found`,
        0
      );
      if (firebase.auth().currentUser) {
        QuoteSubmitPopup.show(false);
      }
      UpdateConfig.setMode("words");
      restart();
      return;
    }

    let rq;
    if (Config.quoteLength != -2) {
      let quoteLengths = Config.quoteLength;
      let groupIndex;
      if (quoteLengths.length > 1) {
        groupIndex =
          quoteLengths[Math.floor(Math.random() * quoteLengths.length)];
        while (quotes.groups[groupIndex].length === 0) {
          groupIndex =
            quoteLengths[Math.floor(Math.random() * quoteLengths.length)];
        }
      } else {
        groupIndex = quoteLengths[0];
        if (quotes.groups[groupIndex].length === 0) {
          Notifications.add("No quotes found for selected quote length", 0);
          TestUI.setTestRestarting(false);
          return;
        }
      }

      rq =
        quotes.groups[groupIndex][
          Math.floor(Math.random() * quotes.groups[groupIndex].length)
        ];
      if (randomQuote != null && rq.id === randomQuote.id) {
        rq =
          quotes.groups[groupIndex][
            Math.floor(Math.random() * quotes.groups[groupIndex].length)
          ];
      }
    } else {
      quotes.groups.forEach((group) => {
        let filtered = group.filter(
          (quote) => quote.id == QuoteSearchPopup.selectedId
        );
        if (filtered.length > 0) {
          rq = filtered[0];
        }
      });
      if (rq == undefined) {
        rq = quotes.groups[0][0];
        Notifications.add("Quote Id Does Not Exist", 0);
      }
    }
    rq.text = rq.text.replace(/ +/gm, " ");
    rq.text = rq.text.replace(/t/gm, "t");
    rq.text = rq.text.replace(/\\\\n/gm, "\n");
    rq.text = rq.text.replace(/t/gm, "t");
    rq.text = rq.text.replace(/\\n/gm, "\n");
    rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
    rq.text = rq.text.replace(/…/g, "...");
    rq.text = rq.text.trim();
    rq.textSplit = rq.text.split(" ");
    rq.language = Config.language.replace(/_\d*k$/g, "");

    setRandomQuote(rq);

    let w = randomQuote.textSplit;

    wordsBound = Math.min(wordsBound, w.length);

    for (let i = 0; i < wordsBound; i++) {
      if (/t/g.test(w[i])) {
        setHasTab(true);
      }
      if (
        Config.britishEnglish &&
        Config.language.replace(/_\d*k$/g, "") === "english"
      ) {
        w[i] = await BritishEnglish.replace(w[i]);
      }

      if (Config.lazyMode === true && !language.noLazyMode) {
        w[i] = LazyMode.replaceAccents(w[i], language.accents);
      }

      words.push(w[i]);
    }
  }
  //handle right-to-left languages
  if (language.leftToRight) {
    TestUI.arrangeCharactersLeftToRight();
  } else {
    TestUI.arrangeCharactersRightToLeft();
  }
  if (language.ligatures) {
    $("#words").addClass("withLigatures");
    $("#resultWordsHistory .words").addClass("withLigatures");
    $("#resultReplay .words").addClass("withLigatures");
  } else {
    $("#words").removeClass("withLigatures");
    $("#resultWordsHistory .words").removeClass("withLigatures");
    $("#resultReplay .words").removeClass("withLigatures");
  }
  // if (Config.mode == "zen") {
  //   // Creating an empty active word element for zen mode
  //   $("#words").append('<div class="word active"></div>');
  //   $("#words").css("height", "auto");
  //   $("#wordsWrapper").css("height", "auto");
  // } else {
  if (UI.getActivePage() == "pageTest") {
    await Funbox.activate();
  }
  TestUI.showWords();
  // }
}

export function calculateWpmAndRaw() {
  let chars = 0;
  let correctWordChars = 0;
  let spaces = 0;
  //check input history
  for (let i = 0; i < input.history.length; i++) {
    let word = Config.mode == "zen" ? input.getHistory(i) : words.get(i);
    if (input.getHistory(i) == word) {
      //the word is correct
      //+1 for space
      correctWordChars += word.length;
      if (
        i < input.history.length - 1 &&
        Misc.getLastChar(input.getHistory(i)) !== "\n"
      ) {
        spaces++;
      }
    }
    chars += input.getHistory(i).length;
  }
  if (input.current !== "") {
    let word = Config.mode == "zen" ? input.current : words.getCurrent();
    //check whats currently typed
    let toAdd = {
      correct: 0,
      incorrect: 0,
      missed: 0,
    };
    for (let c = 0; c < word.length; c++) {
      if (c < input.current.length) {
        //on char that still has a word list pair
        if (input.current[c] == word[c]) {
          toAdd.correct++;
        } else {
          toAdd.incorrect++;
        }
      } else {
        //on char that is extra
        toAdd.missed++;
      }
    }
    chars += toAdd.correct;
    chars += toAdd.incorrect;
    chars += toAdd.missed;
    if (toAdd.incorrect == 0) {
      //word is correct so far, add chars
      correctWordChars += toAdd.correct;
    }
  }
  if (Config.funbox === "nospace" || Config.funbox === "arrows") {
    spaces = 0;
  }
  chars += input.current.length;
  let testSeconds = TestStats.calculateTestSeconds(performance.now());
  let wpm = Math.round(((correctWordChars + spaces) * (60 / testSeconds)) / 5);
  let raw = Math.round(((chars + spaces) * (60 / testSeconds)) / 5);
  return {
    wpm: wpm,
    raw: raw,
  };
}

export async function addWord() {
  let bound = 100;
  if (Config.funbox === "wikipedia" || Config.funbox == "poetry") {
    if (Config.mode == "time" && words.length - words.currentIndex < 20) {
      let section =
        Config.funbox == "wikipedia"
          ? await Wikipedia.getSection()
          : await Poetry.getPoem();
      let wordCount = 0;
      for (let word of section.words) {
        if (wordCount >= Config.words && Config.mode == "words") {
          break;
        }
        wordCount++;
        words.push(word);
        TestUI.addWord(word);
      }
    } else {
      return;
    }
  }

  if (Config.funbox === "plus_one") bound = 1;
  if (Config.funbox === "plus_two") bound = 2;
  if (
    words.length - input.history.length > bound ||
    (Config.mode === "words" &&
      words.length >= Config.words &&
      Config.words > 0) ||
    (Config.mode === "custom" &&
      CustomText.isWordRandom &&
      words.length >= CustomText.word &&
      CustomText.word != 0) ||
    (Config.mode === "custom" &&
      !CustomText.isWordRandom &&
      !CustomText.isTimeRandom &&
      words.length >= CustomText.text.length) ||
    (Config.mode === "quote" && words.length >= randomQuote.textSplit.length)
  )
    return;
  const language =
    Config.mode !== "custom"
      ? await Misc.getCurrentLanguage()
      : {
          //borrow the direction of the current language
          leftToRight: await Misc.getCurrentLanguage().leftToRight,
          words: CustomText.text,
        };
  const wordset = Wordset.withWords(language.words);

  let randomWord = await getNextWord(wordset, language, bound);

  let split = randomWord.split(" ");
  if (split.length > 1) {
    split.forEach((word) => {
      words.push(word);
      TestUI.addWord(word);
    });
  } else {
    words.push(randomWord);
    TestUI.addWord(randomWord);
  }
}

var retrySaving = {
  completedEvent: null,
  canRetry: false,
};

export function retrySavingResult() {
  if (!retrySaving.completedEvent) {
    Notifications.add(
      "Could not retry saving the result as the result no longer exists.",
      0,
      -1
    );
  }
  if (!retrySaving.canRetry) {
    return;
  }

  retrySaving.canRetry = false;
  $("#retrySavingResultButton").addClass("hidden");

  AccountButton.loading(true);

  Notifications.add("Retrying to save...");

  var { completedEvent } = retrySaving;

  axiosInstance
    .post("/results/add", {
      result: completedEvent,
    })
    .then((response) => {
      AccountButton.loading(false);
      Result.hideCrown();

      if (response.status !== 200) {
        Notifications.add("Result not saved. " + response.data.message, -1);
      } else {
        completedEvent._id = response.data.insertedId;
        if (response.data.isPb) {
          completedEvent.isPb = true;
        }

        DB.saveLocalResult(completedEvent);
        DB.updateLocalStats({
          time:
            completedEvent.testDuration +
            completedEvent.incompleteTestSeconds -
            completedEvent.afkDuration,
          started: TestStats.restartCount + 1,
        });

        try {
          firebase.analytics().logEvent("testCompleted", completedEvent);
        } catch (e) {
          console.log("Analytics unavailable");
        }

        if (response.data.isPb) {
          //new pb
          Result.showCrown();
          Result.updateCrown();
          DB.saveLocalPB(
            Config.mode,
            completedEvent.mode2,
            Config.punctuation,
            Config.language,
            Config.difficulty,
            Config.lazyMode,
            completedEvent.wpm,
            completedEvent.acc,
            completedEvent.rawWpm,
            completedEvent.consistency
          );
        }
      }

      $("#retrySavingResultButton").addClass("hidden");
      Notifications.add("Result saved", 1);
    })
    .catch((e) => {
      AccountButton.loading(false);
      let msg = e?.response?.data?.message ?? e.message;
      Notifications.add("Failed to save result: " + msg, -1);
      $("#retrySavingResultButton").removeClass("hidden");
      retrySaving.canRetry = true;
    });
}

function buildCompletedEvent(difficultyFailed) {
  //build completed event object
  let completedEvent = {
    wpm: undefined,
    rawWpm: undefined,
    charStats: undefined,
    acc: undefined,
    mode: Config.mode,
    mode2: undefined,
    quoteLength: -1,
    punctuation: Config.punctuation,
    numbers: Config.numbers,
    lazyMode: Config.lazyMode,
    timestamp: Date.now(),
    language: Config.language,
    restartCount: TestStats.restartCount,
    incompleteTestSeconds:
      TestStats.incompleteSeconds < 0
        ? 0
        : Misc.roundTo2(TestStats.incompleteSeconds),
    difficulty: Config.difficulty,
    blindMode: Config.blindMode,
    tags: undefined,
    keySpacing: TestStats.keypressTimings.spacing.array,
    keyDuration: TestStats.keypressTimings.duration.array,
    consistency: undefined,
    keyConsistency: undefined,
    funbox: Config.funbox,
    bailedOut: bailout,
    chartData: {
      wpm: TestStats.wpmHistory,
      raw: undefined,
      err: undefined,
    },
    customText: undefined,
    testDuration: undefined,
    afkDuration: undefined,
  };

  // stats
  let stats = TestStats.calculateStats();
  if (stats.time % 1 != 0 && Config.mode !== "time") {
    TestStats.setLastSecondNotRound();
  }
  lastTestWpm = stats.wpm;
  completedEvent.wpm = stats.wpm;
  completedEvent.rawWpm = stats.wpmRaw;
  completedEvent.charStats = [
    stats.correctChars + stats.correctSpaces,
    stats.incorrectChars,
    stats.extraChars,
    stats.missedChars,
  ];
  completedEvent.acc = stats.acc;

  // if the last second was not rounded, add another data point to the history
  if (TestStats.lastSecondNotRound && !difficultyFailed) {
    let wpmAndRaw = calculateWpmAndRaw();
    TestStats.pushToWpmHistory(wpmAndRaw.wpm);
    TestStats.pushToRawHistory(wpmAndRaw.raw);
    TestStats.pushKeypressesToHistory();
  }

  //consistency
  let rawPerSecond = TestStats.keypressPerSecond.map((f) =>
    Math.round((f.count / 5) * 60)
  );
  let stddev = Misc.stdDev(rawPerSecond);
  let avg = Misc.mean(rawPerSecond);
  let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg));
  let keyconsistencyarray = TestStats.keypressTimings.spacing.array.slice();
  keyconsistencyarray = keyconsistencyarray.splice(
    0,
    keyconsistencyarray.length - 1
  );
  let keyConsistency = Misc.roundTo2(
    Misc.kogasa(
      Misc.stdDev(keyconsistencyarray) / Misc.mean(keyconsistencyarray)
    )
  );
  if (isNaN(consistency)) {
    consistency = 0;
  }
  completedEvent.keyConsistency = keyConsistency;
  completedEvent.consistency = consistency;
  let smoothedraw = Misc.smooth(rawPerSecond, 1);
  completedEvent.chartData.raw = smoothedraw;
  completedEvent.chartData.unsmoothedRaw = rawPerSecond;

  //smoothed consistency
  let stddev2 = Misc.stdDev(smoothedraw);
  let avg2 = Misc.mean(smoothedraw);
  let smoothConsistency = Misc.roundTo2(Misc.kogasa(stddev2 / avg2));
  completedEvent.smoothConsistency = smoothConsistency;

  //wpm consistency
  let stddev3 = Misc.stdDev(completedEvent.chartData.wpm);
  let avg3 = Misc.mean(completedEvent.chartData.wpm);
  let wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3));
  completedEvent.wpmConsistency = wpmConsistency;

  completedEvent.testDuration = parseFloat(stats.time);
  completedEvent.afkDuration = TestStats.calculateAfkSeconds(
    completedEvent.testDuration
  );

  completedEvent.chartData.err = [];
  for (let i = 0; i < TestStats.keypressPerSecond.length; i++) {
    completedEvent.chartData.err.push(TestStats.keypressPerSecond[i].errors);
  }

  if (Config.mode === "quote") {
    completedEvent.quoteLength = randomQuote.group;
    completedEvent.lang = Config.language.replace(/_\d*k$/g, "");
  }

  completedEvent.mode2 = Misc.getMode2();

  if (Config.mode === "custom") {
    completedEvent.customText = {};
    completedEvent.customText.textLen = CustomText.text.length;
    completedEvent.customText.isWordRandom = CustomText.isWordRandom;
    completedEvent.customText.isTimeRandom = CustomText.isTimeRandom;
    completedEvent.customText.word =
      CustomText.word !== "" && !isNaN(CustomText.word)
        ? CustomText.word
        : null;
    completedEvent.customText.time =
      CustomText.time !== "" && !isNaN(CustomText.time)
        ? CustomText.time
        : null;
  } else {
    delete completedEvent.customText;
  }

  //tags
  let activeTagsIds = [];
  try {
    DB.getSnapshot().tags.forEach((tag) => {
      if (tag.active === true) {
        activeTagsIds.push(tag._id);
      }
    });
  } catch (e) {}
  completedEvent.tags = activeTagsIds;

  if (completedEvent.mode != "custom") delete completedEvent.customText;

  return completedEvent;
}

export async function finish(difficultyFailed = false) {
  if (!active) return;
  if (Config.mode == "zen" && input.current.length != 0) {
    input.pushHistory();
    corrected.pushHistory();
    Replay.replayGetWordsList(input.history);
  }

  TestStats.recordKeypressSpacing(); //this is needed in case there is afk time at the end - to make sure test duration makes sense

  TestUI.setResultCalculating(true);
  TestUI.setResultVisible(true);
  TestStats.setEnd(performance.now());
  setActive(false);
  Replay.stopReplayRecording();
  Focus.set(false);
  Caret.hide();
  LiveWpm.hide();
  PbCrown.hide();
  LiveAcc.hide();
  LiveBurst.hide();
  TimerProgress.hide();
  OutOfFocus.hide();
  TestTimer.clear();
  Funbox.activate("none", null);

  //need one more calculation for the last word if test auto ended
  if (TestStats.burstHistory.length !== input.getHistory().length) {
    let burst = TestStats.calculateBurst();
    TestStats.pushBurstToHistory(burst);
  }

  //remove afk from zen
  if (Config.mode == "zen" || bailout) {
    TestStats.removeAfkData();
  }

  const completedEvent = buildCompletedEvent(difficultyFailed);

  //todo check if any fields are undefined

  ///////// completed event ready

  //afk check
  let kps = TestStats.keypressPerSecond.slice(-5);
  let afkDetected = kps.every((second) => second.afk);
  if (bailout) afkDetected = false;

  let tooShort = false;
  let dontSave = false;
  //fail checks
  if (difficultyFailed) {
    Notifications.add(`Test failed - ${failReason}`, 0, 1);
    dontSave = true;
  } else if (afkDetected) {
    Notifications.add("Test invalid - AFK detected", 0);
    dontSave = true;
  } else if (isRepeated) {
    Notifications.add("Test invalid - repeated", 0);
    dontSave = true;
  } else if (
    (Config.mode === "time" &&
      completedEvent.mode2 < 15 &&
      completedEvent.mode2 > 0) ||
    (Config.mode === "time" &&
      completedEvent.mode2 == 0 &&
      completedEvent.testDuration < 15) ||
    (Config.mode === "words" &&
      completedEvent.mode2 < 10 &&
      completedEvent.mode2 > 0) ||
    (Config.mode === "words" &&
      completedEvent.mode2 == 0 &&
      completedEvent.testDuration < 15) ||
    (Config.mode === "custom" &&
      !CustomText.isWordRandom &&
      !CustomText.isTimeRandom &&
      CustomText.text.length < 10) ||
    (Config.mode === "custom" &&
      CustomText.isWordRandom &&
      !CustomText.isTimeRandom &&
      CustomText.word < 10) ||
    (Config.mode === "custom" &&
      !CustomText.isWordRandom &&
      CustomText.isTimeRandom &&
      CustomText.time < 15) ||
    (Config.mode === "zen" && completedEvent.testDuration < 15)
  ) {
    Notifications.add("Test invalid - too short", 0);
    tooShort = true;
    dontSave = true;
  } else if (completedEvent.wpm < 0 || completedEvent.wpm > 350) {
    Notifications.add("Test invalid - wpm", 0);
    TestStats.setInvalid();
    dontSave = true;
  } else if (completedEvent.acc < 75 || completedEvent.acc > 100) {
    Notifications.add("Test invalid - accuracy", 0);
    TestStats.setInvalid();
    dontSave = true;
  }

  // test is valid

  if (!dontSave) {
    TodayTracker.addSeconds(
      completedEvent.testDuration +
        (TestStats.incompleteSeconds < 0
          ? 0
          : Misc.roundTo2(TestStats.incompleteSeconds)) -
        completedEvent.afkDuration
    );
    Result.updateTodayTracker();
  }

  if (firebase.auth().currentUser == null) {
    $(".pageTest #result #rateQuoteButton").addClass("hidden");
    try {
      firebase.analytics().logEvent("testCompletedNoLogin", completedEvent);
    } catch (e) {
      console.log("Analytics unavailable");
    }
    notSignedInLastResult = completedEvent;
    dontSave = true;
  }

  Result.update(
    completedEvent,
    difficultyFailed,
    failReason,
    afkDetected,
    isRepeated,
    tooShort,
    randomQuote,
    dontSave
  );

  delete completedEvent.chartData.unsmoothedRaw;

  if (completedEvent.testDuration > 122) {
    completedEvent.chartData = "toolong";
    completedEvent.keySpacing = "toolong";
    completedEvent.keyDuration = "toolong";
    TestStats.setKeypressTimingsTooLong();
  }

  if (dontSave) {
    try {
      firebase.analytics().logEvent("testCompletedInvalid", completedEvent);
    } catch (e) {
      console.log("Analytics unavailable");
    }
    return;
  }

  // user is logged in

  if (
    Config.difficulty == "normal" ||
    ((Config.difficulty == "master" || Config.difficulty == "expert") &&
      !difficultyFailed)
  ) {
    TestStats.resetIncomplete();
  }

  completedEvent.uid = firebase.auth().currentUser.uid;
  Result.updateRateQuote(randomQuote);

  Result.updateGraphPBLine();

  AccountButton.loading(true);
  completedEvent.challenge = ChallengeContoller.verify(completedEvent);
  if (!completedEvent.challenge) delete completedEvent.challenge;
  completedEvent.hash = objecthash(completedEvent);
  axiosInstance
    .post("/results/add", {
      result: completedEvent,
    })
    .then((response) => {
      AccountButton.loading(false);
      Result.hideCrown();

      if (response.status !== 200) {
        Notifications.add("Result not saved. " + response.data.message, -1);
      } else {
        completedEvent._id = response.data.insertedId;
        if (response.data.isPb) {
          completedEvent.isPb = true;
        }

        DB.saveLocalResult(completedEvent);
        DB.updateLocalStats({
          time:
            completedEvent.testDuration +
            completedEvent.incompleteTestSeconds -
            completedEvent.afkDuration,
          started: TestStats.restartCount + 1,
        });

        try {
          firebase.analytics().logEvent("testCompleted", completedEvent);
        } catch (e) {
          console.log("Analytics unavailable");
        }

        if (response.data.isPb) {
          //new pb
          Result.showCrown();
          Result.updateCrown();
          DB.saveLocalPB(
            Config.mode,
            completedEvent.mode2,
            Config.punctuation,
            Config.language,
            Config.difficulty,
            Config.lazyMode,
            completedEvent.wpm,
            completedEvent.acc,
            completedEvent.rawWpm,
            completedEvent.consistency
          );
        }
      }

      $("#retrySavingResultButton").addClass("hidden");
    })
    .catch((e) => {
      AccountButton.loading(false);
      let msg = e?.response?.data?.message ?? e.message;
      Notifications.add("Failed to save result: " + msg, -1);
      $("#retrySavingResultButton").removeClass("hidden");

      retrySaving.completedEvent = completedEvent;
      retrySaving.canRetry = true;
    });
}

export function fail(reason) {
  failReason = reason;
  // input.pushHistory();
  // corrected.pushHistory();
  TestStats.pushKeypressesToHistory();
  finish(true);
  let testSeconds = TestStats.calculateTestSeconds(performance.now());
  let afkseconds = TestStats.calculateAfkSeconds(testSeconds);
  let tt = testSeconds - afkseconds;
  if (tt < 0) tt = 0;
  TestStats.incrementIncompleteSeconds(tt);
  TestStats.incrementRestartCount();
}

==> ./monkeytype/src/js/test/test-ui.js <==
import * as Notifications from "./notifications";
import * as ThemeColors from "./theme-colors";
import Config, * as UpdateConfig from "./config";
import * as DB from "./db";
import * as TestLogic from "./test-logic";
import * as Funbox from "./funbox";
import * as PaceCaret from "./pace-caret";
import * as CustomText from "./custom-text";
import * as Keymap from "./keymap";
import * as Caret from "./caret";
import * as CommandlineLists from "./commandline-lists";
import * as Commandline from "./commandline";
import * as OutOfFocus from "./out-of-focus";
import * as ManualRestart from "./manual-restart-tracker";
import * as PractiseWords from "./practise-words";
import * as Replay from "./replay";
import * as TestStats from "./test-stats";
import * as Misc from "./misc";
import * as TestUI from "./test-ui";
import * as ChallengeController from "./challenge-controller";
import * as RateQuotePopup from "./rate-quote-popup";
import * as UI from "./ui";
import * as TestTimer from "./test-timer";

export let currentWordElementIndex = 0;
export let resultVisible = false;
export let activeWordTop = 0;
export let testRestarting = false;
export let lineTransition = false;
export let currentTestLine = 0;
export let resultCalculating = false;

export function setResultVisible(val) {
  resultVisible = val;
}

export function setCurrentWordElementIndex(val) {
  currentWordElementIndex = val;
}

export function setActiveWordTop(val) {
  activeWordTop = val;
}

export function setTestRestarting(val) {
  testRestarting = val;
}

export function setResultCalculating(val) {
  resultCalculating = val;
}

export function reset() {
  currentTestLine = 0;
  currentWordElementIndex = 0;
}

export function focusWords() {
  if (!$("#wordsWrapper").hasClass("hidden")) {
    $("#wordsInput").focus();
  }
}

export function updateActiveElement(backspace) {
  let active = document.querySelector("#words .active");
  if (Config.mode == "zen" && backspace) {
    active.remove();
  } else if (active !== null) {
    if (Config.highlightMode == "word") {
      active.querySelectorAll("letter").forEach((e) => {
        e.classList.remove("correct");
      });
    }
    active.classList.remove("active");
  }
  try {
    let activeWord = document.querySelectorAll("#words .word")[
      currentWordElementIndex
    ];
    activeWord.classList.add("active");
    activeWord.classList.remove("error");
    activeWordTop = document.querySelector("#words .active").offsetTop;
    if (Config.highlightMode == "word") {
      activeWord.querySelectorAll("letter").forEach((e) => {
        e.classList.add("correct");
      });
    }
  } catch (e) {}
}

function getWordHTML(word) {
  let newlineafter = false;
  let retval = `<div class='word'>`;
  for (let c = 0; c < word.length; c++) {
    if (Config.funbox === "arrows") {
      if (word.charAt(c) === "↑") {
        retval += `<letter><i class="fas fa-arrow-up"></i></letter>`;
      }
      if (word.charAt(c) === "↓") {
        retval += `<letter><i class="fas fa-arrow-down"></i></letter>`;
      }
      if (word.charAt(c) === "←") {
        retval += `<letter><i class="fas fa-arrow-left"></i></letter>`;
      }
      if (word.charAt(c) === "→") {
        retval += `<letter><i class="fas fa-arrow-right"></i></letter>`;
      }
    } else if (word.charAt(c) === "t") {
      retval += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
    } else if (word.charAt(c) === "\n") {
      newlineafter = true;
      retval += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
    } else {
      retval += "<letter>" + word.charAt(c) + "</letter>";
    }
  }
  retval += "</div>";
  if (newlineafter) retval += "<div class='newline'></div>";
  return retval;
}

export function showWords() {
  $("#words").empty();

  let wordsHTML = "";
  if (Config.mode !== "zen") {
    for (let i = 0; i < TestLogic.words.length; i++) {
      wordsHTML += getWordHTML(TestLogic.words.get(i));
    }
  } else {
    wordsHTML =
      '<div class="word">word height</div><div class="word active"></div>';
  }

  $("#words").html(wordsHTML);

  $("#wordsWrapper").removeClass("hidden");
  const wordHeight = $(document.querySelector(".word")).outerHeight(true);
  const wordsHeight = $(document.querySelector("#words")).outerHeight(true);
  console.log(
    `Showing words. wordHeight: ${wordHeight}, wordsHeight: ${wordsHeight}`
  );
  if (
    Config.showAllLines &&
    Config.mode != "time" &&
    !(CustomText.isWordRandom && CustomText.word == 0) &&
    !CustomText.isTimeRandom
  ) {
    $("#words").css("height", "auto");
    $("#wordsWrapper").css("height", "auto");
    let nh = wordHeight * 3;

    if (nh > wordsHeight) {
      nh = wordsHeight;
    }
    $(".outOfFocusWarning").css("line-height", nh + "px");
  } else {
    $("#words")
      .css("height", wordHeight * 4 + "px")
      .css("overflow", "hidden");
    $("#wordsWrapper")
      .css("height", wordHeight * 3 + "px")
      .css("overflow", "hidden");
    $(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px");
  }

  if (Config.mode === "zen") {
    $(document.querySelector(".word")).remove();
  } else {
    if (Config.keymapMode === "next") {
      Keymap.highlightKey(
        TestLogic.words
          .getCurrent()
          .substring(
            TestLogic.input.current.length,
            TestLogic.input.current.length + 1
          )
          .toString()
          .toUpperCase()
      );
    }
  }

  updateActiveElement();
  Funbox.toggleScript(TestLogic.words.getCurrent());

  Caret.updatePosition();
}

export function addWord(word) {
  $("#words").append(getWordHTML(word));
}

export function flipColors(tf) {
  if (tf) {
    $("#words").addClass("flipped");
  } else {
    $("#words").removeClass("flipped");
  }
}

export function colorful(tc) {
  if (tc) {
    $("#words").addClass("colorfulMode");
  } else {
    $("#words").removeClass("colorfulMode");
  }
}

export async function screenshot() {
  let revealReplay = false;
  function revertScreenshot() {
    $("#notificationCenter").removeClass("hidden");
    $("#commandLineMobileButton").removeClass("hidden");
    $(".pageTest .ssWatermark").addClass("hidden");
    $(".pageTest .ssWatermark").text("monkeytype.com");
    $(".pageTest .buttons").removeClass("hidden");
    if (revealReplay) $("#resultReplay").removeClass("hidden");
    if (firebase.auth().currentUser == null)
      $(".pageTest .loginTip").removeClass("hidden");
  }

  if (!$("#resultReplay").hasClass("hidden")) {
    revealReplay = true;
    Replay.pauseReplay();
  }
  $("#resultReplay").addClass("hidden");
  $(".pageTest .ssWatermark").removeClass("hidden");
  $(".pageTest .ssWatermark").text(
    moment(Date.now()).format("DD MMM YYYY HH:mm") + " | monkeytype.com "
  );
  if (firebase.auth().currentUser != null) {
    $(".pageTest .ssWatermark").text(
      DB.getSnapshot().name +
        " | " +
        moment(Date.now()).format("DD MMM YYYY HH:mm") +
        " | monkeytype.com  "
    );
  }
  $(".pageTest .buttons").addClass("hidden");
  let src = $("#middle");
  var sourceX = src.position().left; /*X position from div#target*/
  var sourceY = src.position().top; /*Y position from div#target*/
  var sourceWidth = src.outerWidth(
    true
  ); /*clientWidth/offsetWidth from div#target*/
  var sourceHeight = src.outerHeight(
    true
  ); /*clientHeight/offsetHeight from div#target*/
  $("#notificationCenter").addClass("hidden");
  $("#commandLineMobileButton").addClass("hidden");
  $(".pageTest .loginTip").addClass("hidden");
  try {
    let paddingX = 50;
    let paddingY = 25;
    html2canvas(document.body, {
      backgroundColor: await ThemeColors.get("bg"),
      width: sourceWidth + paddingX * 2,
      height: sourceHeight + paddingY * 2,
      x: sourceX - paddingX,
      y: sourceY - paddingY,
    }).then(function (canvas) {
      canvas.toBlob(function (blob) {
        try {
          if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) {
            open(URL.createObjectURL(blob));
            revertScreenshot();
          } else {
            navigator.clipboard
              .write([
                new ClipboardItem(
                  Object.defineProperty({}, blob.type, {
                    value: blob,
                    enumerable: true,
                  })
                ),
              ])
              .then(() => {
                Notifications.add("Copied to clipboard", 1, 2);
                revertScreenshot();
              });
          }
        } catch (e) {
          Notifications.add(
            "Error saving image to clipboard: " + e.message,
            -1
          );
          revertScreenshot();
        }
      });
    });
  } catch (e) {
    Notifications.add("Error creating image: " + e.message, -1);
    revertScreenshot();
  }
  setTimeout(() => {
    revertScreenshot();
  }, 3000);
}

export function updateWordElement(showError = !Config.blindMode) {
  let input = TestLogic.input.current;
  let wordAtIndex;
  let currentWord;
  wordAtIndex = document.querySelector("#words .word.active");
  currentWord = TestLogic.words.getCurrent();
  let ret = "";

  let newlineafter = false;

  if (Config.mode === "zen") {
    for (let i = 0; i < TestLogic.input.current.length; i++) {
      if (TestLogic.input.current[i] === "t") {
        ret += `<letter class='tabChar correct' style="opacity: 0"><i class="fas fa-long-arrow-alt-right"></i></letter>`;
      } else if (TestLogic.input.current[i] === "\n") {
        newlineafter = true;
        ret += `<letter class='nlChar correct' style="opacity: 0"><i class="fas fa-angle-down"></i></letter>`;
      } else {
        ret += `<letter class="correct">${TestLogic.input.current[i]}</letter>`;
      }
    }
  } else {
    let correctSoFar = false;

    // slice earlier if input has trailing compose characters
    const inputWithoutComposeLength = Misc.trailingComposeChars.test(input)
      ? input.search(Misc.trailingComposeChars)
      : input.length;
    if (
      input.search(Misc.trailingComposeChars) < currentWord.length &&
      currentWord.slice(0, inputWithoutComposeLength) ===
        input.slice(0, inputWithoutComposeLength)
    ) {
      correctSoFar = true;
    }

    let wordHighlightClassString = correctSoFar ? "correct" : "incorrect";
    if (Config.blindMode) {
      wordHighlightClassString = "correct";
    }

    for (let i = 0; i < input.length; i++) {
      let charCorrect = currentWord[i] == input[i];

      let correctClass = "correct";
      if (Config.highlightMode == "off") {
        correctClass = "";
      }

      let currentLetter = currentWord[i];
      let tabChar = "";
      let nlChar = "";
      if (Config.funbox === "arrows") {
        if (currentLetter === "↑") {
          currentLetter = `<i class="fas fa-arrow-up"></i>`;
        }
        if (currentLetter === "↓") {
          currentLetter = `<i class="fas fa-arrow-down"></i>`;
        }
        if (currentLetter === "←") {
          currentLetter = `<i class="fas fa-arrow-left"></i>`;
        }
        if (currentLetter === "→") {
          currentLetter = `<i class="fas fa-arrow-right"></i>`;
        }
      } else if (currentLetter === "t") {
        tabChar = "tabChar";
        currentLetter = `<i class="fas fa-long-arrow-alt-right"></i>`;
      } else if (currentLetter === "\n") {
        nlChar = "nlChar";
        currentLetter = `<i class="fas fa-angle-down"></i>`;
      }

      if (
        Misc.trailingComposeChars.test(input) &&
        i > input.search(Misc.trailingComposeChars)
      )
        continue;

      if (charCorrect) {
        ret += `<letter class="${
          Config.highlightMode == "word"
            ? wordHighlightClassString
            : correctClass
        } ${tabChar}${nlChar}">${currentLetter}</letter>`;
      } else if (
        currentLetter !== undefined &&
        Misc.trailingComposeChars.test(input) &&
        i === input.search(Misc.trailingComposeChars)
      ) {
        ret += `<letter class="${
          Config.highlightMode == "word" ? wordHighlightClassString : ""
        } dead">${currentLetter}</letter>`;
      } else if (!showError) {
        if (currentLetter !== undefined) {
          ret += `<letter class="${
            Config.highlightMode == "word"
              ? wordHighlightClassString
              : correctClass
          } ${tabChar}${nlChar}">${currentLetter}</letter>`;
        }
      } else if (currentLetter === undefined) {
        if (!Config.hideExtraLetters) {
          let letter = input[i];
          if (letter == " " || letter == "t" || letter == "\n") {
            letter = "_";
          }
          ret += `<letter class="${
            Config.highlightMode == "word"
              ? wordHighlightClassString
              : "incorrect"
          } extra ${tabChar}${nlChar}">${letter}</letter>`;
        }
      } else {
        ret +=
          `<letter class="${
            Config.highlightMode == "word"
              ? wordHighlightClassString
              : "incorrect"
          } ${tabChar}${nlChar}">` +
          currentLetter +
          (Config.indicateTypos ? `<hint>${input[i]}</hint>` : "") +
          "</letter>";
      }
    }

    const inputWithSingleComposeLength = Misc.trailingComposeChars.test(input)
      ? input.search(Misc.trailingComposeChars) + 1
      : input.length;
    if (inputWithSingleComposeLength < currentWord.length) {
      for (let i = inputWithSingleComposeLength; i < currentWord.length; i++) {
        if (Config.funbox === "arrows") {
          if (currentWord[i] === "↑") {
            ret += `<letter><i class="fas fa-arrow-up"></i></letter>`;
          }
          if (currentWord[i] === "↓") {
            ret += `<letter><i class="fas fa-arrow-down"></i></letter>`;
          }
          if (currentWord[i] === "←") {
            ret += `<letter><i class="fas fa-arrow-left"></i></letter>`;
          }
          if (currentWord[i] === "→") {
            ret += `<letter><i class="fas fa-arrow-right"></i></letter>`;
          }
        } else if (currentWord[i] === "t") {
          ret += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
        } else if (currentWord[i] === "\n") {
          ret += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
        } else {
          ret +=
            `<letter class="${
              Config.highlightMode == "word" ? wordHighlightClassString : ""
            }">` +
            currentWord[i] +
            "</letter>";
        }
      }
    }

    if (Config.highlightMode === "letter" && Config.hideExtraLetters) {
      if (input.length > currentWord.length && !Config.blindMode) {
        $(wordAtIndex).addClass("error");
      } else if (input.length == currentWord.length) {
        $(wordAtIndex).removeClass("error");
      }
    }
  }
  wordAtIndex.innerHTML = ret;
  if (newlineafter) $("#words").append("<div class='newline'></div>");
}

export function lineJump(currentTop) {
  //last word of the line
  if (currentTestLine > 0) {
    let hideBound = currentTop;

    let toHide = [];
    let wordElements = $("#words .word");
    for (let i = 0; i < currentWordElementIndex; i++) {
      if ($(wordElements[i]).hasClass("hidden")) continue;
      let forWordTop = Math.floor(wordElements[i].offsetTop);
      if (forWordTop < hideBound - 10) {
        toHide.push($($("#words .word")[i]));
      }
    }
    const wordHeight = $(document.querySelector(".word")).outerHeight(true);
    if (Config.smoothLineScroll && toHide.length > 0) {
      lineTransition = true;
      $("#words").prepend(
        `<div class="smoothScroller" style="position: fixed;height:${wordHeight}px;width:100%"></div>`
      );
      $("#words .smoothScroller").animate(
        {
          height: 0,
        },
        TestTimer.slowTimer ? 0 : 125,
        () => {
          $("#words .smoothScroller").remove();
        }
      );
      $("#paceCaret").animate(
        {
          top: document.querySelector("#paceCaret").offsetTop - wordHeight,
        },
        TestTimer.slowTimer ? 0 : 125
      );
      $("#words").animate(
        {
          marginTop: `-${wordHeight}px`,
        },
        TestTimer.slowTimer ? 0 : 125,
        () => {
          activeWordTop = document.querySelector("#words .active").offsetTop;

          currentWordElementIndex -= toHide.length;
          lineTransition = false;
          toHide.forEach((el) => el.remove());
          $("#words").css("marginTop", "0");
        }
      );
    } else {
      toHide.forEach((el) => el.remove());
      currentWordElementIndex -= toHide.length;
      $("#paceCaret").css({
        top: document.querySelector("#paceCaret").offsetTop - wordHeight,
      });
    }
  }
  currentTestLine++;
}

export function updateModesNotice() {
  let anim = false;
  if ($(".pageTest #testModesNotice").text() === "") anim = true;

  $(".pageTest #testModesNotice").empty();

  if (TestLogic.isRepeated && Config.mode !== "quote") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button restart" style="color:var(--error-color);"><i class="fas fa-sync-alt"></i>repeated</div>`
    );
  }

  if (TestLogic.hasTab) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button"><i class="fas fa-long-arrow-alt-right"></i>shift + tab to restart</div>`
    );
  }

  if (ChallengeController.active) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsChallenges"><i class="fas fa-award"></i>${ChallengeController.active.display}</div>`
    );
  }

  if (Config.mode === "zen") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button"><i class="fas fa-poll"></i>shift + enter to finish zen </div>`
    );
  }

  // /^[0-9a-zA-Z_.-]+$/.test(name);

  if (
    (/_\d+k$/g.test(Config.language) ||
      /code_/g.test(Config.language) ||
      Config.language == "english_commonly_misspelled") &&
    Config.mode !== "quote"
  ) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsLanguages"><i class="fas fa-globe-americas"></i>${Config.language.replace(
        /_/g,
        " "
      )}</div>`
    );
  }

  if (Config.difficulty === "expert") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star-half-alt"></i>expert</div>`
    );
  } else if (Config.difficulty === "master") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star"></i>master</div>`
    );
  }

  if (Config.blindMode) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button blind"><i class="fas fa-eye-slash"></i>blind</div>`
    );
  }

  if (Config.lazyMode) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsLazyMode"><i class="fas fa-couch"></i>lazy</div>`
    );
  }

  if (
    Config.paceCaret !== "off" ||
    (Config.repeatedPace && TestLogic.isPaceRepeat)
  ) {
    let speed = "";
    try {
      speed = ` (${Math.round(PaceCaret.settings.wpm)} wpm)`;
    } catch {}
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsPaceCaret"><i class="fas fa-tachometer-alt"></i>${
        Config.paceCaret === "average"
          ? "average"
          : Config.paceCaret === "pb"
          ? "pb"
          : "custom"
      } pace${speed}</div>`
    );
  }

  if (Config.minWpm !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsMinWpm"><i class="fas fa-bomb"></i>min ${Config.minWpmCustomSpeed} wpm</div>`
    );
  }

  if (Config.minAcc !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsMinAcc"><i class="fas fa-bomb"></i>min ${Config.minAccCustom}% acc</div>`
    );
  }

  if (Config.minBurst !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsMinBurst"><i class="fas fa-bomb"></i>min ${
        Config.minBurstCustomSpeed
      } burst ${Config.minBurst === "flex" ? "(flex)" : ""}</div>`
    );
  }

  if (Config.funbox !== "none") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsFunbox"><i class="fas fa-gamepad"></i>${Config.funbox.replace(
        /_/g,
        " "
      )}</div>`
    );
  }

  if (Config.confidenceMode === "on") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>confidence</div>`
    );
  }
  if (Config.confidenceMode === "max") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>max confidence</div>`
    );
  }

  if (Config.stopOnError != "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsStopOnError"><i class="fas fa-hand-paper"></i>stop on ${Config.stopOnError}</div>`
    );
  }

  if (Config.layout !== "default") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsLayouts"><i class="fas fa-keyboard"></i>emulating ${Config.layout.replace(
        /_/g,
        " "
      )}</div>`
    );
  }

  if (Config.oppositeShiftMode !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsOppositeShiftMode"><i class="fas fa-exchange-alt"></i>opposite shift${
        Config.oppositeShiftMode === "keymap" ? " (keymap)" : ""
      }</div>`
    );
  }

  let tagsString = "";
  try {
    DB.getSnapshot().tags.forEach((tag) => {
      if (tag.active === true) {
        tagsString += tag.name + ", ";
      }
    });

    if (tagsString !== "") {
      $(".pageTest #testModesNotice").append(
        `<div class="text-button" commands="commandsTags"><i class="fas fa-tag"></i>${tagsString.substring(
          0,
          tagsString.length - 2
        )}</div>`
      );
    }
  } catch {}

  if (anim) {
    $(".pageTest #testModesNotice")
      .css("transition", "none")
      .css("opacity", 0)
      .animate(
        {
          opacity: 1,
        },
        125,
        () => {
          $(".pageTest #testModesNotice").css("transition", ".125s");
        }
      );
  }
}

export function arrangeCharactersRightToLeft() {
  $("#words").addClass("rightToLeftTest");
  $("#resultWordsHistory .words").addClass("rightToLeftTest");
  $("#resultReplay .words").addClass("rightToLeftTest");
}

export function arrangeCharactersLeftToRight() {
  $("#words").removeClass("rightToLeftTest");
  $("#resultWordsHistory .words").removeClass("rightToLeftTest");
  $("#resultReplay .words").removeClass("rightToLeftTest");
}

async function loadWordsHistory() {
  $("#resultWordsHistory .words").empty();
  let wordsHTML = "";
  for (let i = 0; i < TestLogic.input.history.length + 2; i++) {
    let input = TestLogic.input.getHistory(i);
    let word = TestLogic.words.get(i);
    let wordEl = "";
    try {
      if (input === "") throw new Error("empty input word");
      if (
        TestLogic.corrected.getHistory(i) !== undefined &&
        TestLogic.corrected.getHistory(i) !== ""
      ) {
        wordEl = `<div class='word' burst="${
          TestStats.burstHistory[i]
        }" input="${TestLogic.corrected
          .getHistory(i)
          .replace(/"/g, "&quot;")
          .replace(/ /g, "_")}">`;
      } else {
        wordEl = `<div class='word' burst="${
          TestStats.burstHistory[i]
        }" input="${input.replace(/"/g, "&quot;").replace(/ /g, "_")}">`;
      }
      if (i === TestLogic.input.history.length - 1) {
        //last word
        let wordstats = {
          correct: 0,
          incorrect: 0,
          missed: 0,
        };
        let length = Config.mode == "zen" ? input.length : word.length;
        for (let c = 0; c < length; c++) {
          if (c < input.length) {
            //on char that still has a word list pair
            if (Config.mode == "zen" || input[c] == word[c]) {
              wordstats.correct++;
            } else {
              wordstats.incorrect++;
            }
          } else {
            //on char that is extra
            wordstats.missed++;
          }
        }
        if (wordstats.incorrect !== 0 || Config.mode !== "time") {
          if (Config.mode != "zen" && input !== word) {
            wordEl = `<div class='word error' burst="${
              TestStats.burstHistory[i]
            }" input="${input.replace(/"/g, "&quot;").replace(/ /g, "_")}">`;
          }
        }
      } else {
        if (Config.mode != "zen" && input !== word) {
          wordEl = `<div class='word error' burst="${
            TestStats.burstHistory[i]
          }" input="${input.replace(/"/g, "&quot;").replace(/ /g, "_")}">`;
        }
      }

      let loop;
      if (Config.mode == "zen" || input.length > word.length) {
        //input is longer - extra characters possible (loop over input)
        loop = input.length;
      } else {
        //input is shorter or equal (loop over word list)
        loop = word.length;
      }

      for (let c = 0; c < loop; c++) {
        let correctedChar;
        try {
          correctedChar = TestLogic.corrected.getHistory(i)[c];
        } catch (e) {
          correctedChar = undefined;
        }
        let extraCorrected = "";
        if (
          c + 1 === loop &&
          TestLogic.corrected.getHistory(i) !== undefined &&
          TestLogic.corrected.getHistory(i).length > input.length
        ) {
          extraCorrected = "extraCorrected";
        }
        if (Config.mode == "zen" || word[c] !== undefined) {
          if (Config.mode == "zen" || input[c] === word[c]) {
            if (correctedChar === input[c] || correctedChar === undefined) {
              wordEl += `<letter class="correct ${extraCorrected}">${input[c]}</letter>`;
            } else {
              wordEl +=
                `<letter class="corrected ${extraCorrected}">` +
                input[c] +
                "</letter>";
            }
          } else {
            if (input[c] === TestLogic.input.current) {
              wordEl +=
                `<letter class='correct ${extraCorrected}'>` +
                word[c] +
                "</letter>";
            } else if (input[c] === undefined) {
              wordEl += "<letter>" + word[c] + "</letter>";
            } else {
              wordEl +=
                `<letter class="incorrect ${extraCorrected}">` +
                word[c] +
                "</letter>";
            }
          }
        } else {
          wordEl += '<letter class="incorrect extra">' + input[c] + "</letter>";
        }
      }
      wordEl += "</div>";
    } catch (e) {
      try {
        wordEl = "<div class='word'>";
        for (let c = 0; c < word.length; c++) {
          wordEl += "<letter>" + word[c] + "</letter>";
        }
        wordEl += "</div>";
      } catch {}
    }
    wordsHTML += wordEl;
  }
  $("#resultWordsHistory .words").html(wordsHTML);
  $("#showWordHistoryButton").addClass("loaded");
  return true;
}

export function toggleResultWords() {
  if (resultVisible) {
    if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) {
      //show

      if (!$("#showWordHistoryButton").hasClass("loaded")) {
        $("#words").html(
          `<div class="preloader"><i class="fas fa-fw fa-spin fa-circle-notch"></i></div>`
        );
        loadWordsHistory().then(() => {
          if (Config.burstHeatmap) {
            TestUI.applyBurstHeatmap();
          }
          $("#resultWordsHistory")
            .removeClass("hidden")
            .css("display", "none")
            .slideDown(250, () => {
              if (Config.burstHeatmap) {
                TestUI.applyBurstHeatmap();
              }
            });
        });
      } else {
        if (Config.burstHeatmap) {
          TestUI.applyBurstHeatmap();
        }
        $("#resultWordsHistory")
          .removeClass("hidden")
          .css("display", "none")
          .slideDown(250);
      }
    } else {
      //hide

      $("#resultWordsHistory").slideUp(250, () => {
        $("#resultWordsHistory").addClass("hidden");
      });
    }
  }
}

export function applyBurstHeatmap() {
  if (Config.burstHeatmap) {
    $("#resultWordsHistory .heatmapLegend").removeClass("hidden");

    let burstlist = [...TestStats.burstHistory];

    burstlist = burstlist.filter((x) => x !== Infinity);
    burstlist = burstlist.filter((x) => x < 350);

    if (
      TestLogic.input.getHistory(TestLogic.input.getHistory().length - 1)
        .length !== TestLogic.words.getCurrent()?.length
    ) {
      burstlist = burstlist.splice(0, burstlist.length - 1);
    }

    let median = Misc.median(burstlist);
    let adatm = [];
    burstlist.forEach((burst) => {
      adatm.push(Math.abs(median - burst));
    });
    let step = Misc.mean(adatm);
    let steps = [
      {
        val: 0,
        class: "heatmap-0",
      },
      {
        val: median - step * 1.5,
        class: "heatmap-1",
      },
      {
        val: median - step * 0.5,
        class: "heatmap-2",
      },
      {
        val: median + step * 0.5,
        class: "heatmap-3",
      },
      {
        val: median + step * 1.5,
        class: "heatmap-4",
      },
    ];
    $("#resultWordsHistory .words .word").each((index, word) => {
      let wordBurstVal = parseInt($(word).attr("burst"));
      let cls = "";
      steps.forEach((step) => {
        if (wordBurstVal > step.val) cls = step.class;
      });
      $(word).addClass(cls);
    });
  } else {
    $("#resultWordsHistory .heatmapLegend").addClass("hidden");
    $("#resultWordsHistory .words .word").removeClass("heatmap-0");
    $("#resultWordsHistory .words .word").removeClass("heatmap-1");
    $("#resultWordsHistory .words .word").removeClass("heatmap-2");
    $("#resultWordsHistory .words .word").removeClass("heatmap-3");
    $("#resultWordsHistory .words .word").removeClass("heatmap-4");
  }
}

export function highlightBadWord(index, showError) {
  if (!showError) return;
  $($("#words .word")[index]).addClass("error");
}

$(document.body).on("click", "#saveScreenshotButton", () => {
  screenshot();
});

$(document).on("click", "#testModesNotice .text-button.restart", (event) => {
  TestLogic.restart();
});

$(document).on("click", "#testModesNotice .text-button.blind", (event) => {
  UpdateConfig.toggleBlindMode();
});

$(".pageTest #copyWordsListButton").click(async (event) => {
  try {
    let words;
    if (Config.mode == "zen") {
      words = TestLogic.input.history.join(" ");
    } else {
      words = TestLogic.words
        .get()
        .slice(0, TestLogic.input.history.length)
        .join(" ");
    }
    await navigator.clipboard.writeText(words);
    Notifications.add("Copied to clipboard", 0, 2);
  } catch (e) {
    Notifications.add("Could not copy to clipboard: " + e, -1);
  }
});

$(".pageTest #rateQuoteButton").click(async (event) => {
  RateQuotePopup.show(TestLogic.randomQuote);
});

$(".pageTest #toggleBurstHeatmap").click(async (event) => {
  UpdateConfig.setBurstHeatmap(!Config.burstHeatmap);
});

$(".pageTest .loginTip .link").click(async (event) => {
  UI.changePage("login");
});

$(document).on("mouseleave", "#resultWordsHistory .words .word", (e) => {
  $(".wordInputAfter").remove();
});

$("#wpmChart").on("mouseleave", (e) => {
  $(".wordInputAfter").remove();
});

$(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => {
  if (resultVisible) {
    let input = $(e.currentTarget).attr("input");
    let burst = $(e.currentTarget).attr("burst");
    if (input != undefined)
      $(e.currentTarget).append(
        `<div class="wordInputAfter">
          <div class="text">
          ${input
            .replace(/t/g, "_")
            .replace(/\n/g, "_")
            .replace(/</g, "&lt")
            .replace(/>/g, "&gt")}
          </div>
          <div class="speed">
          ${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${
          Config.alwaysShowCPM ? "cpm" : "wpm"
        }
          </div>
          </div>`
      );
  }
});

$(document).on("click", "#testModesNotice .text-button", (event) => {
  // console.log("CommandlineLists."+$(event.currentTarget).attr("commands"));
  let commands = CommandlineLists.getList(
    $(event.currentTarget).attr("commands")
  );
  let func = $(event.currentTarget).attr("function");
  if (commands !== undefined) {
    if ($(event.currentTarget).attr("commands") === "commandsTags") {
      CommandlineLists.updateTagCommands();
    }
    CommandlineLists.pushCurrent(commands);
    Commandline.show();
  } else if (func != undefined) {
    eval(func);
  }
});

$("#wordsInput").on("focus", () => {
  if (!resultVisible && Config.showOutOfFocusWarning) {
    OutOfFocus.hide();
  }
  Caret.show(TestLogic.input.current);
});

$("#wordsInput").on("focusout", () => {
  if (!resultVisible && Config.showOutOfFocusWarning) {
    OutOfFocus.show();
  }
  Caret.hide();
});

$(document).on("keypress", "#restartTestButton", (event) => {
  if (event.key == "Enter") {
    ManualRestart.reset();
    if (
      TestLogic.active &&
      Config.repeatQuotes === "typing" &&
      Config.mode === "quote"
    ) {
      TestLogic.restart(true);
    } else {
      TestLogic.restart();
    }
  }
});

$(document.body).on("click", "#restartTestButton", () => {
  ManualRestart.set();
  if (resultCalculating) return;
  if (
    TestLogic.active &&
    Config.repeatQuotes === "typing" &&
    Config.mode === "quote"
  ) {
    TestLogic.restart(true);
  } else {
    TestLogic.restart();
  }
});

$(document.body).on(
  "click",
  "#retrySavingResultButton",
  TestLogic.retrySavingResult
);

$(document).on("keypress", "#practiseWordsButton", (event) => {
  if (event.keyCode == 13) {
    PractiseWords.showPopup(true);
  }
});

$(document.body).on("click", "#practiseWordsButton", () => {
  // PractiseWords.init();
  PractiseWords.showPopup();
});

$(document).on("keypress", "#nextTestButton", (event) => {
  if (event.keyCode == 13) {
    TestLogic.restart();
  }
});

$(document.body).on("click", "#nextTestButton", () => {
  ManualRestart.set();
  TestLogic.restart();
});

$(document).on("keypress", "#showWordHistoryButton", (event) => {
  if (event.keyCode == 13) {
    toggleResultWords();
  }
});

$(document.body).on("click", "#showWordHistoryButton", () => {
  toggleResultWords();
});

$(document.body).on("click", "#restartTestButtonWithSameWordset", () => {
  if (Config.mode == "zen") {
    Notifications.add("Repeat test disabled in zen mode");
    return;
  }
  ManualRestart.set();
  TestLogic.restart(true);
});

$(document).on("keypress", "#restartTestButtonWithSameWordset", (event) => {
  if (Config.mode == "zen") {
    Notifications.add("Repeat test disabled in zen mode");
    return;
  }
  if (event.keyCode == 13) {
    TestLogic.restart(true);
  }
});

$("#wordsWrapper").on("click", () => {
  focusWords();
});

==> ./monkeytype/src/js/test/lazy-mode.js <==
let accents = [
  ["áàâäåãąą́āą̄ă", "a"],
  ["éèêëẽęę́ēę̄ėě", "e"],
  ["íìîïĩįį́īį̄", "i"],
  ["óòôöøõóōǫǫ́ǭő", "o"],
  ["úùûüŭũúūůű", "u"],
  ["ńň", "n"],
  ["çĉčć", "c"],
  ["ř", "r"],
  ["ď", "d"],
  ["ťț", "t"],
  ["æ", "ae"],
  ["œ", "oe"],
  ["ẅ", "w"],
  ["ĝğg̃", "g"],
  ["ĥ", "h"],
  ["ĵ", "j"],
  ["ń", "n"],
  ["ŝśšș", "s"],
  ["żźž", "z"],
  ["ÿỹýÿŷ", "y"],
  ["ł", "l"],
  ["أإآ", "ا"],
  ["َ", ""],
  ["ُ", ""],
  ["ِ", ""],
  ["ْ", ""],
  ["ً", ""],
  ["ٌ", ""],
  ["ٍ", ""],
  ["ّ", ""],
];

export function replaceAccents(word, accentsOverride) {
  let newWord = word;
  if (!accents && !accentsOverride) return newWord;
  let regex;
  let list = accentsOverride || accents;
  for (let i = 0; i < list.length; i++) {
    regex = new RegExp(`[${list[i][0]}]`, "gi");
    newWord = newWord.replace(regex, list[i][1]);
  }
  return newWord;
}

==> ./monkeytype/src/js/test/british-english.js <==
import { capitalizeFirstLetter } from "./misc";

let list = null;

export async function getList() {
  if (list == null) {
    return $.getJSON("languages/britishenglish.json", function (data) {
      list = data;
      return list;
    });
  } else {
    return list;
  }
}

export async function replace(word) {
  let list = await getList();
  let replacement = list.find((a) =>
    word.match(RegExp(`^([\\W]*${a[0]}[\\W]*)$`, "gi"))
  );
  return replacement
    ? word.replace(
        RegExp(`^(?:([\\W]*)(${replacement[0]})([\\W]*))$`, "gi"),
        (_, $1, $2, $3) =>
          $1 +
          ($2.charAt(0) === $2.charAt(0).toUpperCase()
            ? $2 === $2.toUpperCase()
              ? replacement[1].toUpperCase()
              : capitalizeFirstLetter(replacement[1])
            : replacement[1]) +
          $3
      )
    : word;
}

==> ./monkeytype/src/js/test/custom-text.js <==
export let text = "The quick brown fox jumps over the lazy dog".split(" ");
export let isWordRandom = false;
export let isTimeRandom = false;
export let word = "";
export let time = "";
export let delimiter = " ";

export function setText(txt) {
  text = txt;
}

export function setIsWordRandom(val) {
  isWordRandom = val;
}

export function setIsTimeRandom(val) {
  isTimeRandom = val;
}

export function setTime(val) {
  time = val;
}

export function setWord(val) {
  word = val;
}

export function setDelimiter(val) {
  delimiter = val;
}

==> ./monkeytype/src/js/test/live-burst.js <==
import Config from "./config";
import * as TestLogic from "./test-logic";

export function update(burst) {
  let number = burst;
  if (Config.blindMode) {
    number = 0;
  }
  document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = number;
  document.querySelector("#liveBurst").innerHTML = number;
}

export function show() {
  if (!Config.showLiveBurst) return;
  if (!TestLogic.active) return;
  if (Config.timerStyle === "mini") {
    if (!$("#miniTimerAndLiveWpm .burst").hasClass("hidden")) return;
    $("#miniTimerAndLiveWpm .burst")
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: Config.timerOpacity,
        },
        125
      );
  } else {
    if (!$("#liveBurst").hasClass("hidden")) return;
    $("#liveBurst").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: Config.timerOpacity,
      },
      125
    );
  }
}

export function hide() {
  $("#liveBurst").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#liveBurst").addClass("hidden");
    }
  );
  $("#miniTimerAndLiveWpm .burst").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#miniTimerAndLiveWpm .burst").addClass("hidden");
    }
  );
}

==> ./monkeytype/src/js/test/live-wpm.js <==
import Config from "./config";
import * as TestLogic from "./test-logic";

let liveWpmElement = document.querySelector("#liveWpm");
let miniLiveWpmElement = document.querySelector("#miniTimerAndLiveWpm .wpm");

export function update(wpm, raw) {
  // if (!TestLogic.active || !Config.showLiveWpm) {
  //   hideLiveWpm();
  // } else {
  //   showLiveWpm();
  // }
  let number = wpm;
  if (Config.blindMode) {
    number = raw;
  }
  if (Config.alwaysShowCPM) {
    number = Math.round(number * 5);
  }
  miniLiveWpmElement.innerHTML = number;
  liveWpmElement.innerHTML = number;
}

export function show() {
  if (!Config.showLiveWpm) return;
  if (!TestLogic.active) return;
  if (Config.timerStyle === "mini") {
    // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity);
    if (!$("#miniTimerAndLiveWpm .wpm").hasClass("hidden")) return;
    $("#miniTimerAndLiveWpm .wpm")
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: Config.timerOpacity,
        },
        125
      );
  } else {
    // $("#liveWpm").css("opacity", Config.timerOpacity);
    if (!$("#liveWpm").hasClass("hidden")) return;
    $("#liveWpm").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: Config.timerOpacity,
      },
      125
    );
  }
}

export function hide() {
  $("#liveWpm").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#liveWpm").addClass("hidden");
    }
  );
  $("#miniTimerAndLiveWpm .wpm").animate(
    {
      opacity: Config.timerOpacity,
    },
    125,
    () => {
      $("#miniTimerAndLiveWpm .wpm").addClass("hidden");
    }
  );
}

==> ./monkeytype/src/js/test/focus.js <==
import * as Caret from "./caret";
import * as UI from "./ui";

let state = false;

export function set(foc, withCursor = false) {
  if (foc && !state) {
    state = true;
    Caret.stopAnimation();
    $("#top").addClass("focus");
    $("#bottom").addClass("focus");
    if (!withCursor) $("body").css("cursor", "none");
    $("#middle").addClass("focus");
  } else if (!foc && state) {
    state = false;
    Caret.startAnimation();
    $("#top").removeClass("focus");
    $("#bottom").removeClass("focus");
    $("body").css("cursor", "default");
    $("#middle").removeClass("focus");
  }
}

$(document).mousemove(function (event) {
  if (!state) return;
  if (UI.getActivePage() == "pageLoading") return;
  if (UI.getActivePage() == "pageAccount" && state == true) return;
  if (
    $("#top").hasClass("focus") &&
    (event.originalEvent.movementX > 0 || event.originalEvent.movementY > 0)
  ) {
    set(false);
  }
});

==> ./monkeytype/src/js/test/today-tracker.js <==
import * as Misc from "./misc";
import * as DB from "./db";

let seconds = 0;
let addedAllToday = false;
let dayToday = null;

export function addSeconds(s) {
  if (addedAllToday) {
    let nowDate = new Date();
    nowDate = nowDate.getDate();
    if (nowDate > dayToday) {
      seconds = s;
      return;
    }
  }
  seconds += s;
}

export function getString() {
  let secString = Misc.secondsToString(Math.round(seconds), true, true);
  return secString + (addedAllToday === true ? " today" : " session");
}

export async function addAllFromToday() {
  let todayDate = new Date();
  todayDate.setSeconds(0);
  todayDate.setMinutes(0);
  todayDate.setHours(0);
  todayDate.setMilliseconds(0);
  dayToday = todayDate.getDate();
  todayDate = todayDate.getTime();

  seconds = 0;

  let results = await DB.getSnapshot().results;

  results.forEach((result) => {
    let resultDate = new Date(result.timestamp);
    resultDate.setSeconds(0);
    resultDate.setMinutes(0);
    resultDate.setHours(0);
    resultDate.setMilliseconds(0);
    resultDate = resultDate.getTime();

    if (resultDate >= todayDate) {
      seconds +=
        result.testDuration + result.incompleteTestSeconds - result.afkDuration;
    }
  });

  addedAllToday = true;
}

==> ./monkeytype/src/js/test/wikipedia.js <==
import * as Loader from "./loader";
import Config from "./config";
import * as Misc from "./misc";

export class Section {
  constructor(title, author, words) {
    this.title = title;
    this.author = author;
    this.words = words;
  }
}

export async function getTLD(languageGroup) {
  // language group to tld
  switch (languageGroup.name) {
    case "english":
      return "en";

    case "spanish":
      return "es";

    case "french":
      return "fr";

    case "german":
      return "de";

    case "portuguese":
      return "pt";

    case "italian":
      return "it";

    case "dutch":
      return "nl";

    default:
      return "en";
  }
}

export async function getSection() {
  // console.log("Getting section");
  Loader.show();

  // get TLD for wikipedia according to language group
  let urlTLD = "en";
  let currentLanguageGroup = await Misc.findCurrentGroup(Config.language);
  urlTLD = await getTLD(currentLanguageGroup);

  const randomPostURL = `https://${urlTLD}.wikipedia.org/api/rest_v1/page/random/summary`;
  var sectionObj = {};
  var randomPostReq = await fetch(randomPostURL);
  var pageid = 0;

  if (randomPostReq.status == 200) {
    let postObj = await randomPostReq.json();
    sectionObj.title = postObj.title;
    sectionObj.author = postObj.author;
    pageid = postObj.pageid;
  }

  return new Promise((res, rej) => {
    if (randomPostReq.status != 200) {
      Loader.hide();
      rej(randomPostReq.status);
    }

    const sectionURL = `https://${urlTLD}.wikipedia.org/w/api.php?action=query&format=json&pageids=${pageid}&prop=extracts&exintro=true&origin=*`;

    var sectionReq = new XMLHttpRequest();
    sectionReq.onload = () => {
      if (sectionReq.readyState == 4) {
        if (sectionReq.status == 200) {
          let sectionText = JSON.parse(sectionReq.responseText).query.pages[
            pageid.toString()
          ].extract;
          let words = [];

          // Remove double whitespaces and finally trailing whitespaces.
          sectionText = sectionText.replace(/<\/p><p>+/g, " ");
          sectionText = $("<div/>").html(sectionText).text();

          sectionText = sectionText.replace(/\s+/g, " ");
          sectionText = sectionText.trim();

          // // Add spaces
          // sectionText = sectionText.replace(/[a-zA-Z0-9]{3,}\.[a-zA-Z]/g, (x) =>
          //   x.replace(/\./, ". ")
          // );

          sectionText.split(" ").forEach((word) => {
            words.push(word);
          });

          let section = new Section(sectionObj.title, sectionObj.author, words);
          Loader.hide();
          res(section);
        } else {
          Loader.hide();
          rej(sectionReq.status);
        }
      }
    };
    sectionReq.open("GET", sectionURL);
    sectionReq.send();
  });
}

==> ./monkeytype/src/js/test/timer-progress.js <==
import Config from "./config";
import * as CustomText from "./custom-text";
import * as Misc from "./misc";
import * as TestLogic from "./test-logic";
import * as TestTimer from "./test-timer";

export function show() {
  let op = Config.showTimerProgress ? Config.timerOpacity : 0;
  if (Config.mode != "zen" && Config.timerStyle === "bar") {
    $("#timerWrapper").stop(true, true).removeClass("hidden").animate(
      {
        opacity: op,
      },
      125
    );
  } else if (Config.timerStyle === "text") {
    $("#timerNumber")
      .stop(true, true)
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: op,
        },
        125
      );
  } else if (Config.mode == "zen" || Config.timerStyle === "mini") {
    if (op > 0) {
      $("#miniTimerAndLiveWpm .time")
        .stop(true, true)
        .removeClass("hidden")
        .animate(
          {
            opacity: op,
          },
          125
        );
    }
  }
}

export function hide() {
  $("#timerWrapper").stop(true, true).animate(
    {
      opacity: 0,
    },
    125
  );
  $("#miniTimerAndLiveWpm .time")
    .stop(true, true)
    .animate(
      {
        opacity: 0,
      },
      125,
      () => {
        $("#miniTimerAndLiveWpm .time").addClass("hidden");
      }
    );
  $("#timerNumber").stop(true, true).animate(
    {
      opacity: 0,
    },
    125
  );
}

export function restart() {
  if (Config.timerStyle === "bar") {
    if (Config.mode === "time") {
      $("#timer").stop(true, true).animate(
        {
          width: "100vw",
        },
        0
      );
    } else if (Config.mode === "words" || Config.mode === "custom") {
      $("#timer").stop(true, true).animate(
        {
          width: "0vw",
        },
        0
      );
    }
  }
}

let timerNumberElement = document.querySelector("#timerNumber");
let miniTimerNumberElement = document.querySelector(
  "#miniTimerAndLiveWpm .time"
);

export function update() {
  let time = TestTimer.time;
  if (
    Config.mode === "time" ||
    (Config.mode === "custom" && CustomText.isTimeRandom)
  ) {
    let maxtime = Config.time;
    if (Config.mode === "custom" && CustomText.isTimeRandom) {
      maxtime = CustomText.time;
    }
    if (Config.timerStyle === "bar") {
      let percent = 100 - ((time + 1) / maxtime) * 100;
      $("#timer")
        .stop(true, true)
        .animate(
          {
            width: percent + "vw",
          },
          TestTimer.slowTimer ? 0 : 1000,
          "linear"
        );
    } else if (Config.timerStyle === "text") {
      let displayTime = Misc.secondsToString(maxtime - time);
      if (maxtime === 0) {
        displayTime = Misc.secondsToString(time);
      }
      timerNumberElement.innerHTML = "<div>" + displayTime + "</div>";
    } else if (Config.timerStyle === "mini") {
      let displayTime = Misc.secondsToString(maxtime - time);
      if (maxtime === 0) {
        displayTime = Misc.secondsToString(time);
      }
      miniTimerNumberElement.innerHTML = displayTime;
    }
  } else if (
    Config.mode === "words" ||
    Config.mode === "custom" ||
    Config.mode === "quote"
  ) {
    let outof = TestLogic.words.length;
    if (Config.mode === "words") {
      outof = Config.words;
    }
    if (Config.mode === "custom") {
      if (CustomText.isWordRandom) {
        outof = CustomText.word;
      } else {
        outof = CustomText.text.length;
      }
    }
    if (Config.mode === "quote") {
      outof = TestLogic?.randomQuote?.textSplit?.length ?? 1;
    }
    if (Config.timerStyle === "bar") {
      let percent = Math.floor(
        ((TestLogic.words.currentIndex + 1) / outof) * 100
      );
      $("#timer")
        .stop(true, true)
        .animate(
          {
            width: percent + "vw",
          },
          TestTimer.slowTimer ? 0 : 250
        );
    } else if (Config.timerStyle === "text") {
      if (outof === 0) {
        timerNumberElement.innerHTML =
          "<div>" + `${TestLogic.input.history.length}` + "</div>";
      } else {
        timerNumberElement.innerHTML =
          "<div>" + `${TestLogic.input.history.length}/${outof}` + "</div>";
      }
    } else if (Config.timerStyle === "mini") {
      if (Config.words === 0) {
        miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`;
      } else {
        miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}/${outof}`;
      }
    }
  } else if (Config.mode == "zen") {
    if (Config.timerStyle === "text") {
      timerNumberElement.innerHTML =
        "<div>" + `${TestLogic.input.history.length}` + "</div>";
    } else {
      miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`;
    }
  }
}

export function updateStyle() {
  if (!TestLogic.active) return;
  hide();
  update();
  setTimeout(() => {
    show();
  }, 125);
}

==> ./monkeytype/src/js/test/pb-crown.js <==
export function hide() {
  $("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden");
}

export function show() {
  $("#result .stats .wpm .crown")
    .removeClass("hidden")
    .css("opacity", "0")
    .animate(
      {
        opacity: 1,
      },
      250,
      "easeOutCubic"
    );
}

==> ./monkeytype/src/js/test/out-of-focus.js <==
import * as Misc from "./misc";

let outOfFocusTimeouts = [];

export function hide() {
  $("#words").css("transition", "none").removeClass("blurred");
  $(".outOfFocusWarning").addClass("hidden");
  Misc.clearTimeouts(outOfFocusTimeouts);
}

export function show() {
  outOfFocusTimeouts.push(
    setTimeout(() => {
      $("#words").css("transition", "0.25s").addClass("blurred");
      $(".outOfFocusWarning").removeClass("hidden");
    }, 1000)
  );
}

==> ./monkeytype/src/js/test/result.js <==
import * as TestUI from "./test-ui";
import Config from "./config";
import * as Misc from "./misc";
import * as TestStats from "./test-stats";
import * as Keymap from "./keymap";
import * as ChartController from "./chart-controller";
import * as UI from "./ui";
import * as ThemeColors from "./theme-colors";
import * as DB from "./db";
import * as TodayTracker from "./today-tracker";
import * as PbCrown from "./pb-crown";
import * as RateQuotePopup from "./rate-quote-popup";
import * as TestLogic from "./test-logic";
import * as Notifications from "./notifications";

let result;
let maxChartVal;

let useUnsmoothedRaw = false;

export function toggleUnsmoothedRaw() {
  useUnsmoothedRaw = !useUnsmoothedRaw;
  Notifications.add(useUnsmoothedRaw ? "on" : "off", 1);
}

async function updateGraph() {
  ChartController.result.options.annotation.annotations = [];
  let labels = [];
  for (let i = 1; i <= TestStats.wpmHistory.length; i++) {
    if (TestStats.lastSecondNotRound && i === TestStats.wpmHistory.length) {
      labels.push(Misc.roundTo2(result.testDuration).toString());
    } else {
      labels.push(i.toString());
    }
  }
  ChartController.result.updateColors();
  ChartController.result.data.labels = labels;
  ChartController.result.options.scales.yAxes[0].scaleLabel.labelString = Config.alwaysShowCPM
    ? "Character per Minute"
    : "Words per Minute";
  let chartData1 = Config.alwaysShowCPM
    ? TestStats.wpmHistory.map((a) => a * 5)
    : TestStats.wpmHistory;

  let chartData2;

  if (useUnsmoothedRaw) {
    chartData2 = Config.alwaysShowCPM
      ? result.chartData.unsmoothedRaw.map((a) => a * 5)
      : result.chartData.unsmoothedRaw;
  } else {
    chartData2 = Config.alwaysShowCPM
      ? result.chartData.raw.map((a) => a * 5)
      : result.chartData.raw;
  }

  ChartController.result.data.datasets[0].data = chartData1;
  ChartController.result.data.datasets[1].data = chartData2;

  ChartController.result.data.datasets[0].label = Config.alwaysShowCPM
    ? "cpm"
    : "wpm";

  maxChartVal = Math.max(...[Math.max(...chartData2), Math.max(...chartData1)]);
  if (!Config.startGraphsAtZero) {
    let minChartVal = Math.min(
      ...[Math.min(...chartData2), Math.min(...chartData1)]
    );
    ChartController.result.options.scales.yAxes[0].ticks.min = minChartVal;
    ChartController.result.options.scales.yAxes[1].ticks.min = minChartVal;
  } else {
    ChartController.result.options.scales.yAxes[0].ticks.min = 0;
    ChartController.result.options.scales.yAxes[1].ticks.min = 0;
  }

  ChartController.result.data.datasets[2].data = result.chartData.err;

  let fc = await ThemeColors.get("sub");
  if (Config.funbox !== "none") {
    let content = Config.funbox;
    if (Config.funbox === "layoutfluid") {
      content += " " + Config.customLayoutfluid.replace(/#/g, " ");
    }
    ChartController.result.options.annotation.annotations.push({
      enabled: false,
      type: "line",
      mode: "horizontal",
      scaleID: "wpm",
      value: 0,
      borderColor: "transparent",
      borderWidth: 1,
      borderDash: [2, 2],
      label: {
        backgroundColor: "transparent",
        fontFamily: Config.fontFamily.replace(/_/g, " "),
        fontSize: 11,
        fontStyle: "normal",
        fontColor: fc,
        xPadding: 6,
        yPadding: 6,
        cornerRadius: 3,
        position: "left",
        enabled: true,
        content: `${content}`,
        yAdjust: -11,
      },
    });
  }

  ChartController.result.options.scales.yAxes[0].ticks.max = maxChartVal;
  ChartController.result.options.scales.yAxes[1].ticks.max = maxChartVal;

  ChartController.result.update({ duration: 0 });
  ChartController.result.resize();
}

export async function updateGraphPBLine() {
  let themecolors = await ThemeColors.get();
  let lpb = await DB.getLocalPB(
    result.mode,
    result.mode2,
    result.punctuation,
    result.language,
    result.difficulty,
    result.lazyMode,
    result.funbox
  );
  if (lpb == 0) return;
  let chartlpb = Misc.roundTo2(Config.alwaysShowCPM ? lpb * 5 : lpb).toFixed(2);
  ChartController.result.options.annotation.annotations.push({
    enabled: false,
    type: "line",
    mode: "horizontal",
    scaleID: "wpm",
    value: chartlpb,
    borderColor: themecolors["sub"],
    borderWidth: 1,
    borderDash: [2, 2],
    label: {
      backgroundColor: themecolors["sub"],
      fontFamily: Config.fontFamily.replace(/_/g, " "),
      fontSize: 11,
      fontStyle: "normal",
      fontColor: themecolors["bg"],
      xPadding: 6,
      yPadding: 6,
      cornerRadius: 3,
      position: "center",
      enabled: true,
      content: `PB: ${chartlpb}`,
    },
  });
  if (
    maxChartVal >= parseFloat(chartlpb) - 20 &&
    maxChartVal <= parseFloat(chartlpb) + 20
  ) {
    maxChartVal = parseFloat(chartlpb) + 20;
  }
  ChartController.result.options.scales.yAxes[0].ticks.max = Math.round(
    maxChartVal
  );
  ChartController.result.options.scales.yAxes[1].ticks.max = Math.round(
    maxChartVal
  );
  ChartController.result.update({ duration: 0 });
}

function updateWpmAndAcc() {
  let inf = false;
  if (result.wpm >= 1000) {
    inf = true;
  }
  if (Config.alwaysShowDecimalPlaces) {
    if (Config.alwaysShowCPM == false) {
      $("#result .stats .wpm .top .text").text("wpm");
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(
          Misc.roundTo2(result.wpm).toFixed(2)
        );
      }
      $("#result .stats .raw .bottom").text(
        Misc.roundTo2(result.rawWpm).toFixed(2)
      );
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(result.wpm * 5).toFixed(2) + " cpm"
      );
    } else {
      $("#result .stats .wpm .top .text").text("cpm");
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(
          Misc.roundTo2(result.wpm * 5).toFixed(2)
        );
      }
      $("#result .stats .raw .bottom").text(
        Misc.roundTo2(result.rawWpm * 5).toFixed(2)
      );
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(result.wpm).toFixed(2) + " wpm"
      );
    }

    $("#result .stats .acc .bottom").text(
      result.acc == 100 ? "100%" : Misc.roundTo2(result.acc).toFixed(2) + "%"
    );
    let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s";
    if (result.testDuration > 61) {
      time = Misc.secondsToString(Misc.roundTo2(result.testDuration));
    }
    $("#result .stats .time .bottom .text").text(time);
    $("#result .stats .raw .bottom").removeAttr("aria-label");
    $("#result .stats .acc .bottom").removeAttr("aria-label");
  } else {
    //not showing decimal places
    if (Config.alwaysShowCPM == false) {
      $("#result .stats .wpm .top .text").text("wpm");
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        result.wpm + ` (${Misc.roundTo2(result.wpm * 5)} cpm)`
      );
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Math.round(result.wpm));
      }
      $("#result .stats .raw .bottom").text(Math.round(result.rawWpm));
      $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm);
    } else {
      $("#result .stats .wpm .top .text").text("cpm");
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(result.wpm * 5) + ` (${Misc.roundTo2(result.wpm)} wpm)`
      );
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Math.round(result.wpm * 5));
      }
      $("#result .stats .raw .bottom").text(Math.round(result.rawWpm * 5));
      $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm * 5);
    }

    $("#result .stats .acc .bottom").text(Math.floor(result.acc) + "%");
    $("#result .stats .acc .bottom").attr("aria-label", result.acc + "%");
  }
}

function updateConsistency() {
  if (Config.alwaysShowDecimalPlaces) {
    $("#result .stats .consistency .bottom").text(
      Misc.roundTo2(result.consistency).toFixed(2) + "%"
    );
    $("#result .stats .consistency .bottom").attr(
      "aria-label",
      `${result.keyConsistency.toFixed(2)}% key`
    );
  } else {
    $("#result .stats .consistency .bottom").text(
      Math.round(result.consistency) + "%"
    );
    $("#result .stats .consistency .bottom").attr(
      "aria-label",
      `${result.consistency}% (${result.keyConsistency}% key)`
    );
  }
}

function updateTime() {
  let afkSecondsPercent = Misc.roundTo2(
    (result.afkDuration / result.testDuration) * 100
  );
  $("#result .stats .time .bottom .afk").text("");
  if (afkSecondsPercent > 0) {
    $("#result .stats .time .bottom .afk").text(afkSecondsPercent + "% afk");
  }
  $("#result .stats .time .bottom").attr(
    "aria-label",
    `${result.afkDuration}s afk ${afkSecondsPercent}%`
  );
  if (Config.alwaysShowDecimalPlaces) {
    let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s";
    if (result.testDuration > 61) {
      time = Misc.secondsToString(Misc.roundTo2(result.testDuration));
    }
    $("#result .stats .time .bottom .text").text(time);
  } else {
    let time = Math.round(result.testDuration) + "s";
    if (result.testDuration > 61) {
      time = Misc.secondsToString(Math.round(result.testDuration));
    }
    $("#result .stats .time .bottom .text").text(time);
    $("#result .stats .time .bottom").attr(
      "aria-label",
      `${Misc.roundTo2(result.testDuration)}s (${
        result.afkDuration
      }s afk ${afkSecondsPercent}%)`
    );
  }
}

export function updateTodayTracker() {
  $("#result .stats .time .bottom .timeToday").text(TodayTracker.getString());
}

function updateKey() {
  $("#result .stats .key .bottom").text(
    result.charStats[0] +
      "/" +
      result.charStats[1] +
      "/" +
      result.charStats[2] +
      "/" +
      result.charStats[3]
  );
}

export function showCrown() {
  PbCrown.show();
}

export function hideCrown() {
  PbCrown.hide();
  $("#result .stats .wpm .crown").attr("aria-label", "");
}

export async function updateCrown() {
  let pbDiff = 0;
  const lpb = await DB.getLocalPB(
    Config.mode,
    result.mode2,
    Config.punctuation,
    Config.language,
    Config.difficulty,
    Config.lazyMode,
    Config.funbox
  );
  pbDiff = Math.abs(result.wpm - lpb);
  $("#result .stats .wpm .crown").attr(
    "aria-label",
    "+" + Misc.roundTo2(pbDiff)
  );
}

function updateTags(dontSave) {
  let activeTags = [];
  try {
    DB.getSnapshot().tags.forEach((tag) => {
      if (tag.active === true) {
        activeTags.push(tag);
      }
    });
  } catch (e) {}

  $("#result .stats .tags").addClass("hidden");
  if (activeTags.length == 0) {
    $("#result .stats .tags").addClass("hidden");
  } else {
    $("#result .stats .tags").removeClass("hidden");
  }
  $("#result .stats .tags .bottom").text("");
  let annotationSide = "left";
  let labelAdjust = 15;
  activeTags.forEach(async (tag) => {
    let tpb = await DB.getLocalTagPB(
      tag._id,
      Config.mode,
      result.mode2,
      Config.punctuation,
      Config.language,
      Config.difficulty,
      Config.lazyMode
    );
    $("#result .stats .tags .bottom").append(`
      <div tagid="${tag._id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.name}<i class="fas fa-crown hidden"></i></div>
    `);
    if (Config.mode != "quote" && !dontSave) {
      if (tpb < result.wpm) {
        //new pb for that tag
        DB.saveLocalTagPB(
          tag._id,
          Config.mode,
          result.mode2,
          Config.punctuation,
          Config.language,
          Config.difficulty,
          Config.lazyMode,
          result.wpm,
          result.acc,
          result.rawWpm,
          result.consistency
        );
        $(
          `#result .stats .tags .bottom div[tagid="${tag._id}"] .fas`
        ).removeClass("hidden");
        $(`#result .stats .tags .bottom div[tagid="${tag._id}"]`).attr(
          "aria-label",
          "+" + Misc.roundTo2(result.wpm - tpb)
        );
        // console.log("new pb for tag " + tag.name);
      } else {
        let themecolors = await ThemeColors.get();
        ChartController.result.options.annotation.annotations.push({
          enabled: false,
          type: "line",
          mode: "horizontal",
          scaleID: "wpm",
          value: Config.alwaysShowCPM ? tpb * 5 : tpb,
          borderColor: themecolors["sub"],
          borderWidth: 1,
          borderDash: [2, 2],
          label: {
            backgroundColor: themecolors["sub"],
            fontFamily: Config.fontFamily.replace(/_/g, " "),
            fontSize: 11,
            fontStyle: "normal",
            fontColor: themecolors["bg"],
            xPadding: 6,
            yPadding: 6,
            cornerRadius: 3,
            position: annotationSide,
            xAdjust: labelAdjust,
            enabled: true,
            content: `${tag.name} PB: ${Misc.roundTo2(
              Config.alwaysShowCPM ? tpb * 5 : tpb
            ).toFixed(2)}`,
          },
        });
        if (annotationSide === "left") {
          annotationSide = "right";
          labelAdjust = -15;
        } else {
          annotationSide = "left";
          labelAdjust = 15;
        }
      }
    }
  });
}

function updateTestType() {
  let testType = "";

  if (Config.mode === "quote") {
    let qlen = "";
    if (Config.quoteLength === 0) {
      qlen = "short ";
    } else if (Config.quoteLength === 1) {
      qlen = "medium ";
    } else if (Config.quoteLength === 2) {
      qlen = "long ";
    } else if (Config.quoteLength === 3) {
      qlen = "thicc ";
    }
    testType += qlen + Config.mode;
  } else {
    testType += Config.mode;
  }
  if (Config.mode == "time") {
    testType += " " + Config.time;
  } else if (Config.mode == "words") {
    testType += " " + Config.words;
  }
  if (
    Config.mode != "custom" &&
    Config.funbox !== "gibberish" &&
    Config.funbox !== "ascii" &&
    Config.funbox !== "58008"
  ) {
    testType += "<br>" + result.language.replace(/_/g, " ");
  }
  if (Config.punctuation) {
    testType += "<br>punctuation";
  }
  if (Config.numbers) {
    testType += "<br>numbers";
  }
  if (Config.blindMode) {
    testType += "<br>blind";
  }
  if (Config.lazyMode) {
    testType += "<br>lazy";
  }
  if (Config.funbox !== "none") {
    testType += "<br>" + Config.funbox.replace(/_/g, " ");
  }
  if (Config.difficulty == "expert") {
    testType += "<br>expert";
  } else if (Config.difficulty == "master") {
    testType += "<br>master";
  }

  $("#result .stats .testType .bottom").html(testType);
}

function updateOther(
  difficultyFailed,
  failReason,
  afkDetected,
  isRepeated,
  tooShort
) {
  let otherText = "";
  if (difficultyFailed) {
    otherText += `<br>failed (${failReason})`;
  }
  if (afkDetected) {
    otherText += "<br>afk detected";
  }
  if (TestStats.invalid) {
    otherText += "<br>invalid";
    let extra = "";
    if (result.wpm < 0 || result.wpm > 350) {
      extra += "wpm";
    }
    if (result.acc < 75 || result.acc > 100) {
      if (extra.length > 0) {
        extra += ", ";
      }
      extra += "accuracy";
    }
    if (extra.length > 0) {
      otherText += ` (${extra})`;
    }
  }
  if (isRepeated) {
    otherText += "<br>repeated";
  }
  if (result.bailedOut) {
    otherText += "<br>bailed out";
  }
  if (tooShort) {
    otherText += "<br>too short";
  }

  if (otherText == "") {
    $("#result .stats .info").addClass("hidden");
  } else {
    $("#result .stats .info").removeClass("hidden");
    otherText = otherText.substring(4);
    $("#result .stats .info .bottom").html(otherText);
  }
}

export function updateRateQuote(randomQuote) {
  if (Config.mode === "quote") {
    let userqr = DB.getSnapshot().quoteRatings?.[randomQuote.language]?.[
      randomQuote.id
    ];
    if (userqr) {
      $(".pageTest #result #rateQuoteButton .icon")
        .removeClass("far")
        .addClass("fas");
    }
    RateQuotePopup.getQuoteStats(randomQuote).then((quoteStats) => {
      if (quoteStats !== null) {
        $(".pageTest #result #rateQuoteButton .rating").text(
          quoteStats.average
        );
      }
      $(".pageTest #result #rateQuoteButton")
        .css({ opacity: 0 })
        .removeClass("hidden")
        .css({ opacity: 1 });
    });
  }
}

function updateQuoteSource(randomQuote) {
  if (Config.mode === "quote") {
    $("#result .stats .source").removeClass("hidden");
    $("#result .stats .source .bottom").html(randomQuote.source);
  } else {
    $("#result .stats .source").addClass("hidden");
  }
}

export function update(
  res,
  difficultyFailed,
  failReason,
  afkDetected,
  isRepeated,
  tooShort,
  randomQuote,
  dontSave
) {
  result = res;
  $("#result #resultWordsHistory").addClass("hidden");
  $("#retrySavingResultButton").addClass("hidden");
  $(".pageTest #result #rateQuoteButton .icon")
    .removeClass("fas")
    .addClass("far");
  $(".pageTest #result #rateQuoteButton .rating").text("");
  $(".pageTest #result #rateQuoteButton").addClass("hidden");
  $("#testModesNotice").css("opacity", 0);
  $("#words").removeClass("blurred");
  $("#wordsInput").blur();
  $("#result .stats .time .bottom .afk").text("");
  if (firebase.auth().currentUser != null) {
    $("#result .loginTip").addClass("hidden");
  } else {
    $("#result .loginTip").removeClass("hidden");
  }
  updateWpmAndAcc();
  updateConsistency();
  updateTime();
  updateKey();
  updateTestType();
  updateQuoteSource(randomQuote);
  updateGraph();
  updateGraphPBLine();
  updateTags(dontSave);
  updateOther(difficultyFailed, failReason, afkDetected, isRepeated, tooShort);

  if (
    $("#result .stats .tags").hasClass("hidden") &&
    $("#result .stats .info").hasClass("hidden")
  ) {
    $("#result .stats .infoAndTags").addClass("hidden");
  } else {
    $("#result .stats .infoAndTags").removeClass("hidden");
  }

  if (TestLogic.glarsesMode) {
    $("#middle #result .noStressMessage").remove();
    $("#middle #result").prepend(`

      <div class='noStressMessage' style="
        text-align: center;
        grid-column: 1/3;
        font-size: 2rem;
        padding-bottom: 2rem;
      ">
      <i class="fas fa-check"></i>
      </div>

    `);
    $("#middle #result .stats").addClass("hidden");
    $("#middle #result .chart").addClass("hidden");
    $("#middle #result #resultWordsHistory").addClass("hidden");
    $("#middle #result #resultReplay").addClass("hidden");
    $("#middle #result .loginTip").addClass("hidden");
    $("#middle #result #showWordHistoryButton").addClass("hidden");
    $("#middle #result #watchReplayButton").addClass("hidden");
    $("#middle #result #saveScreenshotButton").addClass("hidden");

    console.log(
      `Test Completed: ${result.wpm} wpm ${result.acc}% acc ${result.rawWpm} raw ${result.consistency}% consistency`
    );
  } else {
    $("#middle #result .stats").removeClass("hidden");
    $("#middle #result .chart").removeClass("hidden");
    // $("#middle #result #resultWordsHistory").removeClass("hidden");
    if (firebase.auth().currentUser == null) {
      $("#middle #result .loginTip").removeClass("hidden");
    }
    $("#middle #result #showWordHistoryButton").removeClass("hidden");
    $("#middle #result #watchReplayButton").removeClass("hidden");
    $("#middle #result #saveScreenshotButton").removeClass("hidden");
  }

  if (window.scrollY > 0)
    $([document.documentElement, document.body])
      .stop()
      .animate({ scrollTop: 0 }, 250);

  UI.swapElements(
    $("#typingTest"),
    $("#result"),
    250,
    () => {
      TestUI.setResultCalculating(false);
      $("#words").empty();
      ChartController.result.resize();

      if (Config.alwaysShowWordsHistory && Config.burstHeatmap) {
        TestUI.applyBurstHeatmap();
      }
      $("#result").focus();
      window.scrollTo({ top: 0 });
      $("#testModesNotice").addClass("hidden");
    },
    () => {
      $("#resultExtraButtons").removeClass("hidden").css("opacity", 0).animate(
        {
          opacity: 1,
        },
        125
      );
      if (Config.alwaysShowWordsHistory && !TestLogic.glarsesMode) {
        TestUI.toggleResultWords();
      }
      Keymap.hide();
    }
  );
}

==> ./monkeytype/src/js/test/test-config.js <==
import * as CustomWordAmountPopup from "./custom-word-amount-popup";
import * as CustomTestDurationPopup from "./custom-test-duration-popup";
import * as UpdateConfig from "./config";
import * as ManualRestart from "./manual-restart-tracker";
import * as TestLogic from "./test-logic";
import * as QuoteSearchPopup from "./quote-search-popup";
import * as CustomTextPopup from "./custom-text-popup";
import * as UI from "./ui";

// export function show() {
//   $("#top .config").removeClass("hidden").css("opacity", 1);
// }

// export function hide() {
//   $("#top .config").css("opacity", 0).addClass("hidden");
// }

export function show() {
  $("#top .config")
    .stop(true, true)
    .removeClass("hidden")
    .css("opacity", 0)
    .animate(
      {
        opacity: 1,
      },
      125
    );
}

export function hide() {
  $("#top .config")
    .stop(true, true)
    .css("opacity", 1)
    .animate(
      {
        opacity: 0,
      },
      125,
      () => {
        $("#top .config").addClass("hidden");
      }
    );
}

export function update(previous, current) {
  if (previous == current) return;
  $("#top .config .mode .text-button").removeClass("active");
  $("#top .config .mode .text-button[mode='" + current + "']").addClass(
    "active"
  );
  if (current == "time") {
    // $("#top .config .wordCount").addClass("hidden");
    // $("#top .config .time").removeClass("hidden");
    // $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").removeClass("disabled");
    $("#top .config .numbersMode").removeClass("disabled");
    // $("#top .config .puncAndNum").removeClass("disabled");
    // $("#top .config .punctuationMode").removeClass("hidden");
    // $("#top .config .numbersMode").removeClass("hidden");
    // $("#top .config .quoteLength").addClass("hidden");
  } else if (current == "words") {
    // $("#top .config .wordCount").removeClass("hidden");
    // $("#top .config .time").addClass("hidden");
    // $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").removeClass("disabled");
    $("#top .config .numbersMode").removeClass("disabled");
    // $("#top .config .puncAndNum").removeClass("disabled");
    // $("#top .config .punctuationMode").removeClass("hidden");
    // $("#top .config .numbersMode").removeClass("hidden");
    // $("#top .config .quoteLength").addClass("hidden");
  } else if (current == "custom") {
    // $("#top .config .wordCount").addClass("hidden");
    // $("#top .config .time").addClass("hidden");
    // $("#top .config .customText").removeClass("hidden");
    $("#top .config .punctuationMode").removeClass("disabled");
    $("#top .config .numbersMode").removeClass("disabled");
    // $("#top .config .puncAndNum").removeClass("disabled");
    // $("#top .config .punctuationMode").removeClass("hidden");
    // $("#top .config .numbersMode").removeClass("hidden");
    // $("#top .config .quoteLength").addClass("hidden");
  } else if (current == "quote") {
    // $("#top .config .wordCount").addClass("hidden");
    // $("#top .config .time").addClass("hidden");
    // $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").addClass("disabled");
    $("#top .config .numbersMode").addClass("disabled");
    // $("#top .config .puncAndNum").addClass("disabled");
    // $("#top .config .punctuationMode").removeClass("hidden");
    // $("#top .config .numbersMode").removeClass("hidden");
    // $("#result .stats .source").removeClass("hidden");
    // $("#top .config .quoteLength").removeClass("hidden");
  } else if (current == "zen") {
    // $("#top .config .wordCount").addClass("hidden");
    // $("#top .config .time").addClass("hidden");
    // $("#top .config .customText").addClass("hidden");
    // $("#top .config .punctuationMode").addClass("hidden");
    // $("#top .config .numbersMode").addClass("hidden");
    // $("#top .config .quoteLength").addClass("hidden");
  }

  let submenu = {
    time: "time",
    words: "wordCount",
    custom: "customText",
    quote: "quoteLength",
    zen: "",
  };

  let animTime = 250;

  if (current == "zen") {
    $(`#top .config .${submenu[previous]}`).animate(
      {
        opacity: 0,
      },
      animTime / 2,
      () => {
        $(`#top .config .${submenu[previous]}`).addClass("hidden");
      }
    );
    $(`#top .config .puncAndNum`).animate(
      {
        opacity: 0,
      },
      animTime / 2,
      () => {
        $(`#top .config .puncAndNum`).addClass("invisible");
      }
    );
    return;
  }

  if (previous == "zen") {
    setTimeout(() => {
      $(`#top .config .${submenu[current]}`).removeClass("hidden");
      $(`#top .config .${submenu[current]}`)
        .css({ opacity: 0 })
        .animate(
          {
            opacity: 1,
          },
          animTime / 2
        );
      $(`#top .config .puncAndNum`).removeClass("invisible");
      $(`#top .config .puncAndNum`)
        .css({ opacity: 0 })
        .animate(
          {
            opacity: 1,
          },
          animTime / 2
        );
    }, animTime / 2);
    return;
  }

  UI.swapElements(
    $("#top .config ." + submenu[previous]),
    $("#top .config ." + submenu[current]),
    animTime
  );
}

$(document).on("click", "#top .config .wordCount .text-button", (e) => {
  const wrd = $(e.currentTarget).attr("wordCount");
  if (wrd == "custom") {
    CustomWordAmountPopup.show();
  } else {
    UpdateConfig.setWordCount(wrd);
    ManualRestart.set();
    TestLogic.restart();
  }
});

$(document).on("click", "#top .config .time .text-button", (e) => {
  let mode = $(e.currentTarget).attr("timeConfig");
  if (mode == "custom") {
    CustomTestDurationPopup.show();
  } else {
    UpdateConfig.setTimeConfig(mode);
    ManualRestart.set();
    TestLogic.restart();
  }
});

$(document).on("click", "#top .config .quoteLength .text-button", (e) => {
  let len = $(e.currentTarget).attr("quoteLength");
  if (len == -2) {
    // UpdateConfig.setQuoteLength(-2, false, e.shiftKey);
    QuoteSearchPopup.show();
  } else {
    if (len == -1) {
      len = [0, 1, 2, 3];
    }
    UpdateConfig.setQuoteLength(len, false, e.shiftKey);
    ManualRestart.set();
    TestLogic.restart();
  }
});

$(document).on("click", "#top .config .customText .text-button", () => {
  CustomTextPopup.show();
});

$(document).on("click", "#top .config .punctuationMode .text-button", () => {
  UpdateConfig.togglePunctuation();
  ManualRestart.set();
  TestLogic.restart();
});

$(document).on("click", "#top .config .numbersMode .text-button", () => {
  UpdateConfig.toggleNumbers();
  ManualRestart.set();
  TestLogic.restart();
});

$(document).on("click", "#top .config .mode .text-button", (e) => {
  if ($(e.currentTarget).hasClass("active")) return;
  const mode = $(e.currentTarget).attr("mode");
  UpdateConfig.setMode(mode);
  ManualRestart.set();
  TestLogic.restart();
});

==> ./monkeytype/src/js/test/practise-words.js <==
import * as TestStats from "./test-stats";
import * as Notifications from "./notifications";
import Config, * as UpdateConfig from "./config";
import * as CustomText from "./custom-text";
import * as TestLogic from "./test-logic";

export let before = {
  mode: null,
  punctuation: null,
  numbers: null,
};

export function init(missed, slow) {
  if (Config.mode === "zen") return;
  let limit;
  if ((missed && !slow) || (!missed && slow)) {
    limit = 20;
  } else if (missed && slow) {
    limit = 10;
  }

  let sortableMissedWords = [];
  if (missed) {
    Object.keys(TestStats.missedWords).forEach((missedWord) => {
      sortableMissedWords.push([missedWord, TestStats.missedWords[missedWord]]);
    });
    sortableMissedWords.sort((a, b) => {
      return b[1] - a[1];
    });
    sortableMissedWords = sortableMissedWords.slice(0, limit);
  }

  if (missed && !slow && sortableMissedWords.length == 0) {
    Notifications.add("You haven't missed any words", 0);
    return;
  }

  let sortableSlowWords = [];
  if (slow) {
    sortableSlowWords = TestLogic.words.get().map(function (e, i) {
      return [e, TestStats.burstHistory[i]];
    });
    sortableSlowWords.sort((a, b) => {
      return a[1] - b[1];
    });
    sortableSlowWords = sortableSlowWords.slice(
      0,
      Math.min(limit, Math.round(TestLogic.words.length * 0.2))
    );
  }

  // console.log(sortableMissedWords);
  // console.log(sortableSlowWords);

  if (sortableMissedWords.length == 0 && sortableSlowWords.length == 0) {
    Notifications.add("Could not start a new custom test", 0);
    return;
  }

  let newCustomText = [];
  sortableMissedWords.forEach((missed, index) => {
    for (let i = 0; i < missed[1]; i++) {
      newCustomText.push(missed[0]);
    }
  });

  sortableSlowWords.forEach((slow, index) => {
    for (let i = 0; i < sortableSlowWords.length - index; i++) {
      newCustomText.push(slow[0]);
    }
  });

  // console.log(newCustomText);

  let mode = before.mode === null ? Config.mode : before.mode;
  let punctuation =
    before.punctuation === null ? Config.punctuation : before.punctuation;
  let numbers = before.numbers === null ? Config.numbers : before.numbers;
  UpdateConfig.setMode("custom");

  CustomText.setText(newCustomText);
  CustomText.setIsWordRandom(true);
  CustomText.setWord(
    (sortableSlowWords.length + sortableMissedWords.length) * 5
  );

  TestLogic.restart(false, false, false, true);
  before.mode = mode;
  before.punctuation = punctuation;
  before.numbers = numbers;
}

export function resetBefore() {
  before.mode = null;
  before.punctuation = null;
  before.numbers = null;
}

export function showPopup(focus = false) {
  if ($("#practiseWordsPopupWrapper").hasClass("hidden")) {
    if (Config.mode === "zen") {
      Notifications.add("Practice words is unsupported in zen mode", 0);
      return;
    }
    $("#practiseWordsPopupWrapper")
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate({ opacity: 1 }, 100, () => {
        if (focus) {
          console.log("focusing");
          $("#practiseWordsPopup .missed").focus();
        }
      });
  }
}

function hidePopup() {
  if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) {
    $("#practiseWordsPopupWrapper")
      .stop(true, true)
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        100,
        (e) => {
          $("#practiseWordsPopupWrapper").addClass("hidden");
        }
      );
  }
}

$("#practiseWordsPopupWrapper").click((e) => {
  if ($(e.target).attr("id") === "practiseWordsPopupWrapper") {
    hidePopup();
  }
});

$("#practiseWordsPopup .button.missed").click(() => {
  hidePopup();
  init(true, false);
});

$("#practiseWordsPopup .button.slow").click(() => {
  hidePopup();
  init(false, true);
});

$("#practiseWordsPopup .button.both").click(() => {
  hidePopup();
  init(true, true);
});

$("#practiseWordsPopup .button").keypress((e) => {
  if (e.key == "Enter") {
    $(e.currentTarget).click();
  }
});

$("#practiseWordsPopup .button.both").on("focusout", (e) => {
  e.preventDefault();
  $("#practiseWordsPopup .missed").focus();
});

==> ./monkeytype/src/js/test/test-stats.js <==
import * as TestLogic from "./test-logic";
import Config from "./config";
import * as Misc from "./misc";
import * as TestStats from "./test-stats";

export let invalid = false;
export let start, end;
export let start2, end2;
export let wpmHistory = [];
export let rawHistory = [];
export let burstHistory = [];

export let keypressPerSecond = [];
export let currentKeypress = {
  count: 0,
  errors: 0,
  words: [],
  afk: true,
};
export let lastKeypress;
export let currentBurstStart = 0;

// export let errorsPerSecond = [];
// export let currentError = {
//   count: 0,
//   words: [],
// };
export let lastSecondNotRound = false;
export let missedWords = {};
export let accuracy = {
  correct: 0,
  incorrect: 0,
};
export let keypressTimings = {
  spacing: {
    current: -1,
    array: [],
  },
  duration: {
    current: -1,
    array: [],
  },
};

export function getStats() {
  let ret = {
    start,
    end,
    wpmHistory,
    rawHistory,
    burstHistory,
    keypressPerSecond,
    currentKeypress,
    lastKeypress,
    currentBurstStart,
    lastSecondNotRound,
    missedWords,
    accuracy,
    keypressTimings,
  };

  try {
    ret.keySpacingStats = {
      average:
        keypressTimings.spacing.array.reduce(
          (previous, current) => (current += previous)
        ) / keypressTimings.spacing.array.length,
      sd: Misc.stdDev(keypressTimings.spacing.array),
    };
  } catch (e) {
    //
  }
  try {
    ret.keyDurationStats = {
      average:
        keypressTimings.duration.array.reduce(
          (previous, current) => (current += previous)
        ) / keypressTimings.duration.array.length,
      sd: Misc.stdDev(keypressTimings.duration.array),
    };
  } catch (e) {
    //
  }

  return ret;
}

export function restart() {
  start = 0;
  end = 0;
  invalid = false;
  wpmHistory = [];
  rawHistory = [];
  burstHistory = [];
  keypressPerSecond = [];
  currentKeypress = {
    count: 0,
    errors: 0,
    words: [],
    afk: true,
  };
  currentBurstStart = 0;
  // errorsPerSecond = [];
  // currentError = {
  //   count: 0,
  //   words: [],
  // };
  lastSecondNotRound = false;
  missedWords = {};
  accuracy = {
    correct: 0,
    incorrect: 0,
  };
  keypressTimings = {
    spacing: {
      current: -1,
      array: [],
    },
    duration: {
      current: -1,
      array: [],
    },
  };
}

export let restartCount = 0;
export let incompleteSeconds = 0;

export function incrementRestartCount() {
  restartCount++;
}

export function incrementIncompleteSeconds(val) {
  incompleteSeconds += val;
}

export function resetIncomplete() {
  restartCount = 0;
  incompleteSeconds = 0;
}

export function setInvalid() {
  invalid = true;
}

export function calculateTestSeconds(now) {
  if (now === undefined) {
    let endAfkSeconds = (end - lastKeypress) / 1000;
    if ((Config.mode == "zen" || TestLogic.bailout) && endAfkSeconds < 7) {
      return (lastKeypress - start) / 1000;
    } else {
      return (end - start) / 1000;
    }
  } else {
    return (now - start) / 1000;
  }
}

export function setEnd(e) {
  end = e;
  end2 = Date.now();
}

export function setStart(s) {
  start = s;
  start2 = Date.now();
}

export function updateLastKeypress() {
  lastKeypress = performance.now();
}

export function pushToWpmHistory(word) {
  wpmHistory.push(word);
}

export function pushToRawHistory(word) {
  rawHistory.push(word);
}

export function incrementKeypressCount() {
  currentKeypress.count++;
}

export function setKeypressNotAfk() {
  currentKeypress.afk = false;
}

export function incrementKeypressErrors() {
  currentKeypress.errors++;
}

export function pushKeypressWord(word) {
  currentKeypress.words.push(word);
}

export function pushKeypressesToHistory() {
  keypressPerSecond.push(currentKeypress);
  currentKeypress = {
    count: 0,
    errors: 0,
    words: [],
    afk: true,
  };
}

export function calculateAfkSeconds(testSeconds) {
  let extraAfk = 0;
  if (testSeconds !== undefined) {
    if (Config.mode === "time") {
      extraAfk = Math.round(testSeconds) - keypressPerSecond.length;
    } else {
      extraAfk = Math.ceil(testSeconds) - keypressPerSecond.length;
    }
    if (extraAfk < 0) extraAfk = 0;
    // console.log("-- extra afk debug");
    // console.log("should be " + Math.ceil(testSeconds));
    // console.log(keypressPerSecond.length);
    // console.log(
    //   `gonna add extra ${extraAfk} seconds of afk because of no keypress data`
    // );
  }
  let ret = keypressPerSecond.filter((x) => x.afk).length;
  return ret + extraAfk;
}

export function setLastSecondNotRound() {
  lastSecondNotRound = true;
}

export function setBurstStart(time) {
  currentBurstStart = time;
}

export function calculateBurst() {
  let timeToWrite = (performance.now() - currentBurstStart) / 1000;
  let wordLength;
  if (Config.mode === "zen") {
    wordLength = TestLogic.input.current.length;
    if (wordLength == 0) {
      wordLength = TestLogic.input.getHistoryLast().length;
    }
  } else {
    wordLength = TestLogic.words.getCurrent().length;
  }
  let speed = Misc.roundTo2((wordLength * (60 / timeToWrite)) / 5);
  return Math.round(speed);
}

export function pushBurstToHistory(speed) {
  if (burstHistory[TestLogic.words.currentIndex] === undefined) {
    burstHistory.push(speed);
  } else {
    //repeated word - override
    burstHistory[TestLogic.words.currentIndex] = speed;
  }
}

export function calculateAccuracy() {
  let acc = (accuracy.correct / (accuracy.correct + accuracy.incorrect)) * 100;
  return isNaN(acc) ? 100 : acc;
}

export function incrementAccuracy(correctincorrect) {
  if (correctincorrect) {
    accuracy.correct++;
  } else {
    accuracy.incorrect++;
  }
}

export function setKeypressTimingsTooLong() {
  keypressTimings.spacing.array = "toolong";
  keypressTimings.duration.array = "toolong";
}

export function pushKeypressDuration(val) {
  keypressTimings.duration.array.push(val);
}

export function setKeypressDuration(val) {
  keypressTimings.duration.current = val;
}

export function pushKeypressSpacing(val) {
  keypressTimings.spacing.array.push(val);
}

export function setKeypressSpacing(val) {
  keypressTimings.spacing.current = val;
}

export function recordKeypressSpacing() {
  let now = performance.now();
  let diff = Math.abs(keypressTimings.spacing.current - now);
  if (keypressTimings.spacing.current !== -1) {
    pushKeypressSpacing(diff);
  }
  setKeypressSpacing(now);
}

export function resetKeypressTimings() {
  keypressTimings = {
    spacing: {
      current: performance.now(),
      array: [],
    },
    duration: {
      current: performance.now(),
      array: [],
    },
  };
}

export function pushMissedWord(word) {
  if (!Object.keys(missedWords).includes(word)) {
    missedWords[word] = 1;
  } else {
    missedWords[word]++;
  }
}

export function removeAfkData() {
  let testSeconds = calculateTestSeconds();
  keypressPerSecond.splice(testSeconds);
  keypressTimings.duration.array.splice(testSeconds);
  keypressTimings.spacing.array.splice(testSeconds);
  wpmHistory.splice(testSeconds);
}

function countChars() {
  let correctWordChars = 0;
  let correctChars = 0;
  let incorrectChars = 0;
  let extraChars = 0;
  let missedChars = 0;
  let spaces = 0;
  let correctspaces = 0;
  for (let i = 0; i < TestLogic.input.history.length; i++) {
    let word =
      Config.mode == "zen"
        ? TestLogic.input.getHistory(i)
        : TestLogic.words.get(i);
    if (TestLogic.input.getHistory(i) === "") {
      //last word that was not started
      continue;
    }
    if (TestLogic.input.getHistory(i) == word) {
      //the word is correct
      correctWordChars += word.length;
      correctChars += word.length;
      if (
        i < TestLogic.input.history.length - 1 &&
        Misc.getLastChar(TestLogic.input.getHistory(i)) !== "\n"
      ) {
        correctspaces++;
      }
    } else if (TestLogic.input.getHistory(i).length >= word.length) {
      //too many chars
      for (let c = 0; c < TestLogic.input.getHistory(i).length; c++) {
        if (c < word.length) {
          //on char that still has a word list pair
          if (TestLogic.input.getHistory(i)[c] == word[c]) {
            correctChars++;
          } else {
            incorrectChars++;
          }
        } else {
          //on char that is extra
          extraChars++;
        }
      }
    } else {
      //not enough chars
      let toAdd = {
        correct: 0,
        incorrect: 0,
        missed: 0,
      };
      for (let c = 0; c < word.length; c++) {
        if (c < TestLogic.input.getHistory(i).length) {
          //on char that still has a word list pair
          if (TestLogic.input.getHistory(i)[c] == word[c]) {
            toAdd.correct++;
          } else {
            toAdd.incorrect++;
          }
        } else {
          //on char that is extra
          toAdd.missed++;
        }
      }
      correctChars += toAdd.correct;
      incorrectChars += toAdd.incorrect;
      if (i === TestLogic.input.history.length - 1 && Config.mode == "time") {
        //last word - check if it was all correct - add to correct word chars
        if (toAdd.incorrect === 0) correctWordChars += toAdd.correct;
      } else {
        missedChars += toAdd.missed;
      }
    }
    if (i < TestLogic.input.history.length - 1) {
      spaces++;
    }
  }
  if (Config.funbox === "nospace" || Config.funbox === "arrows") {
    spaces = 0;
    correctspaces = 0;
  }
  return {
    spaces: spaces,
    correctWordChars: correctWordChars,
    allCorrectChars: correctChars,
    incorrectChars:
      Config.mode == "zen" ? TestStats.accuracy.incorrect : incorrectChars,
    extraChars: extraChars,
    missedChars: missedChars,
    correctSpaces: correctspaces,
  };
}

export function calculateStats() {
  let testSeconds = TestStats.calculateTestSeconds();
  console.log((TestStats.end2 - TestStats.start2) / 1000);
  console.log(testSeconds);
  if (Config.mode != "custom") {
    testSeconds = Misc.roundTo2(testSeconds);
  }
  let chars = countChars();
  let wpm = Misc.roundTo2(
    ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5
  );
  let wpmraw = Misc.roundTo2(
    ((chars.allCorrectChars +
      chars.spaces +
      chars.incorrectChars +
      chars.extraChars) *
      (60 / testSeconds)) /
      5
  );
  let acc = Misc.roundTo2(TestStats.calculateAccuracy());
  return {
    wpm: isNaN(wpm) ? 0 : wpm,
    wpmRaw: isNaN(wpmraw) ? 0 : wpmraw,
    acc: acc,
    correctChars: chars.correctWordChars,
    incorrectChars: chars.incorrectChars,
    missedChars: chars.missedChars,
    extraChars: chars.extraChars,
    allChars:
      chars.allCorrectChars +
      chars.spaces +
      chars.incorrectChars +
      chars.extraChars,
    time: testSeconds,
    spaces: chars.spaces,
    correctSpaces: chars.correctSpaces,
  };
}
