Refactor extension to webpack

This change is the biggest in the history of the project. The entire
project has been refactored so it is built with webpack.

This involves:
- Creating webpack and npm config files.
- Fixing some bugs in the code due to the fact that webpack uses strict
mode.
- Merging some pieces of code which were shared throughout the codebase
(not exhaustive, more work should be done in this direction).
- Splitting the console_inject.js file into separate files (it had 1000+
lines).
- Adapting all the build-related files (Makefile, bash scripts, etc.)
- Changing the docs to explain the new build process.
- Changing the Zuul playbook/roles to adapt to the new build process.

Change-Id: I16476d47825461c3a318b3f1a1eddb06b2df2e89
diff --git a/src/static/LICENSE b/src/static/LICENSE
new file mode 100644
index 0000000..0b165c4
--- /dev/null
+++ b/src/static/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Adrià Vilanova Martínez (@avm99963)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/src/static/_locales/ca/messages.json b/src/static/_locales/ca/messages.json
new file mode 100644
index 0000000..967853a
--- /dev/null
+++ b/src/static/_locales/ca/messages.json
@@ -0,0 +1,250 @@
+{
+  "appName": {
+    "message": "Eines avançades per TW (antigament Scroll infinit)",
+    "description": "The extension's name"
+  },
+  "appNameBeta": {
+    "message": "Eines avançades per TW (beta)",
+    "description": "The extension's name for the beta version"
+  },
+  "appNameGecko": {
+    "message": "Eines avançades per TW",
+    "description": "The extension's name (with a 50 character limit for Gecko)"
+  },
+  "appDescription": {
+    "message": "Fes scroll infinit i obtén altres millores als Fòrums de Google i la Consola de la Comunitat",
+    "description": "The extension's description"
+  },
+  "options_list": {
+    "message": "Activa el scroll infinit a les llistes de fils.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_thread": {
+    "message": "Carrega automàticament missatges en grups petits quan facis scroll per un fil.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_threadall": {
+    "message": "Carrega automàticament tots els missatges de cop quan facis scroll per un fil.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancements": {
+    "message": "Millores addicionals",
+    "description": "Heading for several options that can be enabled in the options page"
+  },
+  "options_fixedtoolbar": {
+    "message": "Mantén visible la barra d'eines de la llista de fils a la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_redirect": {
+    "message": "Redirigeix tots els fils oberts a TW a la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_loaddrafts": {
+    "message": "Activa la <i>flag<\/i> <code class=\"help\" title=\"Aquesta flag permet a la Consola de la Comunitat carregar una resposta automàticament desada anteriorment quan obres un fil i fas clic al botó de respondre.\">enableLoadingDraftMessages<\/code> de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_experimental_label": {
+    "message": "(experimental)",
+    "description": "Label which is placed next to an option to indicate that it may not work well or may break at any time"
+  },
+  "options_history": {
+    "message": "Mostra un enllaç \"historial de publicacions\" als perfils d'usuari.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_escalatethreads":{
+    "message": "Activa la <i>flag</i> <code class=\"help\" title=\"Aquesta flag activa la funcionalitat d'escalar fils del fòrum públic als privats a la Consola de la Comunitat.\">enableEscalateThread</code> de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_movethreads":{
+    "message": "Activa la <i>flag</i> <code class=\"help\" title=\"Aquesta flag activa la funcionalitat de moure fils d'un fòrum on ets un EP a un altre fòrum a la Consola de la Comunitat.\">enableMoveThread</code> de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_increasecontrast":{
+    "message": "Incrementa el contrast entre els fils llegits i no llegits a la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme": {
+    "message": "Activa el tema fosc a la Consola de la Comunitat, controlat per <span id='ccdarktheme_mode--container'></span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme_mode_switch": {
+    "message": "un botó a la consola",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccdarktheme_mode_system": {
+    "message": "la configuració del mode fosc del SO",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccforcehidedrawer": {
+    "message": "Amaga sempre la barra lateral de l'esquerra a la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdragndropfix": {
+    "message": "Permet arrossegar adreces d'interès a l'editor de text de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_batchlock": {
+    "message": "Afegeix l'opció per bloquejar diversos fils a la llista de fils de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancedannouncementsdot": {
+    "message": "Mostra el punt que notifica que hi ha anuncis sense llegir d'una manera més prominent a la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_repositionexpandthread": {
+    "message": "Posa el botó \"expandir fil\" a l'esquerra del tot en les llistes de fils de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_threadlistavatars": {
+    "message": "Mostra fotos de perfil a les llistes de fils de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_autorefreshlist": {
+    "message": "Actualitza les llistes de fils de la Consola de la Comunitat automàticament.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_disableunifiedprofiles": {
+    "message": "Desactiva a la força l'experiment <code class=\"help\" title=\"Aquest experiment, quan està actiu, introdueix un rediseny de la interfície dels perfils que també unifica tots els perfils en un únic.\">SMEI_UNIFIED_PROFILES</code> a la Consola de la Comunitat.",
+    "description": "Link to learn more about the profile indicator feature"
+  },
+  "options_forcemarkasread": {
+    "message": "Cada vegada que obris un fil a la Consola de la Comunitat, envia automàticament una petició per marcar-lo com a llegit. Aquesta és una opció temporal usada com a <i>workaround</i> d'<a href=\"https://support.google.com/s/community/forum/51488989/thread/114559215\" target=\"_blank\" rel=\"noreferrer noopener\">aquest bug</a>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicator_header": {
+    "message": "Punt indicador",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info sobre les 2 darreres opcions",
+    "description": "Link to learn more about the profile indicator feature"
+  },
+  "options_profileindicator": {
+    "message": "Mostra <span class=\"help\" title=\"Si l'autor ha participat a altres fils, es mostrarà un punt vermell al costat del seu nom d'usuari. Si les publicacions més recents han estat llegides, es mostrarà un punt taronja. Pots posar el teu ratolí a sobre del punt per mostrar què vol dir el seu color.\">si l'autor del fil ha participat a altres fils</span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicatoralt": {
+    "message": "Mostra el nombre de preguntes i respostes escrites per l'autor del fil durant els últims <span id='profileindicatoralt_months--container'></span> mesos al costat del seu nom d'usuari.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_stickysidebarheaders": {
+    "message": "Fes que les capçaleres de la barra lateral de la Consola de la Comunitat es quedin enganxades a dalt (+info a <code>pekb/thread/60784834</code>).",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_save": {
+    "message": "Desa",
+    "description": "Button in the options page to save the settings"
+  },
+  "options_saved": {
+    "message": "Desat",
+    "description": "Message which appears in the options page when the settings are saved"
+  },
+  "options_experiments_title": {
+    "message": "Experiments",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "<i>Welchrome!</i> Aquí a sota trobaràs una llista d'experiments: funcions que estan en desenvolupament i que encara no estan llestes del tot per ser llençades. Són altament experimentals i podrien trencar-se i/o causar problemes, però si ets valent/a, sisplau activa les que més t'interessin i <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>envia feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
+  "inject_links": {
+    "message": "Enllaços",
+    "description": "Heading which we use before the 'previous post' link in a user profile in TW"
+  },
+  "inject_previousposts": {
+    "message": "Historial de publicacions",
+    "description": "Link shown in a user profile (in TW) which points to a search showing the user's posts and messages"
+  },
+  "inject_previousposts_forum": {
+    "message": "Historial de publicacions (en aquest fòrum)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in the current forum"
+  },
+  "inject_previousposts_all": {
+    "message": "Historial de publicacions (en tots els fòrums)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in all forums"
+  },
+  "inject_profileindicator_loading": {
+    "message": "Carregant...",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_first_post": {
+    "message": "Aquest és el primer fil d'aquest fòrum creat per aquest usuari.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_read": {
+    "message": "Aquest usuari ha participat en altres fils d'aquest fòrum, però ja has llegit els 5 darrers on ha participat.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_unread": {
+    "message": "Aquest usuari ha participat en altres fils d'aquest fòrum, alguns dels quals no has llegit.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Nombre de preguntes i respostes en els $1 darrers mesos.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_ccdarkmode_helper": {
+    "message": "Canviar tema",
+    "description": "Tooltip for the dark mode switch."
+  },
+  "inject_extension_badge_helper": {
+    "message": "Afegit per $1",
+    "description": "Tooltip for the extension badge, which appears next to components injected by the extension."
+  },
+  "inject_lockbtn": {
+    "message": "Bloquejar/desbloquejar fils",
+    "description": "Tooltip of the 'lock/unlock threads' icon shown when selecting multiple threads in the Community Console. Also the title for the dialog shown after clicking this icon."
+  },
+  "inject_lockdialog_desc": {
+    "message": "Sisplau, confirma l'acció que volies prendre fent clic al botó corresponent d'aquí avall. Tingues en compte que aquesta acció es prendrà sobre tots els fils que hagis seleccionat.",
+    "description": "Text in the 'lock/unlock threads' dialog, which asks the user for confirmation."
+  },
+  "inject_lockdialog_btn_lock": {
+    "message": "Bloqueja",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_unlock": {
+    "message": "Desbloqueja",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_cancel": {
+    "message": "Cancel·la",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_reload": {
+    "message": "Torna a carregar",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_close": {
+    "message": "Tanca",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_log_entry_beginning": {
+    "message": "Fil $1",
+    "description": "First part of the entry in the log of the 'lock/unlock threads' dialog. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_lock": {
+    "message": "Bloquejat correctament.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_lock": {
+    "message": "Hi ha hagut un error bloquejant-lo ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_unlock": {
+    "message": "Desbloquejat correctament.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_unlock": {
+    "message": "Hi ha hagut un error desbloquejant-lo ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_autorefresh_list_snackbar_message": {
+    "message": "Hi ha nous fils.",
+    "description": "Message shown in a snackbar when new threads are found in a thread list. Another button asks the user to refresh the list."
+  },
+  "inject_autorefresh_list_snackbar_action": {
+    "message": "Actualitza",
+    "description": "Button shown in a snackbar asking users to refresh/reload the list to show the new threads."
+  }
+}
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
new file mode 100644
index 0000000..16c0e0f
--- /dev/null
+++ b/src/static/_locales/en/messages.json
@@ -0,0 +1,250 @@
+{
+  "appName": {
+    "message": "TW Power Tools (formerly Infinite Scroll)",
+    "description": "The extension's name"
+  },
+  "appNameBeta": {
+    "message": "TW Power Tools (beta)",
+    "description": "The extension's name for the beta version"
+  },
+  "appNameGecko": {
+    "message": "TW Power Tools",
+    "description": "The extension's name (with a 50 character limit for Gecko)"
+  },
+  "appDescription": {
+    "message": "Get infinite scroll and other enhancements in the Google Forums and the Community Console",
+    "description": "The extension's description"
+  },
+  "options_list": {
+    "message": "Enable infinite scrolling in thread lists.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_thread": {
+    "message": "Automatically load batches of messages inside threads when scrolling down.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_threadall": {
+    "message": "Automatically load all messages at once inside threads when scrolling down.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancements": {
+    "message": "Additional enhancements",
+    "description": "Heading for several options that can be enabled in the options page"
+  },
+  "options_fixedtoolbar": {
+    "message": "Fix the toolbar in thread lists in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_redirect": {
+    "message": "Redirect all threads opened in TW to the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_loaddrafts": {
+    "message": "Activate the <code class=\"help\" title=\"This flag allows the Community Console to load a previously autosaved reply when loading a thread and clicking the reply button.\">enableLoadingDraftMessages<\/code> Community Console flag.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_experimental_label": {
+    "message": "(experimental)",
+    "description": "Label which is placed next to an option to indicate that it may not work well or may break at any time"
+  },
+  "options_history": {
+    "message": "Show a \"previous posts\" link in user profiles.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_escalatethreads":{
+    "message": "Enables the <code class=\"help\" title=\"This flag activates the functionality which allows a PE to escalate threads from the public forums to the private ones in the Community Console.\">enableEscalateThread</code> Community Console flag.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_movethreads":{
+    "message": "Enables the <code class=\"help\" title=\"This flag activates the functionality which allows a PE to move threads from a forum where they are currently a PE to another forum in the Community Console.\">enableMoveThread</code> Community Console flag.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_stickysidebarheaders": {
+    "message": "Make the headers in the Community Console sidebar stick at the top (+info at <code>pekb/thread/60784834</code>).",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_increasecontrast":{
+    "message": "Increase contrast between read and unread threads in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme": {
+    "message": "Enable the dark theme in the Community Console, controlled <span id='ccdarktheme_mode--container'></span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme_mode_switch": {
+    "message": "with a switch in the CC",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccdarktheme_mode_system": {
+    "message": "by the OS dark mode setting",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccforcehidedrawer": {
+    "message": "Always hide the drawer (left column) in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdragndropfix": {
+    "message": "Allow to drag and drop bookmarks to the Community Console text editor.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_batchlock": {
+    "message": "Add the option to lock multiple threads from the Community Console thread list.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancedannouncementsdot": {
+    "message": "Show the announcements notification dot more prominently in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_repositionexpandthread": {
+    "message": "Place the \"expand thread\" button all the way to the left in the Community Console thread lists.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_threadlistavatars": {
+    "message": "Show avatars in thread lists in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_autorefreshlist": {
+    "message": "Autorefresh thread lists in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_disableunifiedprofiles": {
+    "message": "Force disable the <code class=\"help\" title=\"This experiment, when enabled, introduces a redesign of the profile view which also unifies all forum profiles into a single one.\">SMEI_UNIFIED_PROFILES</code> experiment in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_forcemarkasread": {
+    "message": "Each time you open a thread in the Community Console, automatically send a request to mark it as read. This is a temporary option used to work around <a href=\"https://support.google.com/s/community/forum/51488989/thread/114559215\" target=\"_blank\" rel=\"noreferrer noopener\">this bug</a>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicator_header": {
+    "message": "Indicator dot",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info about the 2 previous options",
+    "description": "Link to learn more about the profile indicator feature"
+  },
+  "options_profileindicator": {
+    "message": "Show <span class=\"help\" title=\"If the OP has participated in other threads, a red dot will be shown next to their username. If the OP's most recent posts have been read, an orange dot will be shown instead. You can hover the dot with your mouse in order to show the meaning of its color.\">whether the OP has participated in other threads</span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicatoralt": {
+    "message": "Show the number of questions and replies written by the OP within the last <span id='profileindicatoralt_months--container'></span> months next to their username.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_save": {
+    "message": "Save",
+    "description": "Button in the options page to save the settings"
+  },
+  "options_saved": {
+    "message": "Saved",
+    "description": "Message which appears in the options page when the settings are saved"
+  },
+  "options_experiments_title": {
+    "message": "Experiments",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "Welchrome! Below you'll a find a list of experiments: features which are in development, and are not quite ready for launch. They are highly experimental and so might break and/or cause issues, but if you're brave, please do enable the ones you're most interested in, and <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>give feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
+  "inject_links": {
+    "message": "Links",
+    "description": "Heading which we use before the 'previous post' link in a user profile in TW"
+  },
+  "inject_previousposts": {
+    "message": "Previous posts",
+    "description": "Link shown in a user profile (in TW) which points to a search showing the user's posts and messages"
+  },
+  "inject_previousposts_forum": {
+    "message": "Previous posts (in this forum)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in the current forum"
+  },
+  "inject_previousposts_all": {
+    "message": "Previous posts (in all forums)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in all forums"
+  },
+  "inject_profileindicator_loading": {
+    "message": "Loading...",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_first_post": {
+    "message": "This is the first thread created by the OP in this forum.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_read": {
+    "message": "The OP participated in other threads in this forum, but you've read the 5 most recent ones.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_unread": {
+    "message": "The OP participated in other threads in this forum, some of which you haven't read.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Number of questions and replies in the last $1 months.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_ccdarktheme_helper": {
+    "message": "Switch theme",
+    "description": "Tooltip for the dark mode switch."
+  },
+  "inject_extension_badge_helper": {
+    "message": "Added by $1",
+    "description": "Tooltip for the extension badge, which appears next to components injected by the extension."
+  },
+  "inject_lockbtn": {
+    "message": "Lock/unlock threads",
+    "description": "Tooltip of the 'lock/unlock threads' icon shown when selecting multiple threads in the Community Console. Also the title for the dialog shown after clicking this icon."
+  },
+  "inject_lockdialog_desc": {
+    "message": "Please, confirm the action you want to take by clicking the appropriate button below. Keep in mind this action will be performed in all the threads you have selected.",
+    "description": "Text in the 'lock/unlock threads' dialog, which asks the user for confirmation."
+  },
+  "inject_lockdialog_btn_lock": {
+    "message": "Lock",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_unlock": {
+    "message": "Unlock",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_cancel": {
+    "message": "Cancel",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_reload": {
+    "message": "Reload",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_close": {
+    "message": "Close",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_log_entry_beginning": {
+    "message": "Thread $1",
+    "description": "First part of the entry in the log of the 'lock/unlock threads' dialog. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_lock": {
+    "message": "Locked successfully.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_lock": {
+    "message": "An error occurred while locking ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_unlock": {
+    "message": "Unlocked successfully.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_unlock": {
+    "message": "An error occurred while unlocking ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_autorefresh_list_snackbar_message": {
+    "message": "There are new threads.",
+    "description": "Message shown in a snackbar when new threads are found in a thread list. Another button asks the user to refresh the list."
+  },
+  "inject_autorefresh_list_snackbar_action": {
+    "message": "Refresh",
+    "description": "Button shown in a snackbar asking users to refresh/reload the list to show the new threads."
+  }
+}
diff --git a/src/static/_locales/es/messages.json b/src/static/_locales/es/messages.json
new file mode 100644
index 0000000..1f70b4e
--- /dev/null
+++ b/src/static/_locales/es/messages.json
@@ -0,0 +1,250 @@
+{
+  "appName": {
+    "message": "Herramientas avanzadas para TW (anteriormente Scroll infinito)",
+    "description": "The extension's name"
+  },
+  "appNameBeta": {
+    "message": "Herramientas avanzadas para TW (beta)",
+    "description": "The extension's name for the beta version"
+  },
+  "appNameGecko": {
+    "message": "Herramientas avanzadas para TW",
+    "description": "The extension's name (with a 50 character limit for Gecko)"
+  },
+  "appDescription": {
+    "message": "Haz scroll infinito y obtén otras mejoras en los Foros de Google y la Consola de la Comunidad",
+    "description": "The extension's description"
+  },
+  "options_list": {
+    "message": "Activa el scroll infinito en las listas de hilos.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_thread": {
+    "message": "Carga automáticamente mensajes en grupos pequeños cuando hagas scroll por un hilo.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_threadall": {
+    "message": "Carga automáticamente todos los mensajes a la vez cuando hagas scroll por un hilo.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancements": {
+    "message": "Mejoras adicionales",
+    "description": "Heading for several options that can be enabled in the options page"
+  },
+  "options_fixedtoolbar": {
+    "message": "Mantén visible la barra de herramientas de la lista de hilos en la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_redirect": {
+    "message": "Redirige todos los hilos abiertos en TW a la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_loaddrafts":{
+    "message": "Activa la <i>flag</i> <code class=\"help\" title=\"Esta flag permite a la Consola de la Comunidad cargar una respuesta previamente autoguardada cuando abres un hilo y haces clic en el botón de responder.\">enableLoadingDraftMessages</code> de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_experimental_label": {
+    "message": "(experimental)",
+    "description": "Label which is placed next to an option to indicate that it may not work well or may break at any time"
+  },
+  "options_history": {
+    "message": "Muestra un enlace \"historial de publicaciones\" en los perfiles de usuario.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_escalatethreads":{
+    "message": "Activa la <i>flag</i> <code class=\"help\" title=\"Esta flag activa la funcionalidad de escalar hilos del foro público a los privados en la Consola de la Comunidad.\">enableEscalateThread</code> de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_movethreads":{
+    "message": "Activa la <i>flag</i> <code class=\"help\" title=\"Esta flag activa la funcionalidad de mover hilos de un foro donde seas un EP a otro foro en la Consola de la Comunidad.\">enableMoveThread</code> de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_increasecontrast":{
+    "message": "Incrementa el contraste entre los hilos leídos y no leídos en la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme": {
+    "message": "Activa el tema oscuro en la Consola de la Comunidad, controlado por <span id='ccdarktheme_mode--container'></span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdarktheme_mode_switch": {
+    "message": "un botón en la consola",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccdarktheme_mode_system": {
+    "message": "la configuración del modo oscuro del SO",
+    "description": "Select option added in #ccdarktheme_mode--container, in the options_ccdarktheme string"
+  },
+  "options_ccforcehidedrawer": {
+    "message": "Esconde siempre la barra lateral izquierda en la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_ccdragndropfix": {
+    "message": "Permite arrastrar marcadores al editor de texto de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_batchlock": {
+    "message": "Añade la opción para bloquear varios hilos en la lista de hilos de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_enhancedannouncementsdot": {
+    "message": "Muestra el punto que notifica que hay anuncios sin leer de manera más prominente en la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_repositionexpandthread": {
+    "message": "Pon el botón \"expandir hilo\" a la izquierda del todo en las listas de hilos de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicator_header": {
+    "message": "Punto indicador",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_threadlistavatars": {
+    "message": "Muestra fotos de perfil en las listas de hilos de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_autorefreshlist": {
+    "message": "Actualiza las listas de hilos de la Consola de la Comunidad automáticamente.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_disableunifiedprofiles": {
+    "message": "Desactiva forzosamente el experimento <code class=\"help\" title=\"Este experimento, cuando está activado, introduce un rediseño de la interfaz de los perfiles que también unifica todos los perfiles en cada foro en uno único.\">SMEI_UNIFIED_PROFILES</code> en la Consola de la Comunidad.",
+    "description": "Link to learn more about the profile indicator feature"
+  },
+  "options_forcemarkasread": {
+    "message": "Cada vez que abras un hilo en la Consola de la Comunidad, enviar automáticamente una petición para marcarlo como leído. Esta es una opción temporal usada como <i>workaround</i> de <a href=\"https://support.google.com/s/community/forum/51488989/thread/114559215\" target=\"_blank\" rel=\"noreferrer noopener\">este bug</a>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info sobre las 2 opciones anteriores",
+    "description": "Link to learn more about the profile indicator feature"
+  },
+  "options_profileindicator": {
+    "message": "Muestra <span class=\"help\" title=\"Si el autor ha participado en otros hilos, se mostrará un punto rojo al lado de su nombre de usuario. Si las publicaciones más recientes se han leído, se mostrará un punto naranja. Puedes poner tu ratón encima del punto para mostrar qué quiere decir su color.\">si el autor del hilo ha participado en otros hilos</span>.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_profileindicatoralt": {
+    "message": "Muestra el número de preguntas y respuestas escritas por el autor del hilo durante los últimos <span id='profileindicatoralt_months--container'></span> meses al lado de su nombre de usuario.",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_stickysidebarheaders": {
+    "message": "Hacer que los encabezados de la barra lateral de la Consola de la Comunidad se queden pegados arriba (+info en <code>pekb/thread/60784834</code>).",
+    "description": "Feature checkbox in the options page"
+  },
+  "options_save": {
+    "message": "Guardar",
+    "description": "Button in the options page to save the settings"
+  },
+  "options_saved": {
+    "message": "Guardado",
+    "description": "Message which appears in the options page when the settings are saved"
+  },
+  "options_experiments_title": {
+    "message": "Experimentos",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "<i>Welchrome!</i> Aquí abajo encontrarás una lista de experimentos: funciones que están en desarrollo y que todavía no están listas del todo para lanzarse. Son altamente experimentales y podrían romperse y/o causar problemas, pero si eres valiente, por favor activa las que más te interesen, ¡y <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>envia feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
+  "inject_links": {
+    "message": "Enlaces",
+    "description": "Heading which we use before the 'previous post' link in a user profile in TW"
+  },
+  "inject_previousposts": {
+    "message": "Historial de publicaciones",
+    "description": "Link shown in a user profile (in TW) which points to a search showing the user's posts and messages"
+  },
+  "inject_previousposts_forum": {
+    "message": "Historial de publicaciones (en este foro)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in the current forum"
+  },
+  "inject_previousposts_all": {
+    "message": "Historial de publicaciones (en todos los foros)",
+    "description": "Link shown in a user profile (in the Community Console) which points to a search showing the user's posts and messages in all forums"
+  },
+  "inject_profileindicator_loading": {
+    "message": "Cargando...",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_first_post": {
+    "message": "Este es el primer hilo de este foro creado por este usuario.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_read": {
+    "message": "Este usuario ha participado en otros hilos de este foro, pero ya has leído los 5 últimos donde ha participado.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicator_other_posts_unread": {
+    "message": "Este usuario ha participado en otros hilos de este foro, algunos de los cuales no has leído.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Número de preguntas y respuestas en los últimos $1 meses.",
+    "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_ccdarktheme_helper": {
+    "message": "Cambiar tema",
+    "description": "Tooltip for the dark mode switch."
+  },
+  "inject_extension_badge_helper": {
+    "message": "Añadido por $1",
+    "description": "Tooltip for the extension badge, which appears next to components injected by the extension."
+  },
+  "inject_lockbtn": {
+    "message": "Bloquear/desbloquear hilos",
+    "description": "Tooltip of the 'lock/unlock threads' icon shown when selecting multiple threads in the Community Console. Also the title for the dialog shown after clicking this icon."
+  },
+  "inject_lockdialog_desc": {
+    "message": "Por favor, confirma la acción que quieres tomar haciendo clic en el botón correspondiente aquí debajo. Ten en cuenta que esta acción se tomará sobre todos los hilos que hayas seleccionado.",
+    "description": "Text in the 'lock/unlock threads' dialog, which asks the user for confirmation."
+  },
+  "inject_lockdialog_btn_lock": {
+    "message": "Bloquear",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_unlock": {
+    "message": "Desbloquear",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_cancel": {
+    "message": "Cancelar",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_reload": {
+    "message": "Recargar",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_btn_close": {
+    "message": "Cerrar",
+    "description": "Button in the 'lock/unlock threads' dialog."
+  },
+  "inject_lockdialog_log_entry_beginning": {
+    "message": "Hilo $1",
+    "description": "First part of the entry in the log of the 'lock/unlock threads' dialog. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_lock": {
+    "message": "Bloqueado correctamente.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_lock": {
+    "message": "Ha ocurrido un error bloqueándolo ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_success_unlock": {
+    "message": "Desbloqueado correctamente.",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_lockdialog_log_entry_error_unlock": {
+    "message": "Ha ocurrido un error desbloqueándolo ($1).",
+    "description": "Second part of the entry in the log of the 'lock/unlock threads' dialog, when the log entry states that the thread was *not* *un*locked successfully. Log entries are of the form '{first_part}: {second_part}'. For example: 'Thread 164: Locked successfully'."
+  },
+  "inject_autorefresh_list_snackbar_message": {
+    "message": "Hay nuevos hilos.",
+    "description": "Message shown in a snackbar when new threads are found in a thread list. Another button asks the user to refresh the list."
+  },
+  "inject_autorefresh_list_snackbar_action": {
+    "message": "Actualizar",
+    "description": "Button shown in a snackbar asking users to refresh/reload the list to show the new threads."
+  }
+}
diff --git a/src/static/_locales/ru/OWNERS b/src/static/_locales/ru/OWNERS
new file mode 100644
index 0000000..f7cf3f7
--- /dev/null
+++ b/src/static/_locales/ru/OWNERS
@@ -0,0 +1 @@
+ai@contributor.pw
diff --git a/src/static/_locales/ru/messages.json b/src/static/_locales/ru/messages.json
new file mode 100644
index 0000000..e616544
--- /dev/null
+++ b/src/static/_locales/ru/messages.json
@@ -0,0 +1,210 @@
+{
+  "appName": {
+    "message": "TW Power Tools (ранее Infinite Scroll)",
+    "description": "Имя расширения"
+  },
+  "appNameBeta": {
+    "message": "TW Power Tools (beta)",
+    "description": "Имя расширения для бета-версии"
+  },
+  "appNameGecko": {
+    "message": "TW Power Tools",
+    "description": "Имя расширения (не более 50 символов для Gecko)"
+  },
+  "appDescription": {
+    "message": "Получите бесконечную прокрутку и другие улучшения на Форумах Google и в Консоли сообщества",
+    "description": "Описание расширения"
+  },
+  "options_list": {
+    "message": "Включить бесконечную прокрутку в списках тем.",
+    "description": "Опция на странице настроек"
+  },
+  "options_thread": {
+    "message": "Автоматическая частичная загрузка сообщений внутри тем при прокрутке вниз.",
+    "description": "Опция на странице настроек"
+  },
+  "options_threadall": {
+    "message": "Автоматически загружать все сообщения сразу в темах при прокрутке вниз.",
+    "description": "Опция на странице настроек"
+  },
+  "options_enhancements": {
+    "message": "Дополнительные возможности",
+    "description": "Заголовок для нескольких опций, которые могут быть включены на странице опций"
+  },
+  "options_fixedtoolbar": {
+    "message": "Закрепить панель инструментов в списках тем в КС [Консоли сообщества].",
+    "description": "Опция на странице настроек"
+  },
+  "options_redirect": {
+    "message": "Перенаправлять все темы, открытые в TW, в КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_loaddrafts": {
+    "message": "Активировать <code class=\"help\" title=\"Этот флаг позволяет КС загружать ранее сохраненный ответ при загрузке цепочки и нажатии кнопки ответа.\">enableLoadingDraftMessages</code> флаг КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_experimental_label": {
+    "message": "(эксперимент)",
+    "description": "Метка, которая размещена рядом с опцией, чтобы указать, что она может не работать или может сломаться в любое время"
+  },
+  "options_history": {
+    "message": "Показывать ссылку \"Сообщения пользователя\" в профилях пользователей.",
+    "description": "Опция на странице настроек"
+  },
+  "options_escalatethreads": {
+    "message": "Включает <code class=\"help\" title=\"Этот флаг активирует функциональность, которая позволяет эксперту по продукту эскалировать потоки из открытых форумов в частные в КС.\">enableEscalateThread</code> флаг КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_movethreads": {
+    "message": "Включает <code class=\"help\" title=\"Этот флаг активирует функциональность, которая позволяет эксперту по продукту перемещать потоки из форума, где они в данный момент являются PE, в другой форум в КС.\">enableMoveThread</code> флаг КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_stickysidebarheaders": {
+    "message": "Сделайте так, чтобы заголовки на боковой панели КС прилипали к верху при прокрутке (+info at <code>pekb/thread/60784834</code>).",
+    "description": "Опция на странице настроек"
+  },
+  "options_increasecontrast": {
+    "message": "Увеличить контраст между прочитанными и непрочитанными цепочками в списке тем КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_ccdarktheme": {
+    "message": "Разрешить темную тему в КС, используя <span id='ccdarktheme_mode--container'></span>.",
+    "description": "Опция на странице настроек"
+  },
+  "options_ccdarktheme_mode_switch": {
+    "message": "переключатель в КС",
+    "description": "Выбирает опцию, добавленную в #ccdarktheme_mode--container, в строке options_ccdarktheme"
+  },
+  "options_ccdarktheme_mode_system": {
+    "message": "темная тема режима настроек ОС",
+    "description": "Выбирает опцию, добавленную в #ccdarktheme_mode--container, в строке options_ccdarktheme"
+  },
+  "options_ccforcehidedrawer": {
+    "message": "Всегда скрывать дрор-панель (левую колонку) в КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_ccdragndropfix": {
+    "message": "Разрешить перетаскивать закладки в текстовый редактор КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_batchlock": {
+    "message": "Добавить опцию одновременной блокировки нескольких веток в спике тем КС.",
+    "description": "Опция на странице настроек"
+  },
+  "options_profileindicator_header": {
+    "message": "Точка индикации",
+    "description": "Заголовок для параметров функции индикатора профиля"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+инфо о двух предыдущих вариантах",
+    "description": "Ссылка, чтобы узнать больше о функции индикатора профиля"
+  },
+  "options_profileindicator": {
+    "message": "Показать, <span class=\"help\" title=\"Если автор темы участвовал в других обсуждениях рядом с его именем будет отображаться красная точка. Если были прочитаны самые последние сообщения автора темы, будет показана оранжевая точка. Вы можете навести курсор мыши на точку, чтобы показать значение ее цвета.\"> участвовал ли автор темы в других обсуждениях</span>.",
+    "description": "Опция на странице настроек"
+  },
+  "options_profileindicatoralt": {
+    "message": "Показывать количество вопросов и ответов, написанных автором темы за последние <span id='profileindicatoralt_months--container'></span> месяцев рядом с их именем пользователя.",
+    "description": "Опция на странице настроек"
+  },
+  "options_save": {
+    "message": "Сохранить",
+    "description": "Кнопка на странице настроек для сохранения настроек"
+  },
+  "options_saved": {
+    "message": "Сохранено",
+    "description": "Сообщение, которое появляется на странице параметров после сохранения настроек"
+  },
+  "inject_links": {
+    "message": "Ссылки",
+    "description": "Заголовок, который мы используем перед ссылкой \"предыдущий пост\" в профиле пользователя в TW"
+  },
+  "inject_previousposts": {
+    "message": "Сообщения пользователя",
+    "description": "Ссылка отображаемая в профиле пользователя (в TW и в КС), которая указывает на поиск, показывающий посты и ответы пользователя"
+  },
+  "inject_previousposts_forum": {
+    "message": "Предыдущие темы (на этом форуме)",
+    "description": "Ссылка, отображаемая в профиле пользователя (в КС), которая указывает на поиск сообщений пользователя на текущем форуме."
+  },
+  "inject_previousposts_all": {
+    "message": "Предыдущие темы (на всех форумах)",
+    "description": "Ссылка, отображаемая в профиле пользователя (в КС), которая указывает на поиск сообщений пользователя на всех форумах."
+  },
+  "inject_profileindicator_loading": {
+    "message": "Загрузка...",
+    "description": "Подсказка для точки индикатора профиля"
+  },
+  "inject_profileindicator_first_post": {
+    "message": "Это первая ветка, созданная автором темы на этом форуме.",
+    "description": "Подсказка для точки индикатора профиля"
+  },
+  "inject_profileindicator_other_posts_read": {
+    "message": "Автор темы участвовал в других обсуждениях этого форума, вы прочитали 5 самых последних.",
+    "description": "Подсказка для точки индикатора профиля"
+  },
+  "inject_profileindicator_other_posts_unread": {
+    "message": "Автор темы участвовал в других обсуждениях этого форума, вы читали не все эти темы.",
+    "description": "Подсказка для точки индикатора профиля"
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Количество вопросов и ответов за последнее время в месяцах: $1.",
+    "description": "Подсказка для точки индикатора профиля"
+  },
+  "inject_ccdarktheme_helper": {
+    "message": "Переключить тему",
+    "description": "Подсказка для переключателя темы"
+  },
+  "inject_extension_badge_helper": {
+    "message": "Добавлено с помощью $1",
+    "description": "Всплывающая подсказка для значка расширения, которая появляется рядом с компонентами, добавленными расширением"
+  },
+  "inject_lockbtn": {
+    "message": "Заблокировать/разблокировать темы",
+    "description": "Всплывающая подсказка значка \"заблокировать/разблокировать темы\", отображаемая при выборе нескольких тем в КС. Также заголовок диалогового окна, отображаемого после щелчка по этому значку"
+  },
+  "inject_lockdialog_desc": {
+    "message": "Подтвердите действие, которое вы хотите предпринять, нажав соответствующую кнопку ниже. Имейте в виду, что это действие будет выполнено для всех выбранных вами тем.",
+    "description": "Текст в диалоговом окне \"Заблокировать/разблокировать темы\", которое запрашивает у пользователя подтверждение."
+  },
+  "inject_lockdialog_btn_lock": {
+    "message": "Заблокировать",
+    "description": "Кнопка в диалоговом окне блокировки/разблокировки тем"
+  },
+  "inject_lockdialog_btn_unlock": {
+    "message": "Разблокировать",
+    "description": "Кнопка в диалоговом окне блокировки/разблокировки тем"
+  },
+  "inject_lockdialog_btn_cancel": {
+    "message": "Отмена",
+    "description": "Кнопка в диалоговом окне блокировки/разблокировки тем"
+  },
+  "inject_lockdialog_btn_reload": {
+    "message": "Перезагрузить",
+    "description": "Кнопка в диалоговом окне блокировки/разблокировки тем"
+  },
+  "inject_lockdialog_btn_close": {
+    "message": "Закрыть",
+    "description": "Кнопка в диалоговом окне блокировки/разблокировки тем"
+  },
+  "inject_lockdialog_log_entry_beginning": {
+    "message": "Тема $1",
+    "description": "Первая часть записи в журнале диалога \"блокировки/разблокировки тем\". Записи журнала имеют форму \"{first_part}: {second_part}\". Например: \"Тема 164: успешно заблокирована\""
+  },
+  "inject_lockdialog_log_entry_success_lock": {
+    "message": "Locked successfully.",
+    "description": "Вторая часть записи в журнале диалога \"блокировки/разблокировки тем\", когда в записи журнала указано, что тема была успешно заблокирована. Записи журнала имеют форму \"{first_part}: {second_part}\". Например: \"Тема 164: успешно заблокирована\""
+  },
+  "inject_lockdialog_log_entry_error_lock": {
+    "message": "An error occurred while locking ($1).",
+    "description": "Вторая часть записи в журнале диалога \"блокировки/разблокировки тем\", когда в записи журнала указано, что тема *не* была успешно заблокирована. Записи журнала имеют форму \"{first_part}: {second_part}\". Например: \"Тема 164: успешно заблокирована\""
+  },
+  "inject_lockdialog_log_entry_success_unlock": {
+    "message": "Unlocked successfully.",
+    "description": "Вторая часть записи в журнале диалога \"блокировки/разблокировки тем\", когда в записи журнала указано, что тема была успешно *раз*блокирована. Записи журнала имеют форму \"{first_part}: {second_part}\". Например: \"Тема 164: успешно заблокирована\""
+  },
+  "inject_lockdialog_log_entry_error_unlock": {
+    "message": "An error occurred while unlocking ($1).",
+    "description": "Вторая часть записи в журнале диалога \"блокировки/разблокировки тем\", когда в записи журнала указано, что тема была *не* была успешно *раз*блокирована. Записи журнала имеют форму \"{first_part}: {second_part}\". Например: \"Тема 164: успешно заблокирована\""
+  }
+}
diff --git a/src/static/css/autorefresh_list.css b/src/static/css/autorefresh_list.css
new file mode 100644
index 0000000..56d3fda
--- /dev/null
+++ b/src/static/css/autorefresh_list.css
@@ -0,0 +1,79 @@
+.TWPT-pane-for-snackbar {
+  height: 48px;
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 9999;
+  display: flex;
+  pointer-events: none;
+}
+
+.TWPT-snackbar {
+  display: flex;
+  align-self: flex-end;
+  justify-content: center;
+  flex: 1;
+  font-size: 15px;
+  font-weight: 400;
+  overflow: hidden;
+  pointer-events: none!important;
+}
+
+.TWPT-snackbar .TWPT-animation-container {
+  max-width: 616px;
+  min-width: 320px;
+  overflow: hidden;
+  pointer-events: auto;
+  transition: all 218ms cubic-bezier(.4, 0, 1, 1);
+}
+
+.TWPT-notification-bar {
+  background: #323232;
+  border-radius: 2px;
+  box-sizing: border-box;
+  color: #fff;
+  display: flex;
+  height: 48px;
+  padding: 6px 24px;
+}
+
+.TWPT-notification-bar focus-trap {
+  display: flex;
+  width: 100%;
+}
+
+.TWPT-notification-bar .TWPT-focus-content-wrapper {
+  display: flex;
+  height: 100%;
+  width: 100%;
+  max-height: inherit;
+  min-height: inherit;
+}
+
+.TWPT-notification-bar .TWPT-badge {
+  margin: auto 10px auto 0;
+}
+
+.TWPT-notification-bar .TWPT-message {
+  display: flex;
+  margin: auto;
+  flex: 1;
+  white-space: pre-line;
+}
+
+.TWPT-notification-bar .TWPT-action {
+  display: flex;
+  align-items: center;
+  color: #c6dafc;
+  cursor: pointer;
+  font-size: 14px;
+  font-weight: 700;
+  margin-left: 24px;
+  padding: 0;
+  text-transform: uppercase;
+}
+
+.TWPT-snackbar.TWPT-hidden {
+  display: none;
+}
diff --git a/src/static/css/batchlock_inject.css b/src/static/css/batchlock_inject.css
new file mode 100644
index 0000000..da50618
--- /dev/null
+++ b/src/static/css/batchlock_inject.css
@@ -0,0 +1,3 @@
+material-button[debugid="batchlock"] {
+  padding-left: 0;
+}
diff --git a/src/static/css/ccdarktheme.css b/src/static/css/ccdarktheme.css
new file mode 100644
index 0000000..5524f8a
--- /dev/null
+++ b/src/static/css/ccdarktheme.css
@@ -0,0 +1,969 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto:700');
+
+:root {
+  --TWPT-primary-text: #e8eaed;
+  --TWPT-primary-text-alt: var(--TWPT-primary-text);
+  --TWPT-secondary-text: #9aa0a6;
+  --TWPT-primary-background: #202124;
+  --TWPT-secondary-background: #28292c;
+  --TWPT-active-background: #3c4043;
+  --TWPT-card-border: #5f6368;
+  --TWPT-subtle-border: #383735;
+  --TWPT-link: #8ab4f8;
+  --TWPT-highlighted-item-background: rgba(255, 255, 255, .08);
+  --TWPT-thread-read-background: var(--TWPT-highlighted-item-background);
+  --TWPT-drawer-background: #2d2e30;
+  --TWPT-button-background: #3c3e42;
+  --TWPT-input-underline: rgba(255, 255, 255, .28);
+  --TWPT-blue-A100: #82B1FF;
+  --TWPT-bad-text: #f6aea9;
+  --TWPT-good-text: #34a853;
+}
+
+body {
+  color: var(--TWPT-primary-text);
+  background-color: var(--TWPT-primary-background)!important;
+}
+
+p {
+  color: var(--TWPT-primary-text);
+}
+
+body.ec a {
+  color: var(--TWPT-link);
+}
+
+/* Header */
+.material-content > header {
+  background-color: var(--TWPT-primary-background)!important;
+}
+
+.material-content > header .app-title-button,
+    .material-content > header .app-title-text {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+.material-content > header material-button,
+    .material-content > header material-button material-icon,
+    .material-content > header notification-bell material-icon {
+  color: rgba(255, 255, 255, .87)!important;
+}
+
+.search-box {
+  background-color: #313235!important;
+}
+
+.search-box .clear-icon {
+  opacity: 0.8;
+  color: rgba(255, 255, 255, .87)!important;
+}
+
+.material-content > header .bell.mixin {
+  fill: rgba(255, 255, 255, .87)!important;
+}
+
+/* Drawer */
+material-drawer, material-drawer .panel, material-list-item {
+  background-color: var(--TWPT-drawer-background)!important;
+}
+
+material-list-item:hover, material-list-item:focus, material-list-item.item--active {
+  background-color: var(--TWPT-active-background)!important;
+}
+
+material-drawer .panel {
+  border-bottom-color: #25231f!important;
+}
+
+material-drawer .drawer-section-title, material-drawer .header > material-icon {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+material-drawer material-list-item,
+    material-drawer material-list-item .title {
+  color: #d2cecb!important;
+}
+
+material-drawer ec-forum-drawer-item material-checkbox material-icon {
+  filter: brightness(1.5);
+}
+
+/* Header menus */
+.popup material-list-item {
+  color: #d2cecb!important;
+}
+
+/* Filters (ec-query-builder) */
+material-condition-builder .compound-condition-operator {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+material-condition-builder .compound-condition-operator::before,
+    material-condition-builder .compound-condition-operator::after {
+  border-left-color: rgba(255, 255, 255, .20)!important;
+}
+
+/* Selector */
+
+/*
+ * This applies not only to the selector but also to the ec-work-state-picker.
+ */
+.popup-wrapper, material-list, .popup .item-group-list {
+  background-color: var(--TWPT-drawer-background)!important;
+}
+
+material-list [group]:not(.empty) + *:not(script):not(template):not(.empty),
+    .popup .item-group-list [group]:not(.empty) + *:not(script):not(template):not(.empty) {
+  box-shadow: inset 0 8px 0 0 var(--TWPT-drawer-background)!important;
+  border-top-color: #1f1f1f!important;
+}
+
+material-list material-select-item:hover,
+    material-list material-select-item:focus,
+    material-list material-select-dropdown-item:hover,
+    material-list material-select-dropdown-item:focus,
+    material-list material-select-dropdown-item.active,
+    material-list material-select-dropdown-item:not(.multiselect).selected,
+    .popup .item-group-list material-select-item:hover,
+    .popup .item-group-list material-select-item:focus,
+    .popup .item-group-list material-select-item.active,
+    .popup .item-group-list material-select-dropdown-item:hover,
+    .popup .item-group-list material-select-dropdown-item:focus,
+    .popup .item-group-list material-select-dropdown-item.active,
+    .popup .item-group-list material-select-dropdown-item:not(.multiselect).selected {
+  background-color: var(--TWPT-highlighted-item-background)!important;
+}
+
+material-list .menu-item-label,
+    material-list .label,
+    material-list .text-segment,
+    .popup .item-group-list .menu-item-label,
+    .popup .item-group-list .label,
+    .popup .item-group-list .text-segment {
+  color: rgba(255, 255, 255, .87)!important;
+}
+
+.popup .item-group-list material-icon {
+  color: rgba(255, 255, 255, .7)!important;
+}
+
+material-list [group] > [label] {
+  color: #8a8a8a!important;
+}
+
+/* This is shown in the new thread view */
+ec-forum-language-picker .labeled-select .select-label {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+/* Main */
+.main {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Border color for the page title bar (most pages use .title-bar although
+ * some use .page-header).
+ */
+main .title-bar, main .page-header {
+  border-bottom-color: var(--TWPT-subtle-border)!important;
+}
+
+main .title-bar .title, main .page-header h1, main .header h1 {
+  color: var(--TWPT-primary-text)!important;
+}
+
+.card {
+  background-color: var(--TWPT-secondary-background)!important;
+  color: var(--TWPT-primary-text)!important;
+  border-color: var(--TWPT-card-border)!important;
+}
+
+.card .card-title, .card, .card-section-title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+.card .card-section-hint, .card .card-section-checkbox-hint {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+/* Action bar */
+.material-content .action-bar material-button,
+    ec-bulk-actions material-button,
+    ec-back-button material-button,
+    .sort-options material-button {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+.material-content .action-bar .review-button.reviewing {
+  color: #1a73e8!important;
+  background: #dae7ff!important;
+}
+
+.material-content .action-bar material-button.starred {
+  color: #fbbc04!important;
+}
+
+/* ec-work-state-picker */
+ec-work-state-picker > button {
+  color: var(--TWPT-secondary-text)!important;
+  background-color: var(--TWPT-button-background)!important;
+  border-color: var(--TWPT-subtle-border)!important;
+}
+
+.material-popup-content .popup-content .title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Thread list */
+ec-thread-list ul.thread-group {
+  background-color: var(--TWPT-primary-background)!important;
+}
+
+ec-thread-list ec-bulk-actions, ec-thread-list ec-thread-summary material-expansionpanel {
+  border-bottom-color: var(--TWPT-subtle-border)!important;
+}
+
+ec-thread-summary material-expansionpanel.read:not(.checked) {
+  background-color: var(--TWPT-thread-read-background)!important;
+}
+
+ec-thread-summary material-expansionpanel.read .title span:not(.icon) {
+  opacity: 0.8;
+}
+
+ec-thread-summary material-expansionpanel.read .title .icon {
+  opacity: 0.48!important;
+}
+
+ec-thread-summary material-expansionpanel.checked {
+  background-color: #2c4b77!important;
+}
+
+ec-thread-summary material-expansionpanel .title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-thread-summary material-expansionpanel ec-second-summary-line,
+    material-expansionpanel .header-content,
+    material-expansionpanel ec-thread-counts > span:not(.recommended-answers),
+    material-expansionpanel .duplicate-label {
+  color: #928e89!important;
+}
+
+material-expansionpanel .removed-label {
+  color: var(--TWPT-bad-text)!important;
+}
+
+ec-thread-summary material-expansionpanel ec-safe-html.body,
+    material-expansionpanel .issue-tracking-work-state {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-thread-summary material-expansionpanel .action material-button,
+    ec-thread-list material-checkbox material-icon,
+    ec-thread-summary material-expansionpanel .star-button:not(.starred) {
+  color: #696867!important; /* Custom value to match previous behavior */
+  opacity: 1!important;
+}
+
+ec-thread-list material-checkbox material-icon.filled {
+  color: #62a5ff!important; /* Custom value to contrast well with the background */
+}
+
+ec-thread-list material-checkbox:focus:not(.disabled).gm-icons .icon,
+    .gm-icons ec-thread-list material-checkbox:focus:not(.disabled) .icon,
+    ec-thread-list material-checkbox:hover:not(.disabled).gm-icons .icon,
+    .gm-icons ec-thread-list material-checkbox:hover:not(.disabled) .icon {
+  color: #807d7c!important; /* custom value */
+}
+
+ec-thread-list material-checkbox:focus:not(.disabled).gm-icons .icon.filled,
+    .gm-icons ec-thread-list material-checkbox:focus:not(.disabled) .icon.filled,
+    ec-thread-list material-checkbox:hover:not(.disabled).gm-icons .icon.filled,
+    .gm-icons ec-thread-list material-checkbox:hover:not(.disabled) .icon.filled {
+  color: #92c1ff!important; /* custom value */
+}
+
+/* .gm-icons is added by the SMEI_GOOGLE_MATERIAL_ICONS experiment. */
+.gm-icons ec-thread-list material-checkbox .icon-container::after{
+  background-color: #dfdedb!important; /* custom value */
+}
+
+/* Thread view (including review/live review components) */
+ec-question, .heading + .group, ec-message {
+  background-color: var(--TWPT-secondary-background)!important;
+}
+
+ec-thread .no-review-needed {
+  background-color: #155829!important;
+  border-color: var(--TWPT-subtle-border)!important;
+}
+
+/* Review bar shown above the main message or replies */
+ec-review-bar {
+  background-color: var(--TWPT-active-background)!important;
+}
+
+ec-review-bar material-chip {
+  background-color: var(--TWPT-button-background)!important;
+}
+
+ec-review-bar material-chip:not(.relevant-active):not(.active) {
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-review-bar material-chip:not(.relevant-active):not(.active) material-icon {
+  color: var(--TWPT-primary-text)!important;
+  fill: var(--TWPT-primary-text)!important;
+}
+
+ec-review-bar material-chip:not(.relevant-active):not(.active) .content {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-review-bar material-chip.relevant-active {
+  border-color: var(--TWPT-good-text)!important;
+}
+
+ec-review-bar material-chip.relevant-active material-icon {
+  color: var(--TWPT-good-text)!important;
+  fill: var(--TWPT-good-text)!important;
+}
+
+ec-review-bar material-chip.relevant-active .content {
+  color: var(--TWPT-good-text)!important;
+}
+
+ec-review-bar material-chip.active {
+  border-color: var(--TWPT-bad-text)!important;
+}
+
+ec-review-bar material-chip.active material-icon {
+  color: var(--TWPT-bad-text)!important;
+  fill: var(--TWPT-bad-text)!important;
+}
+
+ec-review-bar material-chip.active .content {
+  color: var(--TWPT-bad-text)!important;
+}
+
+ec-message-header .header, ec-question .state, ec-question ec-thread-counts > span, ec-message ec-thread-counts > span {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-question .alert, ec-message .alert {
+  background-color: var(--TWPT-active-background)!important;
+}
+
+ec-question .alert material-icon, ec-message .alert material-icon {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-question .alert ec-icon, ec-message .alert ec-icon {
+  color: var(--TWPT-primary-text)!important;
+  fill: var(--TWPT-primary-text)!important;
+}
+
+ec-question .title {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-user-link .name-text {
+  color: var(--TWPT-link)!important;
+}
+
+ec-message-header ec-avatar svg, ec-message-header .role,
+    ec-message .footer .role {
+  filter: brightness(1.5);
+}
+
+ec-question .body, ec-message .body {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-question .thread-insert {
+  background: none!important;
+}
+
+ec-question .details-heading {
+  color: var(--TWPT-primary-text-alt) !important;
+}
+
+ec-question .state-chips material-chip {
+  background-color: var(--TWPT-button-background)!important;
+  box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2),
+      0 1px 1px 0 rgba(0, 0, 0, 0.14),
+      0 1px 3px 0 rgba(0, 0, 0, 0.12);
+}
+
+ec-question .state-chips material-chip .content {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-question .footer,
+    ec-ask-flow .content-disclaimer {
+  color: var(--TWPT-primary-text)!important;
+  background-color: var(--TWPT-active-background)!important;
+  border-top-color: var(--TWPT-card-border)!important;
+}
+
+.heading {
+  color: var(--TWPT-primary-text)!important;
+}
+
+.heading + .group,
+    .load-more-bar,
+    ec-message:not(:first-child),
+    .load-more-bar .load-more-button,
+    .load-more-bar .load-all-button {
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-message .type {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-message .footer ec-relative-time,
+    ec-message .footer ec-safe-html {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-message .helpful-prompt {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-question .me-too-button,
+    ec-question .subscribe-button,
+    ec-message .upvote-button,
+    ec-message .downvote-button {
+  color: var(--TWPT-secondary-text)!important;
+  background-color: var(--TWPT-button-background)!important;
+}
+
+ec-message .alert {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-question .me-too-button.selected,
+    ec-question .subscribe-button.selected,
+    ec-message .upvote-button.selected,
+    ec-message .downvote-button.selected {
+  color: #4285f4!important;
+}
+
+.load-more-bar .load-more-button, .load-more-bar .load-all-button {
+  background-color: var(--TWPT-secondary-background)!important;
+}
+
+.locked-alert {
+  background-color: var(--TWPT-active-background)!important;
+  border: var(--TWPT-card-border)!important;
+}
+
+.locked-alert material-icon {
+  color: rgba(255, 255, 255, .38)!important;
+}
+
+ec-thread button.reply.collapsed {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-thread .finished-question {
+  background-color: var(--TWPT-active-background)!important;
+  border: var(--TWPT-card-border)!important;
+}
+
+ec-thread .finished-question .next-question {
+  color: var(--TWPT-link)!important;
+  border-color: var(--TWPT-link)!important;
+}
+
+.material-content .action-bar material-button.has-activity {
+  color: #1a73e8!important;
+}
+
+.material-content .action-bar material-button.showing-sidebar {
+  background-color: var(--TWPT-active-background)!important;
+}
+
+ec-activity-panel .title-bar h3 {
+  color: #e8eaf2!important;
+}
+
+ec-activity-panel ec-activity {
+  color: var(--TWPT-secondary-text)!important;
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-activity-panel ec-activity .message {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-activity-panel ec-activity .thread-title {
+  color: #c3bfbc!important;
+}
+
+/* Recommended answers - show in green where we've overwritten the colors */
+.recommended-answers {
+  color: #34a853!important;
+}
+
+/* Suggested answers - show brighter blue */
+.suggested-icon {
+  color: var(--TWPT-blue-A100)!important;
+}
+
+/* Help button (shown in the suggested answers header) */
+.explanation-icon material-icon {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+/* Help button tooltip */
+.popup .paper-container {
+  background: var(--TWPT-drawer-background)!important;
+}
+
+.explanation-icon material-icon:hover {
+  color: var(--TWPT-blue-A100)!important;
+}
+
+/* New thread and edit message views */
+material-stepper {
+  border-top-color: var(--TWPT-card-border)!important;
+}
+
+material-stepper .stepper-step-name,
+    material-stepper .purpose-title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-ask-flow .display-name-label,
+    material-stepper .additional-details-label,
+    material-stepper .detail-label {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+material-stepper .stepper-step[selectable=true]:focus {
+  background-color: rgba(60, 64, 67, .24)!important;
+}
+
+/* Compose thread view */
+ec-thread-composer .main-content .title-label,
+    ec-thread-composer .main-content .post-label {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-thread-composer .main-content .title-input,
+    ec-thread-composer .main-content ec-rich-text-editor {
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-thread-composer .main-content .title-alert {
+  color: var(--TWPT-bad-text)!important;
+}
+
+ec-thread-composer material-drawer {
+  background-color: inherit!important;
+}
+
+ec-thread-composer material-drawer .right-panel {
+  border-left-color: var(--TWPT-card-border)!important;
+}
+
+ec-thread-composer material-drawer .panel-section-title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-thread-composer material-drawer .panel-section-divider {
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-thread-composer material-drawer material-radio .radio-label {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-thread-composer material-drawer material-radio .radio-description,
+    ec-thread-composer material-drawer .settings-section .panel-section-item .select-label {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-thread-composer material-drawer material-checkbox material-icon {
+  opacity: 1!important;
+  color: rgba(255, 255, 255, .74)!important; /* Custom value */
+}
+
+ec-thread-composer material-drawer material-checkbox .content {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+/* Note: see thread view section. There is a rule for the thread creation
+ * footer.
+ **/
+
+/* Profile view */
+ec-user .main-card {
+  background-color: var(--TWPT-secondary-background)!important;
+}
+
+ec-user .abuse-alert-container {
+  background-color: var(--TWPT-active-background)!important;
+  border-top-right-radius: 8px!important;
+}
+
+ec-user .abuse-alert-container .abuse-alert {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-user ec-display-name-editor, ec-user .header .name {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-user .role {
+  filter: brightness(1.5);
+}
+
+ec-user bar-chart .axis text {
+  fill: rgba(255, 255, 255, .54)!important;
+}
+
+ec-user bar-chart .axis path,
+    ec-user bar-chart .axis .gridline,
+    ec-user bar-chart .axis line {
+  stroke: rgba(255, 255, 255, .12)!important;
+}
+
+ec-user bar-chart .axis line.axis-zero-tick,
+    ec-user bar-chart .axis.x .tick-mark {
+  stroke: rgba(255, 255, 255, .38)!important;
+}
+
+ec-user bar-chart .aplos-legend-entry {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+.aplos-hovercard {
+  background: var(--TWPT-secondary-background)!important;
+}
+
+.aplos-hovercard .title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+.aplos-hovercard .subtitle,
+    .aplos-donut-center .subtitle,
+    .aplos-hovercard .series,
+    .aplos-donut-center .series,
+    .aplos-hovercard .value,
+    .aplos-donut-center .value {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+/* Duplicate thread feature */
+.search-results ec-thread-option material-expansionpanel .panel {
+  background-color: var(--TWPT-primary-background)!important;
+}
+
+.search-results ec-thread-option material-expansionpanel.selected .panel,
+    .search-results ec-thread-option material-expansionpanel .panel > .main-header > .header.closed:hover,
+    .search-results ec-thread-option material-expansionpanel .panel > .main-header > .header.closed:focus {
+  background-color: #17191c!important;
+}
+
+/* Disabled buttons */
+material-button[disabled] {
+  color: rgba(255, 255, 255, .26)!important;
+}
+
+/* Material icons */
+ec-filter-drawer-item material-icon,
+    ec-filter-drawer-item ec-icon,
+    material-drawer .drawer-section material-icon,
+    material-drawer .drawer-section ec-icon,
+    material-list material-icon,
+    ec-query-builder material-icon,
+    material-radio .icon-container:not(.checked) material-icon,
+    ec-thread-summary material-expansionpanel .title material-icon,
+    .search-results ec-thread-option material-icon,
+    .search-results ec-thread-option ec-icon,
+    ec-rich-text-editor material-icon,
+    ec-editor-command material-icon,
+    ec-canned-responses ec-canned-response-row material-icon,
+    ec-ask-flow > .header material-button {
+  color: rgba(255, 255, 255, .87)!important;
+}
+
+material-drawer ec-icon,
+  .search-results ec-thread-option ec-icon,
+  ec-thread-summary material-expansionpanel .title ec-icon,
+  ec-announcements-menu-item ec-icon {
+  fill: rgba(255, 255, 255, .87)!important;
+}
+
+/* Dialogs */
+material-dialog, material-dialog .dialog-header {
+  background-color: var(--TWPT-primary-background)!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog .dialog-header {
+  background-color: #d2e3fc!important;
+}
+
+material-dialog .title {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog .dialog-header .title, ec-movable-dialog[showminimize] material-dialog header material-icon {
+  color: var(--TWPT-primary-background)!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog .header-notice, material-dialog .legal-prompt {
+  background-color: #170f01!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog .main .header-notice material-icon[icon="info_outline"] {
+  color: var(--TWPT-blue-A100)!important;
+}
+
+material-dialog .section-title,
+    material-dialog .select-label,
+    material-dialog .input-label,
+    material-dialog .btn-no,
+    ec-display-name-editor,
+    .forum-selection-label {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+material-dialog main > .user {
+  border-bottom-color: var(--TWPT-card-border)!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog footer > [footer] > .footer > [footer] > simple-html {
+  color: var(--TWPT-secondary-text)!important;
+  background-color: var(--TWPT-active-background)!important;
+  border-top-color: #25231f!important;
+}
+
+ec-movable-dialog[showminimize] material-dialog footer > [footer] > .footer > [footer] > simple-html a {
+  color: var(--TWPT-link)!important;
+}
+
+/* Keyboard shortcuts dialog */
+material-dialog .main.with-scroll-strokes table td {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Rich text editor */
+ec-editor-command material-button,
+    ec-formatting-popup material-button {
+  box-shadow: none!important;
+}
+
+ec-editor-command material-button.is-active {
+  background: var(--TWPT-active-background)!important;
+}
+
+ec-rich-text-editor .placeholder {
+  color: rgba(255, 255, 255, .38)!important;
+}
+
+ec-rich-text-editor .input-wrapper,
+    ec-rich-text-editor .spacer {
+  border-color: var(--TWPT-card-border)!important;
+}
+
+ec-rich-text-editor .input-wrapper.input-wrapper--focused {
+  border-color: var(--TWPT-blue-A100)!important;
+}
+
+ec-rich-text-editor .input {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-rich-text-editor .hint {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+material-select-searchbox + material-list material-list-item {
+  color: rgba(255, 255, 255, .87)!important;
+}
+
+ec-attachment .filename {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Thread insert */
+ec-thread-insert .title {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-thread-insert ec-thread-counts,
+    ec-thread-insert .details,
+    ec-thread-insert ec-relative-time {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+/* Text input */
+material-input input {
+  color: var(--TWPT-primary-text)!important;
+}
+
+material-input .label-text,
+    material-input .hint-text,
+    material-input .counter {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+material-input .underline .unfocused-underline {
+  background-color: var(--TWPT-input-underline)!important;
+}
+
+material-input .underline .focused-underline {
+  background-color: var(--TWPT-blue-A100)!important;
+}
+
+label .label {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Input underline */
+material-input .underline .unfocused-underline,
+    material-dropdown-select dropdown-button [buttondecorator] {
+  border-color: var(--TWPT-input-underline)!important;
+}
+
+/* Checkbox input */
+material-checkbox, material-checkbox .content {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+/* Material menu button and dropdown select */
+material-menu material-button, material-dropdown-select dropdown-button {
+  color: var(--TWPT-primary-text)!important;
+}
+
+material-dropdown-select dropdown-button .button.is-disabled .button-text,
+    material-dropdown-select dropdown-button .button.is-disabled material-icon {
+  color: rgba(255, 255, 255, .32)!important;
+}
+
+/* Announcements content */
+ec-announcements-content .header,
+    ec-announcements-content .no-announcements,
+    ec-announcements-content .announcement {
+  border-bottom-color: var(--TWPT-card-border)!important;
+}
+
+ec-announcements-content .header .title, ec-announcements-content .announcement-title {
+  color: var(--TWPT-primary-text-alt)!important;
+}
+
+ec-announcements-content .announcement-date {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-announcements-content .no-announcements-message {
+  color: #c3bfbc!important;
+}
+
+ec-announcements-content .view-all-link,
+    ec-announcements-content .read-more-button {
+  color: var(--TWPT-link)!important;
+}
+
+ec-announcements-content ::-webkit-scrollbar-thumb {
+  background-color: rgba(255, 255, 255, .26)!important;
+}
+
+ec-announcements-content ::-webkit-scrollbar-thumb:hover {
+  background-color: #4285f4!important;
+}
+
+/* Generic popup (for notification bell, account selector, etc.) */
+.popup-wrapper .header-text {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Notifications bell popup/panel */
+.notification-panel .header material-button {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+.notification-panel .cards-container .promo-message {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+.notification-panel .cards-container .promo-message .header-text {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Account selector */
+.popup-wrapper .profile .email {
+  color: rgba(255, 255, 255, .54)!important;
+}
+
+material-gaia-picker-footer {
+  color: rgba(255, 255, 255, .54)!important;
+  background-color: var(--TWPT-active-background)!important;
+}
+
+material-gaia-picker-footer material-button {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+/* Canned responses */
+ec-canned-responses .filter-label,
+    ec-canned-responses ec-canned-response-row .name {
+  color: var(--TWPT-primary-text)!important;
+}
+
+ec-canned-responses .label-row,
+    ec-canned-responses ec-canned-response-row .snippet,
+    ec-canned-responses ec-canned-response-row .tag {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-canned-responses ec-canned-response-row .header.closed:hover,
+    ec-canned-responses ec-canned-response-row .header.closed:focus,
+    ec-canned-responses ec-canned-response-row .header.closed:hover .toolbar,
+    ec-canned-responses ec-canned-response-row .header.closed:focus .toolbar {
+  background-color: var(--TWPT-active-background)!important;
+}
+
+ec-canned-responses .filter-row,
+    ec-canned-responses .label-row,
+    ec-canned-responses ec-canned-response-row material-expansionpanel {
+  border-bottom-color: var(--TWPT-subtle-border)!important;
+}
+
+/* Reply button */
+material-fab.reply-button {
+  background-color: var(--TWPT-link)!important;
+  color: var(--TWPT-primary-background)!important;
+}
+
+/* Settings view */
+ec-settings .forum-language-container {
+  border-bottom-color: var(--TWPT-subtle-border)!important;
+}
+
+ec-settings .forum-language-container material-button {
+  color: var(--TWPT-secondary-text)!important;
+}
+
+ec-settings material-checkbox, ec-settings material-checkbox .content {
+  color: var(--TWPT-primary-text)!important;
+}
+
+/* Loading spinner */
+material-spinner {
+  border-color: var(--TWPT-blue-A100)!important;
+}
+
+/* Custom injected components */
+.TWPT-log {
+  background-color: #424242!important;
+}
+
+.TWPT-log-entry.TWPT-log-entry--error {
+  color: #ff8A80!important;
+}
diff --git a/src/static/css/common/console.css b/src/static/css/common/console.css
new file mode 100644
index 0000000..2eba28f
--- /dev/null
+++ b/src/static/css/common/console.css
@@ -0,0 +1,159 @@
+.TWPT-badge {
+  width: calc(18/13*var(--icon-size, 16px));
+  height: calc(18/13*var(--icon-size, 16px));
+  border-radius: 50%;
+
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: center;
+  align-content: center;
+  align-items: center;
+
+  background-color: #009688;
+  color: #fff;
+  box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
+
+  user-select: none;
+}
+
+.TWPT-badge .material-icon-i {
+  font-size: var(--icon-size, 16px);
+}
+
+.TWPT-btn--with-badge {
+  position: relative;
+  padding: 4px;
+  cursor: pointer;
+}
+
+.TWPT-btn--with-badge .content {
+  padding: 8px;
+}
+
+.TWPT-btn--with-badge .TWPT-badge {
+  --icon-size: 13px;
+  position: absolute;
+  bottom: 6px;
+  right: 5px;
+}
+
+.TWPT-dark-theme {
+  padding: 4px 8px!important;
+}
+
+.TWPT-previous-posts {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.TWPT-previous-posts .TWPT-badge {
+  --icon-size: 18px;
+  margin-right: 8px;
+}
+
+.TWPT-dialog {
+  display: block!important;
+  width: 600px;
+  max-width: 100%;
+  padding: 16px 0;
+  background: white;
+  box-shadow: 0 24px 38px 3px rgba(0,0,0,.14), 0 9px 46px 8px rgba(0,0,0,.12), 0 11px 15px -7px rgba(0,0,0,.2);
+}
+
+.TWPT-dialog-header {
+  padding: 24px 24px 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.TWPT-dialog-header--title {
+  color: #202124;
+  font-family: 'Google Sans', sans-serif;
+  font-size: 22px;
+  font-weight: 400;
+  line-height: 24px;
+  margin-bottom: 4px;
+  text-align: center;
+}
+
+.TWPT-dialog-main {
+  font-size: 13px;
+  font-weight: 400;
+  color: rgba(0, 0, 0, .87);
+  overflow: auto;
+  padding: 0 24px;
+}
+
+.TWPT-dialog-footer {
+  padding: 0 24px;
+}
+
+.TWPT-dialog-footer.is-hidden {
+  display: none;
+}
+
+.TWPT-dialog-footer-btn {
+  display: inline-block;
+  float: right;
+  position: relative;
+  height: 36px;
+  min-width: 64px;
+  margin: 0 4px;
+  cursor: pointer;
+}
+
+.TWPT-dialog-footer-btn:hover::after {
+  content: "";
+  display: block;
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: currentColor;
+  outline: 2px solid transparent;
+  opacity: .12;
+  border-radius: inherit;
+  pointer-events: none;
+}
+
+.TWPT-dialog-footer-btn:not(.is-disabled) {
+  color: #1a73e8!important;
+}
+
+.TWPT-dialog-footer-btn.is-disabled {
+  color: #5f6368!important;
+  cursor: not-allowed;
+}
+
+.TWPT-dialog-footer-btn--content {
+  line-height: 36px;
+  text-align: center;
+}
+
+.TWPT-log {
+  max-height: 300px;
+  padding: 0 8px;
+  margin-bottom: 8px;
+  overflow-y: auto;
+  background-color: #e0e0e0;
+}
+
+.TWPT-log-entry {
+  font-family: 'Roboto Mono', 'Courier New', monospace;
+}
+
+.TWPT-log-entry.TWPT-log-entry--error {
+  color: #ff1744;
+}
+
+/*
+ * Fix for the headers' right controls so the dark theme switch has space and
+ * doesn't overlap the search bar.
+ **/
+.material-content > header .right-control {
+  width: auto!important;
+  max-width: 252px!important;
+}
diff --git a/src/static/css/common/forum.css b/src/static/css/common/forum.css
new file mode 100644
index 0000000..50316d7
--- /dev/null
+++ b/src/static/css/common/forum.css
@@ -0,0 +1,36 @@
+.TWPT-badge {
+  width: calc(18/13*var(--icon-size, 16px));
+  height: calc(18/13*var(--icon-size, 16px));
+  border-radius: 50%;
+  margin: 4px;
+
+  display: inline-flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: center;
+  align-content: center;
+  align-items: center;
+
+  background-color: #009688;
+  color: #fff;
+  box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
+
+  user-select: none;
+}
+
+.TWPT-badge img {
+  height: var(--icon-size, 16px);
+  filter: invert(1);
+}
+
+.TWPT-user-profile__user-links {
+  margin-top: 8px;
+}
+
+.TWPT-user-link > * {
+  vertical-align: middle;
+}
+
+.TWPT-user-link .TWPT-badge {
+  margin-left: 0;
+}
diff --git a/src/static/css/enhanced_announcements_dot.css b/src/static/css/enhanced_announcements_dot.css
new file mode 100644
index 0000000..5c23251
--- /dev/null
+++ b/src/static/css/enhanced_announcements_dot.css
@@ -0,0 +1,41 @@
+@keyframes TWPTAnnouncementDot {
+  from {
+    background-color: #d93025;
+    bottom: 8px;
+  }
+
+  to {
+    background-color: #e2b3b0;
+    bottom: 0px;
+  }
+}
+
+@keyframes TWPTAnnouncementDot-v2 {
+  from {
+    background-color: #d93025;
+    top: 4px;
+  }
+
+  to {
+    background-color: #e2b3b0;
+    top: 12px;
+  }
+}
+
+header .left-control .material-drawer-button.has-updates::after,
+    header .right-control material-button.has-updates::after {
+  height: 10px;
+  width: 10px;
+}
+
+header .left-control .material-drawer-button.has-updates::after {
+  animation: .5s infinite alternate ease-in TWPTAnnouncementDot;
+}
+
+header .right-control material-button.has-updates::after {
+  animation: .5s infinite alternate ease-in TWPTAnnouncementDot-v2;
+}
+
+header .sash {
+  display: none;
+}
diff --git a/src/static/css/profileindicator_inject.css b/src/static/css/profileindicator_inject.css
new file mode 100644
index 0000000..55a7d63
--- /dev/null
+++ b/src/static/css/profileindicator_inject.css
@@ -0,0 +1,98 @@
+@keyframes loading-indicator {
+  from {
+    opacity: 0.25;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+ec-user-link.name > *, .thread-message-header__name > *, .thread-message-header__name > span:nth-child(3) > * {
+  vertical-align: middle;
+}
+
+.profile-indicator {
+  display: inline-block;
+  font-size: 16px;
+  margin-left: 4px;
+}
+
+.profile-indicator a {
+  text-decoration: none!important;
+}
+
+.profile-indicator.profile-indicator--loading a {
+  color: #6f6f6f;
+
+  animation-duration: 0.75s;
+  animation-name: loading-indicator;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+}
+
+.profile-indicator.profile-indicator--first-post a {
+  color: #4285f4;
+}
+
+.profile-indicator.profile-indicator--other-posts-read a {
+  color: #FF8F00;
+}
+
+.profile-indicator.profile-indicator--other-posts-unread a {
+  color: #C62828;
+}
+
+.profile-indicator a {
+  opacity: 1;
+  transition: opacity .5s, color .5s;
+}
+
+.num-posts-indicator {
+  display: inline-block;
+  padding: 3px 7px;
+  margin-left: 8px;
+  border-radius: 16px;
+  height: 14px;
+  min-width: 12px;
+
+  font-size: 12px;
+  line-height: 14px;
+  text-align: center;
+
+  background-color: #E0E0E0;
+  color: black;
+  transition: background-color .5s;
+}
+
+.num-posts-indicator.num-posts-indicator--loading {
+  background-color: #AFAFAF;
+
+  animation-duration: 0.75s;
+  animation-name: loading-indicator;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+}
+
+.num-posts-indicator.num-posts-indicator--first-post {
+  background-color: #BBDEFB;
+}
+
+.num-posts-indicator.num-posts-indicator--other-posts-read {
+  background-color: #FFE0B2;
+}
+
+.num-posts-indicator.num-posts-indicator--other-posts-unread {
+  color: white;
+  background-color: #F44336;
+}
+
+.num-posts-indicator .num-posts-indicator--num {
+  display: inline-block;
+  opacity: 1;
+  transition: opacity .5s;
+}
+
+.num-posts-indicator .num-posts-indicator--num.num-posts-indicator--num--loading {
+  opacity: 0;
+}
diff --git a/src/static/css/reposition_expand_thread.css b/src/static/css/reposition_expand_thread.css
new file mode 100644
index 0000000..8df9d2f
--- /dev/null
+++ b/src/static/css/reposition_expand_thread.css
@@ -0,0 +1,20 @@
+ec-thread-summary .panel .main-header {
+  flex-direction: row-reverse;
+}
+
+ec-thread-summary .panel .main-header .expand-container {
+  padding: 0 0 0 8px;
+}
+
+ec-thread-summary .panel .main-header .header {
+  padding-left: 0px!important;
+  padding-right: 16px!important;
+}
+
+ec-thread-list ec-bulk-actions .selection {
+  padding-left: 29px;
+}
+
+ec-thread-summary .panel .main .content-wrapper > .content > .content {
+  padding: 0 8px 0 121px;
+}
diff --git a/src/static/css/thread_list_avatars.css b/src/static/css/thread_list_avatars.css
new file mode 100644
index 0000000..a418005
--- /dev/null
+++ b/src/static/css/thread_list_avatars.css
@@ -0,0 +1,27 @@
+.TWPT-avatars {
+  display: flex;
+  flex-direction: row-reverse;
+  width: 102px;
+  overflow-x: hidden;
+  margin-left: 8px;
+}
+
+.TWPT-avatars .TWPT-avatar {
+  height: 28px;
+  width: 28px;
+  align-self: center;
+  border-width: 0;
+  border-radius: 50%;
+  margin-left: 6px;
+  background-color: white;
+  background-position: center;
+  background-size: contain;
+  background-repeat: no-repeat;
+}
+
+/*
+ * Changing styles of existing elements so the avatars fit.
+ */
+ec-thread-summary .main-header .panel-description a.header .header-content {
+  width: calc(100% - 204px);
+}
diff --git a/src/static/icons/128.png b/src/static/icons/128.png
new file mode 100644
index 0000000..51c3750
--- /dev/null
+++ b/src/static/icons/128.png
Binary files differ
diff --git a/src/static/icons/512.png b/src/static/icons/512.png
new file mode 100644
index 0000000..3ad2dcc
--- /dev/null
+++ b/src/static/icons/512.png
Binary files differ
diff --git a/src/static/options/chrome_style/chrome_style.css b/src/static/options/chrome_style/chrome_style.css
new file mode 100644
index 0000000..4d3c91b
--- /dev/null
+++ b/src/static/options/chrome_style/chrome_style.css
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This stylesheet is used to apply Chrome styles to extension pages that opt in
+ * to using them.
+ *
+ * These styles have been copied from ui/webui/resources/css/chrome_shared.css
+ * and ui/webui/resources/css/widgets.css *with CSS class logic removed*, so
+ * that it's as close to a user-agent stylesheet as possible.
+ *
+ * For example, extensions shouldn't be able to set a .link-button class and
+ * have it do anything.
+ *
+ * Other than that, keep this file and chrome_shared.css/widgets.cc in sync as
+ * much as possible.
+ */
+
+body {
+  color: #333;
+  cursor: default;
+  /* Note that the correct font-family and font-size are set in
+   * extension_fonts.css. */
+  /* This top margin of 14px matches the top padding on the h1 element on
+   * overlays (see the ".overlay .page h1" selector in overlay.css), which
+   * every dialogue has.
+   *
+   * Similarly, the bottom 14px margin matches the bottom padding of the area
+   * which hosts the buttons (see the ".overlay .page * .action-area" selector
+   * in overlay.css).
+   *
+   * Both have a padding left/right of 17px.
+   *
+   * Note that we're putting this here in the Extension content, rather than
+   * the WebUI element which contains the content, so that scrollbars in the
+   * Extension content don't get a 6px margin, which looks quite odd.
+   */
+  margin: 14px 17px;
+}
+
+p {
+  line-height: 1.8em;
+}
+
+h1,
+h2,
+h3 {
+  -webkit-user-select: none;
+  font-weight: normal;
+  /* Makes the vertical size of the text the same for all fonts. */
+  line-height: 1;
+}
+
+h1 {
+  font-size: 1.5em;
+}
+
+h2 {
+  font-size: 1.3em;
+  margin-bottom: 0.4em;
+}
+
+h3 {
+  color: black;
+  font-size: 1.2em;
+  margin-bottom: 0.8em;
+}
+
+a {
+  color: rgb(17, 85, 204);
+  text-decoration: underline;
+}
+
+a:active {
+  color: rgb(5, 37, 119);
+}
+
+/* Default state **************************************************************/
+
+:-webkit-any(button,
+             input[type='button'],
+             input[type='submit']),
+select,
+input[type='checkbox'],
+input[type='radio'] {
+  -webkit-appearance: none;
+  -webkit-user-select: none;
+  background-image: linear-gradient(#ededed, #ededed 38%, #dedede);
+  border: 1px solid rgba(0, 0, 0, 0.25);
+  border-radius: 2px;
+  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
+      inset 0 1px 2px rgba(255, 255, 255, 0.75);
+  color: #444;
+  font: inherit;
+  margin: 0 1px 0 0;
+  outline: none;
+  text-shadow: 0 1px 0 rgb(240, 240, 240);
+}
+
+:-webkit-any(button,
+             input[type='button'],
+             input[type='submit']),
+select {
+  min-height: 2em;
+  min-width: 4em;
+}
+
+:-webkit-any(button,
+             input[type='button'],
+             input[type='submit']) {
+  -webkit-padding-end: 10px;
+  -webkit-padding-start: 10px;
+}
+
+select {
+  -webkit-appearance: none;
+  -webkit-padding-end: 20px;
+  -webkit-padding-start: 6px;
+  /* OVERRIDE */
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUUlEQVR4AWP4TxREZkYxpKHAKKzKEhOZvyG4zN8SE7Eq+6+wYCHbTwiT7eeChf8VsFsKVQhTxIDDbVCFfF8ginApgyp82wRShEcZVJIVzoJDAGqrgIJGRl20AAAAAElFTkSuQmCC),
+      linear-gradient(#ededed, #ededed 38%, #dedede);
+  background-position: right center;
+  background-repeat: no-repeat;
+}
+
+html[dir='rtl'] select {
+  background-position: center left;
+}
+
+input[type='checkbox'] {
+  height: 13px;
+  position: relative;
+  vertical-align: middle;
+  width: 13px;
+}
+
+input[type='radio'] {
+  /* OVERRIDE */
+  border-radius: 100%;
+  height: 15px;
+  position: relative;
+  vertical-align: middle;
+  width: 15px;
+}
+
+/* TODO(estade): add more types here? */
+input[type='number'],
+input[type='password'],
+input[type='search'],
+input[type='text'],
+input[type='url'],
+input:not([type]),
+textarea {
+  border: 1px solid #bfbfbf;
+  border-radius: 2px;
+  box-sizing: border-box;
+  color: #444;
+  font: inherit;
+  margin: 0;
+  /* Use min-height to accommodate addditional padding for touch as needed. */
+  min-height: 2em;
+  padding: 3px;
+  outline: none;
+}
+
+input[type='search'] {
+  -webkit-appearance: textfield;
+  /* NOTE: Keep a relatively high min-width for this so we don't obscure the end
+   * of the default text in relatively spacious languages (i.e. German). */
+  min-width: 160px;
+}
+
+/* Checked ********************************************************************/
+
+input[type='checkbox']:checked::before {
+  -webkit-user-select: none;
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAAAAZ0lEQVR4AWNAA2xAiAXEM8xiMEAXVGJYz7AZCFEkmBi6wYKtEC4/gxqY9gILrmYQhwiXMWxkiAVyVoOFfSCCpkAmCK4Fk+1QA4GqekECUAMkka0KY9gIFvZDd5oawwyGBqACdIDqOwAQzBnTWnnU+gAAAABJRU5ErkJggg==);
+  background-size: 100% 100%;
+  content: '';
+  display: block;
+  height: 100%;
+  width: 100%;
+}
+
+input[type='radio']:checked::before {
+  background-color: #666;
+  border-radius: 100%;
+  bottom: 3px;
+  content: '';
+  display: block;
+  left: 3px;
+  position: absolute;
+  right: 3px;
+  top: 3px;
+}
+
+/* Hover **********************************************************************/
+
+:enabled:hover:-webkit-any(
+    select,
+    input[type='checkbox'],
+    input[type='radio'],
+    :-webkit-any(
+        button,
+        input[type='button'],
+        input[type='submit'])) {
+  background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
+  border-color: rgba(0, 0, 0, 0.3);
+  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12),
+      inset 0 1px 2px rgba(255, 255, 255, 0.95);
+  color: black;
+}
+
+:enabled:hover:-webkit-any(select) {
+  /* OVERRIDE */
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUUlEQVR4AWP4TxREZkYxpKHAKKzKEhOZvyG4zN8SE7Eq+6+wYCHbTwiT7eeChf8VsFsKVQhTxIDDbVCFfF8ginApgyp82wRShEcZVJIVzoJDAGqrgIJGRl20AAAAAElFTkSuQmCC),
+      linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
+}
+
+/* Active *********************************************************************/
+
+:enabled:active:-webkit-any(
+    select,
+    input[type='checkbox'],
+    input[type='radio'],
+    :-webkit-any(
+        button,
+        input[type='button'],
+        input[type='submit'])) {
+  background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
+  box-shadow: none;
+  text-shadow: none;
+}
+
+:enabled:active:-webkit-any(select) {
+  /* OVERRIDE */
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUUlEQVR4AWP4TxREZkYxpKHAKKzKEhOZvyG4zN8SE7Eq+6+wYCHbTwiT7eeChf8VsFsKVQhTxIDDbVCFfF8ginApgyp82wRShEcZVJIVzoJDAGqrgIJGRl20AAAAAElFTkSuQmCC),
+      linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
+}
+
+/* Disabled *******************************************************************/
+
+:disabled:-webkit-any(
+    button,
+    input[type='button'],
+    input[type='submit']),
+select:disabled {
+  background-image: linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
+  border-color: rgba(80, 80, 80, 0.2);
+  box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08),
+      inset 0 1px 2px rgba(255, 255, 255, 0.75);
+  color: #aaa;
+}
+
+select:disabled {
+  /* OVERRIDE */
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAASUlEQVR4AWP4TxREZkYxpKHAKKzKEhMb/iPDxESsyv4rLFiIULRg4X8F7JaCFSIUMeBwG1QhTBEuZVCFb5tAivAog0qywllwCAAavoiLhz+UlAAAAABJRU5ErkJggg==),
+      linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
+}
+
+input:disabled:-webkit-any([type='checkbox'],
+                           [type='radio']) {
+  opacity: .75;
+}
+
+input:disabled:-webkit-any([type='password'],
+                           [type='search'],
+                           [type='text'],
+                           [type='url'],
+                           :not([type])) {
+  color: #999;
+}
+
+/* Focus **********************************************************************/
+
+:enabled:focus:-webkit-any(
+    select,
+    input[type='checkbox'],
+    input[type='number'],
+    input[type='password'],
+    input[type='radio'],
+    input[type='search'],
+    input[type='text'],
+    input[type='url'],
+    input:not([type]),
+    :-webkit-any(
+         button,
+         input[type='button'],
+         input[type='submit'])) {
+  /* OVERRIDE */
+  -webkit-transition: border-color 200ms;
+  /* We use border color because it follows the border radius (unlike outline).
+   * This is particularly noticeable on mac. */
+  border-color: rgb(77, 144, 254);
+  outline: none;
+}
+
+/* Checkbox/radio helpers ******************************************************
+ *
+ * .checkbox and .radio classes wrap labels. Checkboxes and radios should use
+ * these classes with the markup structure:
+ *
+ *   <div class="checkbox">
+ *     <label>
+ *       <input type="checkbox">
+ *       <span>
+ *     </label>
+ *   </div>
+ */
+
+:-webkit-any(.checkbox, .radio) label {
+  /* Don't expand horizontally: <http://crbug.com/112091>. */
+  align-items: center;
+  display: inline-flex;
+  padding-bottom: 7px;
+  padding-top: 7px;
+}
+
+:-webkit-any(.checkbox, .radio) label input {
+  flex-shrink: 0;
+}
+
+:-webkit-any(.checkbox, .radio) label input ~ span {
+  -webkit-margin-start: 0.6em;
+  /* Make sure long spans wrap at the same horizontal position they start. */
+  display: block;
+}
+
+:-webkit-any(.checkbox, .radio) label:hover {
+  color: black;
+}
+
+label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span {
+  color: #999;
+}
diff --git a/src/static/options/experiments.html b/src/static/options/experiments.html
new file mode 100644
index 0000000..81c8a10
--- /dev/null
+++ b/src/static/options/experiments.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Experiments</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="options.css">
+    <link rel="stylesheet" href="chrome_style/chrome_style.css">
+  </head>
+  <body>
+    <main>
+      <h1 data-i18n="experiments_title"></h1>
+      <p data-i18n="experiments_description"></p>
+      <form>
+        <div class="option"><input type="checkbox" id="threadlistavatars"> <label for="threadlistavatars" data-i18n="threadlistavatars"></label></div>
+        <div class="option"><input type="checkbox" id="autorefreshlist"> <label for="autorefreshlist" data-i18n="autorefreshlist"></label></div>
+        <div class="actions"><button id="save" data-i18n="save"></button></div>
+      </form>
+      <div id="save-indicator"></div>
+    </main>
+    <script src="experiments_bit.js"></script>
+    <script src="../../optionsCommon.bundle.js"></script>
+  </body>
+</html>
diff --git a/src/static/options/experiments_bit.js b/src/static/options/experiments_bit.js
new file mode 100644
index 0000000..af8e086
--- /dev/null
+++ b/src/static/options/experiments_bit.js
@@ -0,0 +1 @@
+window.CONTEXT = 'experiments';
diff --git a/src/static/options/options.css b/src/static/options/options.css
new file mode 100644
index 0000000..6933e13
--- /dev/null
+++ b/src/static/options/options.css
@@ -0,0 +1,55 @@
+body {
+  padding-top: 16px;
+}
+
+main {
+  margin: auto;
+  max-width: 600px;
+}
+
+.features-link, .experiments-link {
+  position: absolute;
+  top: 8px;
+  width: 24px;
+}
+
+.features-link {
+  right: 8px;
+}
+
+.experiments-link {
+  cursor: pointer;
+  right: 40px;
+}
+
+.option {
+  margin: 4px 0;
+  line-height: 1.8em;
+}
+
+.help {
+  cursor: help;
+  border-bottom: dashed 1px gray;
+}
+
+.experimental-label {
+  color: gray;
+}
+
+.actions {
+  text-align: center;
+}
+
+#profileindicatoralt_months {
+  width: 3em;
+}
+
+#save-indicator {
+  text-align: center;
+  margin-bottom: 16px;
+  color: green;
+}
+
+[hidden] {
+  display: none;
+}
diff --git a/src/static/options/options.html b/src/static/options/options.html
new file mode 100644
index 0000000..09ce1bb
--- /dev/null
+++ b/src/static/options/options.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Options</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="options.css">
+    <link rel="stylesheet" href="chrome_style/chrome_style.css">
+  </head>
+  <body>
+    <main>
+      <div class="experiments-link" hidden>
+        <!--
+          Material Design Icon - social/science
+           - LICENSE: Apache License Version 2.0
+           - Source: https://github.com/google/material-design-icons/
+           - Author: Google LLC
+        -->
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M13,11.33L18,18H6l5-6.67V6h2 M15.96,4H8.04C7.62,4,7.39,4.48,7.65,4.81L9,6.5v4.17L3.2,18.4C2.71,19.06,3.18,20,4,20h16 c0.82,0,1.29-0.94,0.8-1.6L15,10.67V6.5l1.35-1.69C16.61,4.48,16.38,4,15.96,4L15.96,4z"/></g></svg>
+      </div>
+      <a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/master/docs/features.md" target="_blank" rel="noreferrer noopener" class="features-link">
+        <!--
+          Material Design Icon - action/help_outline
+           - LICENSE: Apache License Version 2.0
+           - Source: https://github.com/google/material-design-icons/
+           - Author: Google LLC
+        -->
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v2h-2zm1.61-9.96c-2.06-.3-3.88.97-4.43 2.79-.18.58.26 1.17.87 1.17h.2c.41 0 .74-.29.88-.67.32-.89 1.27-1.5 2.3-1.28.95.2 1.65 1.13 1.57 2.1-.1 1.34-1.62 1.63-2.45 2.88 0 .01-.01.01-.01.02-.01.02-.02.03-.03.05-.09.15-.18.32-.25.5-.01.03-.03.05-.04.08-.01.02-.01.04-.02.07-.12.34-.2.75-.2 1.25h2c0-.42.11-.77.28-1.07.02-.03.03-.06.05-.09.08-.14.18-.27.28-.39.01-.01.02-.03.03-.04.1-.12.21-.23.33-.34.96-.91 2.26-1.65 1.99-3.56-.24-1.74-1.61-3.21-3.35-3.47z"/></svg>
+      </a>
+      <form>
+        <div class="option"><input type="checkbox" id="list"> <label for="list" data-i18n="list"></label></div>
+        <div class="option"><input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label></div>
+        <div class="option"><input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label></div>
+        <h4 data-i18n="enhancements"></h4>
+        <div class="option"><input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label></div>
+        <div class="option"><input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label></div>
+        <div class="option"><input type="checkbox" id="history"> <label for="history" data-i18n="history"></label></div>
+        <div class="option"><input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label></div>
+        <div class="option"><input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label></div>
+        <div class="option"><input type="checkbox" id="ccdarktheme"> <label for="ccdarktheme" data-i18n="ccdarktheme"></label></div>
+        <div class="option"><input type="checkbox" id="ccforcehidedrawer"> <label for="ccforcehidedrawer" data-i18n="ccforcehidedrawer"></label></div>
+        <div id="dragndrop-wrapper" class="option" hidden><input type="checkbox" id="ccdragndropfix"> <label for="ccdragndropfix" data-i18n="ccdragndropfix"></label></div>
+        <div class="option"><input type="checkbox" id="batchlock"> <label for="batchlock" data-i18n="batchlock"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="enhancedannouncementsdot"> <label for="enhancedannouncementsdot" data-i18n="enhancedannouncementsdot"></label></div>
+        <div class="option"><input type="checkbox" id="repositionexpandthread"> <label for="repositionexpandthread" data-i18n="repositionexpandthread"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="disableunifiedprofiles"> <label for="disableunifiedprofiles" data-i18n="disableunifiedprofiles"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="forcemarkasread"> <label for="forcemarkasread" data-i18n="forcemarkasread"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <h4 data-i18n="profileindicator_header"></h4>
+        <div class="option"><input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="profileindicatoralt"> <label for="profileindicatoralt" data-i18n="profileindicatoralt"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/refs/heads/master/docs/op_indicator.md" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a></div>
+        <div class="actions"><button id="save" data-i18n="save"></button></div>
+      </form>
+      <div id="save-indicator"></div>
+    </main>
+    <script src="options_bit.js"></script>
+    <script src="../../optionsCommon.bundle.js"></script>
+  </body>
+</html>
diff --git a/src/static/options/options_bit.js b/src/static/options/options_bit.js
new file mode 100644
index 0000000..a6186d3
--- /dev/null
+++ b/src/static/options/options_bit.js
@@ -0,0 +1 @@
+window.CONTEXT = 'options';